import 'dart:async'; import 'dart:convert'; import 'package:mobx/mobx.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart'; import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:cake_wallet/bitcoin/utils.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/entities/sync_status.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:cake_wallet/entities/node.dart'; import 'package:cake_wallet/core/wallet_base.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase with Store { BitcoinWalletBase._internal( {@required this.eclient, @required this.path, @required String password, @required WalletInfo walletInfo, @required List initialAddresses, int accountIndex = 0, this.transactionHistory, this.mnemonic, BitcoinBalance initialBalance}) : balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0), hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), network: bitcoin.bitcoin), addresses = initialAddresses != null ? ObservableList.of(initialAddresses) : ObservableList(), syncStatus = NotConnectedSyncStatus(), _password = password, _accountIndex = accountIndex, super(walletInfo) { _scripthashesUpdateSubject = {}; } static BitcoinWallet fromJSON( {@required String password, @required String name, @required String dirPath, String jsonSource}) { final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String; final accountIndex = (data['account_index'] == 'null' || data['account_index'] == null) ? 0 : int.parse(data['account_index'] as String); final _addresses = data['addresses'] as List ?? []; final addresses = []; final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? BitcoinBalance(confirmed: 0, unconfirmed: 0); _addresses.forEach((Object el) { if (el is String) { addresses.add(BitcoinAddressRecord.fromJSON(el)); } }); return BitcoinWalletBase.build( dirPath: dirPath, mnemonic: mnemonic, password: password, name: name, accountIndex: accountIndex, initialAddresses: addresses, initialBalance: balance); } static BitcoinWallet build( {@required String mnemonic, @required String password, @required String name, @required String dirPath, List initialAddresses, BitcoinBalance initialBalance, int accountIndex = 0}) { final walletPath = '$dirPath/$name'; final eclient = ElectrumClient(); final history = BitcoinTransactionHistory( eclient: eclient, dirPath: dirPath, password: password); return BitcoinWallet._internal( eclient: eclient, path: walletPath, mnemonic: mnemonic, password: password, accountIndex: accountIndex, initialAddresses: initialAddresses, initialBalance: initialBalance, transactionHistory: history); } @override final BitcoinTransactionHistory transactionHistory; final String path; final bitcoin.HDWallet hd; final ElectrumClient eclient; final String mnemonic; @override @observable String address; @override @observable BitcoinBalance balance; @override @observable SyncStatus syncStatus; ObservableList addresses; List get scriptHashes => addresses.map((addr) => scriptHash(addr.address)).toList(); String get xpub => hd.base58; @override String get seed => mnemonic; @override BitcoinWalletKeys get keys => BitcoinWalletKeys( wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); final String _password; int _accountIndex; Map> _scripthashesUpdateSubject; Future init() async { if (addresses.isEmpty) { final index = 0; addresses .add(BitcoinAddressRecord(_getAddress(index: index), index: index)); } address = addresses.first.address; transactionHistory.wallet = this; await transactionHistory.init(); } Future generateNewAddress({String label}) async { _accountIndex += 1; final address = BitcoinAddressRecord(_getAddress(index: _accountIndex), index: _accountIndex, label: label); addresses.add(address); await save(); return address; } Future updateAddress(String address, {String label}) async { for (final addr in addresses) { if (addr.address == address) { addr.label = label; await save(); break; } } } @action @override Future startSync() async { try { syncStatus = StartingSyncStatus(); transactionHistory.updateAsync( onFinished: () => print('transactionHistory update finished!')); _subscribeForUpdates(); await _updateBalance(); syncStatus = SyncedSyncStatus(); } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); } } @action @override Future connectToNode({@required Node node}) async { try { syncStatus = ConnectingSyncStatus(); await eclient.connectToUri(node.uri); eclient.onConnectionStatusChange = (bool isConnected) { if (!isConnected) { syncStatus = LostConnectionSyncStatus(); } }; syncStatus = ConnectedSyncStatus(); } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); } } @override Future createTransaction( Object credentials) async { final transactionCredentials = credentials as BitcoinTransactionCredentials; final inputs = []; final fee = _feeMultiplier(transactionCredentials.priority); final amount = transactionCredentials.amount != null ? doubleToBitcoinAmount(transactionCredentials.amount) : balance.total - fee; final totalAmount = amount + fee; final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); var leftAmount = totalAmount; final changeAddress = address; var totalInputAmount = 0; final unspent = addresses.map((address) => eclient .getListUnspentWithAddress(address.address) .then((unspent) => unspent .map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))); for (final unptsFutures in unspent) { final utxs = await unptsFutures; for (final utx in utxs) { final inAmount = utx.value > totalAmount ? totalAmount : utx.value; leftAmount = leftAmount - inAmount; totalInputAmount += inAmount; inputs.add(utx); if (leftAmount <= 0) { break; } } if (leftAmount <= 0) { break; } } if (inputs.isEmpty) { throw BitcoinTransactionNoInputsException(); } if (amount <= 0 || totalInputAmount < amount) { throw BitcoinTransactionWrongBalanceException(); } final changeValue = totalInputAmount - amount - fee; txb.setVersion(1); inputs.forEach((input) { if (input.isP2wpkh) { final p2wpkh = bitcoin .P2WPKH( data: generatePaymentData(hd: hd, index: input.address.index), network: bitcoin.bitcoin) .data; txb.addInput(input.hash, input.vout, null, p2wpkh.output); } else { txb.addInput(input.hash, input.vout); } }); txb.addOutput(transactionCredentials.address, amount); if (changeValue > 0) { txb.addOutput(changeAddress, changeValue); } for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; final keyPair = generateKeyPair(hd: hd, index: input.address.index); final witnessValue = input.isP2wpkh ? input.value : null; txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); } return PendingBitcoinTransaction(txb.build(), eclient: eclient, amount: amount, fee: fee) ..addListener((transaction) => transactionHistory.addOne(transaction)); } String toJSON() => json.encode({ 'mnemonic': mnemonic, 'account_index': _accountIndex.toString(), 'addresses': addresses.map((addr) => addr.toJSON()).toList(), 'balance': balance?.toJSON() }); @override double calculateEstimatedFee(TransactionPriority priority) => bitcoinAmountToDouble(amount: _feeMultiplier(priority)); @override Future save() async => await write(path: path, password: _password, data: toJSON()); bitcoin.ECPair keyPairFor({@required int index}) => generateKeyPair(hd: hd, index: index); @override Future rescan({int height}) async { // FIXME: Unimplemented } void _subscribeForUpdates() { scriptHashes.forEach((sh) async { await _scripthashesUpdateSubject[sh]?.close(); _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh].listen((event) async { transactionHistory.updateAsync(); await _updateBalance(); }); }); } Future _fetchBalances() async { final balances = await Future.wait( scriptHashes.map((sHash) => eclient.getBalance(sHash))); final balance = balances.fold( BitcoinBalance(confirmed: 0, unconfirmed: 0), (BitcoinBalance acc, val) => BitcoinBalance( confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), unconfirmed: (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); return balance; } Future _updateBalance() async { balance = await _fetchBalances(); await save(); } String _getAddress({@required int index}) => generateAddress(hd: hd, index: index); int _feeMultiplier(TransactionPriority priority) { switch (priority) { case TransactionPriority.slow: return 6000; case TransactionPriority.regular: return 9000; case TransactionPriority.fast: return 15000; default: return 0; } } }