import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/pending_transaction.dart'; import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart'; import 'package:cake_wallet/src/domain/common/transaction_history.dart'; import 'package:cake_wallet/src/domain/common/wallet.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; class BitcoinWallet extends Wallet { BitcoinWallet( {@required this.hdwallet, @required this.eclient, @required this.path, @required String password, int accountIndex = 0, this.mnemonic}) : _accountIndex = accountIndex, _password = password, _syncStatus = BehaviorSubject(), _onBalanceChange = BehaviorSubject(), _onAddressChange = BehaviorSubject(), _onNameChange = BehaviorSubject(); @override Observable get onBalanceChange => _onBalanceChange.stream; @override Observable get syncStatus => _syncStatus.stream; @override String get name => path.split('/').last ?? ''; @override String get address => hdwallet.address; String get xpub => hdwallet.base58; final String path; final bitcoin.HDWallet hdwallet; final ElectrumClient eclient; final String mnemonic; BitcoinTransactionHistory history; final BehaviorSubject _syncStatus; final BehaviorSubject _onBalanceChange; final BehaviorSubject _onAddressChange; final BehaviorSubject _onNameChange; BehaviorSubject _addressUpdatesSubject; StreamSubscription _addressUpdatesSubscription; final String _password; int _accountIndex; static Future load( {@required String name, @required String password}) async { final walletDirPath = await pathForWalletDir(name: name, type: WalletType.bitcoin); final walletPath = '$walletDirPath/$name'; final walletJSONRaw = await read(path: walletPath, password: password); final jsoned = json.decode(walletJSONRaw) as Map; final mnemonic = jsoned['mnemonic'] as String; final accountIndex = (jsoned['account_index'] == "null" || jsoned['account_index'] == null) ? 0 : int.parse(jsoned['account_index'] as String); return await build( mnemonic: mnemonic, password: password, name: name, accountIndex: accountIndex); } static Future build( {@required String mnemonic, @required String password, @required String name, int accountIndex = 0}) async { final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), network: bitcoin.bitcoin); final walletDirPath = await pathForWalletDir(name: name, type: WalletType.bitcoin); final walletPath = '$walletDirPath/$name'; final historyPath = '$walletDirPath/transactions.json'; final eclient = ElectrumClient(); final wallet = BitcoinWallet( hdwallet: hd, eclient: eclient, path: walletPath, mnemonic: mnemonic, password: password, accountIndex: accountIndex); final history = BitcoinTransactionHistory( eclient: eclient, path: historyPath, password: password, wallet: wallet); wallet.history = history; await history.init(); await wallet.updateInfo(); return wallet; } List getAddresses() => _accountIndex == 0 ? [address] : List.generate( _accountIndex, (i) => _getAddress(hd: hdwallet, index: i)); Future newAddress() async { _accountIndex += 1; final address = _getAddress(hd: hdwallet, index: _accountIndex); await save(); return address; } @override Future close() async { await _addressUpdatesSubscription?.cancel(); } @override Future connectToNode( {Node node, bool useSSL = false, bool isLightWallet = false}) async { try { // FIXME: Hardcoded server address // final uri = Uri.parse(node.uri); // https://electrum2.hodlister.co:50002 await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); _syncStatus.value = ConnectedSyncStatus(); } catch (e) { print(e.toString()); _syncStatus.value = FailedSyncStatus(); } } @override Future createTransaction( TransactionCreationCredentials credentials) async { final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); final transactions = history.transactionsAll; history.transactionsAll.sort((q, w) => q.height.compareTo(w.height)); final prevTx = transactions.first; txb.setVersion(1); txb.addInput(prevTx, 0); txb.addOutput('address', 112); txb.sign(vin: null, keyPair: null); final hex = txb.build().toHex(); // broadcast transaction to electrum return null; } @override Future getAddress() async => address; @override Future getCurrentHeight() async => 0; @override Future getFilename() async => path.split('/').last ?? ''; @override Future getFullBalance() async => bitcoinAmountToString(amount: _onBalanceChange.value.total); @override TransactionHistory getHistory() => history; @override Future> getKeys() async => {'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey}; @override Future getName() async => path.split('/').last ?? ''; @override Future getNodeHeight() async => 0; @override Future getSeed() async => mnemonic; @override WalletType getType() => WalletType.bitcoin; @override Future getUnlockedBalance() async => bitcoinAmountToString(amount: _onBalanceChange.value.total); @override Future isConnected() async => eclient.isConnected; @override Observable get onAddressChange => _onAddressChange.stream; @override Observable get onNameChange => _onNameChange.stream; @override Future rescan({int restoreHeight = 0}) { // TODO: implement rescan return null; } @override Future startSync() async { _addressUpdatesSubject = eclient.addressUpdate(address: address); _addressUpdatesSubscription = _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); _onBalanceChange.value = await fetchBalance(); getHistory().update(); } @override Future updateInfo() async { _onNameChange.value = await getName(); // _addressUpdatesSubject = eclient.addressUpdate(address: address); // _addressUpdatesSubscription = // _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); _onBalanceChange.value = BitcoinBalance(confirmed: 0, unconfirmed: 0); print(await getKeys()); } Future fetchBalance() async { final balance = await _fetchBalances(); return BitcoinBalance( confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']); } Future save() async => await write( path: path, password: _password, obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()}); String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin .P2PKH( data: PaymentData( pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) .data .address; Future> _fetchBalances() async { final balances = await Future.wait( getAddresses().map((address) => eclient.getBalance(address: address))); final balance = balances.fold(Map(), (Map acc, val) { acc['confirmed'] = (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); acc['unconfirmed'] = (val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0); return acc; }); return balance; } }