diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index d06ffe402..1013eca72 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -4,20 +4,16 @@ import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { try { - return bitcoin.P2PKH( - data: PaymentData(output: script), - network: networkType) - .data - .address!; + return bitcoin.P2PKH(data: PaymentData(output: script), network: networkType).data.address!; } catch (_) {} try { - return bitcoin.P2WPKH( - data: PaymentData(output: script), - network: networkType) - .data - .address!; - } catch(_) {} + return bitcoin.P2WPKH(data: PaymentData(output: script), network: networkType).data.address!; + } catch (_) {} + + try { + return bitcoin.P2TR(data: PaymentData(output: script), network: networkType).data.address!; + } catch (_) {} return ''; -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 392771ab0..ce1391029 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,26 +1,37 @@ import 'dart:convert'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; class BitcoinAddressRecord { BitcoinAddressRecord(this.address, - {required this.index, this.isHidden = false, bool isUsed = false}) - : _isUsed = isUsed; + {required this.index, + this.isHidden = false, + bool isUsed = false, + this.silentAddressLabel, + this.silentPaymentTweak, + this.type}) + : _isUsed = isUsed; factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; - return BitcoinAddressRecord( - decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, - isUsed: decoded['isUsed'] as bool? ?? false); + return BitcoinAddressRecord(decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false, + silentAddressLabel: decoded['silentAddressLabel'] as String?, + silentPaymentTweak: decoded['silentPaymentTweak'] as String?, + type: decoded['type'] != null && decoded['type'] != '' + ? AddressType.values.firstWhere((type) => type.toString() == decoded['type'] as String) + : null); } @override - bool operator ==(Object o) => - o is BitcoinAddressRecord && address == o.address; + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; final String address; final bool isHidden; + final String? silentAddressLabel; + final String? silentPaymentTweak; final int index; bool get isUsed => _isUsed; @@ -31,10 +42,15 @@ class BitcoinAddressRecord { void setAsUsed() => _isUsed = true; - String toJSON() => - json.encode({ + AddressType? type; + + String toJSON() => json.encode({ 'address': address, 'index': index, 'isHidden': isHidden, - 'isUsed': isUsed}); + 'isUsed': isUsed, + 'silentAddressLabel': silentAddressLabel, + 'silentPaymentTweak': silentPaymentTweak, + 'type': type?.toString() ?? '', + }); } diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 9c198c27c..e5be417ec 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -2,14 +2,19 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; class BitcoinUnspent extends Unspent { - BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) + BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout, + {this.silentPaymentTweak, bool? isTaproot}) : bitcoinAddressRecord = addressRecord, + isTaproot = isTaproot ?? false, super(addressRecord.address, hash, value, vout, null); - factory BitcoinUnspent.fromJSON( - BitcoinAddressRecord address, Map json) => - BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, - json['tx_pos'] as int); + factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map json) => + BitcoinUnspent( + address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int, + silentPaymentTweak: json['silent_payment_tweak'] as String?, + isTaproot: json['is_taproot'] as bool?); final BitcoinAddressRecord bitcoinAddressRecord; + String? silentPaymentTweak; + bool isTaproot = false; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 2c66d02fe..734c78fd4 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -22,53 +22,58 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + bitcoin.NetworkType? networkType, required Uint8List seedBytes, List? initialAddresses, ElectrumBalance? initialBalance, int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) + int initialChangeAddressIndex = 0, + bitcoin.SilentPaymentReceiver? silentAddress}) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - networkType: bitcoin.bitcoin, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.btc) { - walletAddresses = BitcoinWalletAddresses( - walletInfo, + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: networkType ?? bitcoin.bitcoin, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.btc, + ) { + walletAddresses = BitcoinWalletAddresses(walletInfo, electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/1"), - networkType: networkType); + sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + networkType: networkType ?? bitcoin.bitcoin, + silentAddress: silentAddress); } - static Future create({ - required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0 - }) async { + static Future create( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + bitcoin.NetworkType? networkType, + List? initialAddresses, + ElectrumBalance? initialBalance, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) async { return BitcoinWallet( mnemonic: mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, + networkType: networkType, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: await mnemonicToSeedBytes(mnemonic), initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + initialChangeAddressIndex: initialChangeAddressIndex, + silentAddress: await bitcoin.SilentPaymentReceiver.fromMnemonic(mnemonic, + hrp: networkType == bitcoin.bitcoin ? 'sp' : 'tsp')); } static Future open({ @@ -77,16 +82,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password); return BitcoinWallet( mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, + networkType: snp.networkType, initialAddresses: snp.addresses, initialBalance: snp.balance, seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + initialChangeAddressIndex: snp.changeAddressIndex, + silentAddress: await bitcoin.SilentPaymentReceiver.fromMnemonic(snp.mnemonic, + hrp: snp.networkType == bitcoin.bitcoin ? 'sp' : 'tsp')); } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 36d37127d..ecef0d064 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -18,7 +18,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required ElectrumClient electrumClient, List? initialAddresses, int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) + int initialChangeAddressIndex = 0, + bitcoin.SilentPaymentReceiver? silentAddress}) : super(walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, @@ -26,9 +27,18 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S mainHd: mainHd, sideHd: sideHd, electrumClient: electrumClient, - networkType: networkType); + networkType: networkType, + silentAddress: silentAddress); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + String getAddress( + {required int index, required bitcoin.HDWallet hd, bitcoin.AddressType? addressType}) { + if (addressType == bitcoin.AddressType.p2pkh) + return generateP2PKHAddress(hd: hd, index: index, networkType: networkType); + + if (addressType == bitcoin.AddressType.p2tr) + return generateP2TRAddress(hd: hd, index: index, networkType: networkType); + + return generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 3a97e0682..444d82c21 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -11,11 +11,10 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -class BitcoinWalletService extends WalletService< - BitcoinNewWalletCredentials, - BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials> { +class BitcoinWalletService extends WalletService { BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -25,12 +24,14 @@ class BitcoinWalletService extends WalletService< WalletType getType() => WalletType.bitcoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await BitcoinWalletBase.create( - mnemonic: await generateMnemonic(), - password: credentials.password!, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + mnemonic: await generateMnemonic(), + password: credentials.password!, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + networkType: isTestnet == true ? bitcoin.testnet : bitcoin.bitcoin, + ); await wallet.save(); await wallet.init(); return wallet; @@ -42,33 +43,36 @@ class BitcoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(name, getType()))!; + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; final wallet = await BitcoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + ); await wallet.init(); return wallet; } @override Future remove(String wallet) async { - File(await pathForWalletDir(name: wallet, type: getType())) - .delete(recursive: true); - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(wallet, getType()))!; + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); } @override Future rename(String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; final currentWallet = await BitcoinWalletBase.open( - password: password, - name: currentName, - walletInfo: currentWalletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + ); await currentWallet.renameWalletFiles(newName); @@ -80,24 +84,23 @@ class BitcoinWalletService extends WalletService< } @override - Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async => + Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials) async => throw UnimplementedError(); @override - Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials) async { if (!validateMnemonic(credentials.mnemonic)) { throw BitcoinMnemonicIsIncorrectException(); } final wallet = await BitcoinWalletBase.create( - password: credentials.password!, - mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + ); await wallet.save(); await wallet.init(); return wallet; } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a05c251fe..d7438061c 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -8,6 +8,7 @@ import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:collection/collection.dart'; +import 'package:http/http.dart' as http; String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); @@ -22,10 +23,7 @@ String jsonrpc( '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { - SocketTask({ - required this.isSubscription, - this.completer, - this.subject}); + SocketTask({required this.isSubscription, this.completer, this.subject}); final Completer? completer; final BehaviorSubject? subject; @@ -51,8 +49,12 @@ class ElectrumClient { Timer? _aliveTimer; String unterminatedString; - Future connectToUri(Uri uri) async => + Uri? uri; + + Future connectToUri(Uri uri) async { + this.uri = uri; await connect(host: uri.host, port: uri.port); + } Future connect({required String host, required int port}) async { try { @@ -104,21 +106,20 @@ class ElectrumClient { } if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; + final response = json.decode(unterminatedString) as Map; _handleResponse(response); unterminatedString = ''; } } on TypeError catch (e) { - if (!e.toString().contains('Map') && !e.toString().contains('Map')) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { return; } unterminatedString += message; if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; + final response = json.decode(unterminatedString) as Map; _handleResponse(response); // unterminatedString = null; unterminatedString = ''; @@ -142,8 +143,7 @@ class ElectrumClient { } } - Future> version() => - call(method: 'server.version').then((dynamic result) { + Future> version() => call(method: 'server.version').then((dynamic result) { if (result is List) { return result.map((dynamic val) => val.toString()).toList(); } @@ -180,9 +180,8 @@ class ElectrumClient { Future>> getListUnspentWithAddress( String address, NetworkType networkType) => call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, networkType: networkType)]) - .then((dynamic result) { + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address, networkType: networkType)]).then((dynamic result) { if (result is List) { return result.map((dynamic val) { if (val is Map) { @@ -229,19 +228,25 @@ class ElectrumClient { return []; }); - Future> getTransactionRaw( - {required String hash}) async => - callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) + Future getTransactionRaw( + {required String hash, required NetworkType networkType}) async => + callWithTimeout( + method: 'blockchain.transaction.get', + params: networkType.bech32 == bitcoin.bech32 ? [hash, true] : [hash], + timeout: 10000) .then((dynamic result) { if (result is Map) { return result; } + if (networkType.bech32 == testnet.bech32 && result is String) { + return result; + } + return {}; }); - Future getTransactionHex( - {required String hash}) async => + Future getTransactionHex({required String hash}) async => callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) .then((dynamic result) { if (result is String) { @@ -251,30 +256,41 @@ class ElectrumClient { return ''; }); - Future broadcastTransaction( - {required String transactionRaw}) async => - call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) - .then((dynamic result) { - if (result is String) { - return result; - } + Future broadcastTransaction({required String transactionRaw}) async { + return http + .post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'), + headers: {'Content-Type': 'application/json; charset=utf-8'}, + body: transactionRaw) + .then((http.Response response) { + print(response.body); + if (response.statusCode == 200) { + return response.body; + } - return ''; - }); + return ''; + }); + return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) + .then((dynamic result) { + if (result is String) { + return result; + } - Future> getMerkle( - {required String hash, required int height}) async => - await call( - method: 'blockchain.transaction.get_merkle', - params: [hash, height]) as Map; + return ''; + }); + } - Future> getHeader({required int height}) async => - await call(method: 'blockchain.block.get_header', params: [height]) + Future getTxidFromPos({required int height, required int pos}) async => + await call(method: 'blockchain.transaction.id_from_pos', params: [height, pos]) as String; + + Future> getMerkle({required String hash, required int height}) async => + await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map; + Future> getHeader({required int height}) async => + await call(method: 'blockchain.block.get_header', params: [height]) as Map; + Future estimatefee({required int p}) => - call(method: 'blockchain.estimatefee', params: [p]) - .then((dynamic result) { + call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { if (result is double) { return result; } @@ -319,15 +335,9 @@ class ElectrumClient { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); final bottomDoubleString = await estimatefee(p: 100); - final top = - (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) - .round(); - final middle = - (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) - .round(); - final bottom = - (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) - .round(); + final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); + final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); + final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); return [bottom, middle, top]; } catch (_) { @@ -335,6 +345,27 @@ class ElectrumClient { } } + // https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe + // example response: + // { + // "height": 520481, + // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" + // } + Future getCurrentBlockChainTip() => + call(method: 'blockchain.headers.subscribe').then((result) { + if (result is Map) { + return result["height"] as int; + } + + return null; + }); + + BehaviorSubject? chainTipUpdate() { + _id += 1; + return subscribe( + id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe'); + } + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( @@ -344,16 +375,14 @@ class ElectrumClient { } BehaviorSubject? subscribe( - {required String id, - required String method, - List params = const []}) { + {required String id, required String method, List params = const []}) { try { final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); socket!.write(jsonrpc(method: method, id: _id, params: params)); return subscription; - } catch(e) { + } catch (e) { print(e.toString()); return null; } @@ -370,9 +399,7 @@ class ElectrumClient { } Future callWithTimeout( - {required String method, - List params = const [], - int timeout = 4000}) async { + {required String method, List params = const [], int timeout = 4000}) async { try { final completer = Completer(); _id += 1; @@ -386,7 +413,7 @@ class ElectrumClient { }); return completer.future; - } catch(e) { + } catch (e) { print(e.toString()); } } @@ -397,8 +424,8 @@ class ElectrumClient { onConnectionStatusChange = null; } - void _registryTask(int id, Completer completer) => _tasks[id.toString()] = - SocketTask(completer: completer, isSubscription: false); + void _registryTask(int id, Completer completer) => + _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false); void _regisrySubscription(String id, BehaviorSubject subject) => _tasks[id] = SocketTask(subject: subject, isSubscription: true); @@ -419,8 +446,7 @@ class ElectrumClient { } } - void _methodHandler( - {required String method, required Map request}) { + void _methodHandler({required String method, required Map request}) { switch (method) { case 'blockchain.scripthash.subscribe': final params = request['params'] as List; @@ -451,8 +477,8 @@ class ElectrumClient { _methodHandler(method: method, request: response); return; } - - if (id != null){ + + if (id != null) { _finish(id, result); } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index bf5ec2c4f..62cc79dd3 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -10,9 +10,7 @@ import 'package:cw_core/wallet_type.dart'; class ElectrumTransactionBundle { ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, - required this.confirmations, - this.time}); + {required this.ins, required this.confirmations, this.time}); final bitcoin.Transaction originalTransaction; final List ins; final int? time; @@ -39,8 +37,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.confirmations = confirmations; } - factory ElectrumTransactionInfo.fromElectrumVerbose( - Map obj, WalletType type, + factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, {required List addresses, required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; @@ -58,10 +55,8 @@ class ElectrumTransactionInfo extends TransactionInfo { for (dynamic vin in vins) { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; - final outAddresses = - (out['scriptPubKey']['addresses'] as List?)?.toSet(); - inputsAmount += - stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); + final outAddresses = (out['scriptPubKey']['addresses'] as List?)?.toSet(); + inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -69,11 +64,9 @@ class ElectrumTransactionInfo extends TransactionInfo { } for (dynamic out in vout) { - final outAddresses = - out['scriptPubKey']['addresses'] as List? ?? []; + final outAddresses = out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount( - (out['value'] as double? ?? 0.0).toString()); + final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -96,14 +89,11 @@ class ElectrumTransactionInfo extends TransactionInfo { } factory ElectrumTransactionInfo.fromElectrumBundle( - ElectrumTransactionBundle bundle, - WalletType type, - bitcoin.NetworkType networkType, - {required Set addresses, - required int height}) { + ElectrumTransactionBundle bundle, WalletType type, bitcoin.NetworkType networkType, + {required Set addresses, required int height}) { final date = bundle.time != null - ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) - : DateTime.now(); + ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) + : DateTime.now(); var direction = TransactionDirection.incoming; var amount = 0; var inputAmount = 0; @@ -152,8 +142,8 @@ class ElectrumTransactionInfo extends TransactionInfo { if (addresses != null) { tx.outs.forEach((out) { try { - final p2pkh = bitcoin.P2PKH( - data: PaymentData(output: out.script), network: bitcoin.bitcoin); + final p2pkh = + bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); exist = addresses.contains(p2pkh.data.address); if (exist) { @@ -163,9 +153,8 @@ class ElectrumTransactionInfo extends TransactionInfo { }); } - final date = timestamp != null - ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) - : DateTime.now(); + final date = + timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); return ElectrumTransactionInfo(type, id: tx.getId(), @@ -178,8 +167,7 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory ElectrumTransactionInfo.fromJson( - Map data, WalletType type) { + factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { return ElectrumTransactionInfo(type, id: data['id'] as String, height: data['height'] as int, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 05486aa20..f5f2b1075 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,8 +1,16 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math'; +import 'package:http/http.dart' as http; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; +import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_to_output_script.dart'; @@ -17,7 +25,6 @@ import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/file.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; @@ -27,16 +34,10 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:rxdart/subjects.dart'; part 'electrum_wallet.g.dart'; @@ -115,6 +116,9 @@ abstract class ElectrumWalletBase @override String get seed => mnemonic; + @override + String get password => _password; + bitcoin.NetworkType networkType; @override @@ -125,21 +129,80 @@ abstract class ElectrumWalletBase List unspentCoins; List _feeRates; Map?> _scripthashesUpdateSubject; + BehaviorSubject? _chainTipUpdateSubject; bool _isTransactionUpdating; + Future? _isolate; void Function(FlutterErrorDetails)? _onError; + Timer? _autoSaveTimer; + static const int _autoSaveInterval = 30; Future init() async { await walletAddresses.init(); await transactionHistory.init(); - await save(); + + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + } + + void _setListeners(int height, {int? chainTip}) async { + final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0; + syncStatus = AttemptingSyncStatus(); + + if (_isolate != null) { + final runningIsolate = await _isolate!; + runningIsolate.kill(priority: Isolate.immediate); + } + + final receivePort = ReceivePort(); + _isolate = Isolate.spawn( + startRefresh, + ScanData( + sendPort: receivePort.sendPort, + silentAddress: walletAddresses.silentAddress!.toString(), + scanPrivkeyCompressed: + walletAddresses.silentAddress!.scanPrivkey.toCompressedHex().fromHex, + spendPubkeyCompressed: + walletAddresses.silentAddress!.spendPubkey.toCompressedHex().fromHex, + networkType: networkType, + height: height, + chainTip: currentChainTip, + electrumClient: ElectrumClient(), + transactionHistoryIds: transactionHistory.transactions.keys.toList(), + node: electrumClient.uri.toString(), + )); + + await for (var message in receivePort) { + if (message is BitcoinUnspent) { + unspentCoins.add(message); + walletAddresses.addresses.add(message.bitcoinAddressRecord); + await save(); + + await updateUnspent(); + await updateBalance(); + await updateTransactions(); + _subscribeForUpdates(); + } + + // check if is a SyncStatus type since "is SyncStatus" doesn't work here + if (message is SyncResponse) { + syncStatus = message.syncStatus; + walletInfo.restoreHeight = message.height; + await walletInfo.save(); + } + } } @action @override Future startSync() async { try { - syncStatus = AttemptingSyncStatus(); + await _setInitialHeight(); + } catch (_) {} + + try { + rescan(height: walletInfo.restoreHeight); + await walletAddresses.discoverAddresses(); await updateTransactions(); _subscribeForUpdates(); @@ -149,8 +212,6 @@ abstract class ElectrumWalletBase Timer.periodic( const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); - - syncStatus = SyncedSyncStatus(); } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -170,6 +231,12 @@ abstract class ElectrumWalletBase } }; syncStatus = ConnectedSyncStatus(); + + final currentChainTip = await electrumClient.getCurrentBlockChainTip(); + + if ((currentChainTip ?? 0) > walletInfo.restoreHeight) { + _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip); + } } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); @@ -178,173 +245,236 @@ abstract class ElectrumWalletBase @override Future createTransaction(Object credentials) async { - const minAmount = 546; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final inputs = []; - final outputs = transactionCredentials.outputs; - final hasMultiDestination = outputs.length > 1; - var allInputsAmount = 0; + try { + const minAmount = 546; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final inputs = []; + final outputs = transactionCredentials.outputs; + final hasMultiDestination = outputs.length > 1; + var allInputsAmount = 0; - if (unspentCoins.isEmpty) { - await updateUnspent(); - } - - for (final utx in unspentCoins) { - if (utx.isSending) { - allInputsAmount += utx.value; - inputs.add(utx); - } - } - - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - final allAmountFee = transactionCredentials.feeRate != null - ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) - : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); - - final allAmount = allInputsAmount - allAmountFee; - - var credentialsAmount = 0; - var amount = 0; - var fee = 0; - - if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { - throw BitcoinTransactionWrongBalanceException(currency); + if (unspentCoins.isEmpty) { + await updateUnspent(); } - credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); - - if (allAmount - credentialsAmount < minAmount) { - throw BitcoinTransactionWrongBalanceException(currency); + for (int i = 0; i < unspentCoins.length; i++) { + final utx = unspentCoins[i]; + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); + } } - amount = credentialsAmount; + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } - if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, - outputsCount: outputs.length + 1); + final allAmountFee = transactionCredentials.feeRate != null + ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) + : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); + + final allAmount = allInputsAmount - allAmountFee; + + var credentialsAmount = 0; + var amount = 0; + var fee = 0; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); + + if (allAmount - credentialsAmount < minAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = credentialsAmount; + + if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, + outputsCount: outputs.length + 1); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount, + outputsCount: outputs.length + 1); + } } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount, - outputsCount: outputs.length + 1); - } - } else { - final output = outputs.first; - credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0; + final output = outputs.first; + credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0; - if (credentialsAmount > allAmount) { + if (credentialsAmount > allAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = output.sendAll || allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + + if (output.sendAll || amount == allAmount) { + fee = allAmountFee; + } else if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount); + } + } + + if (fee == 0 && networkType == bitcoin.bitcoin) { throw BitcoinTransactionWrongBalanceException(currency); } - amount = output.sendAll || allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; + final totalAmount = amount + fee; - if (output.sendAll || amount == allAmount) { - fee = allAmountFee; - } else if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); - } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount); + if ((totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) && + networkType == bitcoin.bitcoin) { + throw BitcoinTransactionWrongBalanceException(currency); } - } - if (fee == 0) { - throw BitcoinTransactionWrongBalanceException(currency); - } + final changeAddress = await walletAddresses.getChangeAddress(); + var leftAmount = totalAmount; + var totalInputAmount = 0; - final totalAmount = amount + fee; + final txb = bitcoin.TransactionBuilder(network: networkType, version: 1); - if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } + List inputPrivKeys = []; + List outpoints = []; - final txb = bitcoin.TransactionBuilder(network: networkType); - final changeAddress = await walletAddresses.getChangeAddress(); - var leftAmount = totalAmount; - var totalInputAmount = 0; - - inputs.clear(); - - for (final utx in unspentCoins) { - if (utx.isSending) { + for (int i = 0; i < inputs.length; i++) { + final utx = inputs[i]; leftAmount = leftAmount - utx.value; totalInputAmount += utx.value; - inputs.add(utx); + + outpoints.add(bitcoin.Outpoint(txid: utx.hash, index: utx.vout)); + + if (utx.bitcoinAddressRecord.silentPaymentTweak != null) { + // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending + final d = bitcoin.PrivateKey.fromHex(bitcoin.getSecp256k1(), + walletAddresses.silentAddress!.spendPrivkey.toCompressedHex()) + .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!.fromHex.bigint)!; + + inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, utx.isTaproot)); + + final point = bitcoin.ECPublic.fromHex(d.publicKey.toHex()).toTapPoint(); + final p2tr = bitcoin.P2trAddress(program: point); + + bitcoin.ECPair keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex, + compressed: true, network: networkType); + + txb.addInput( + utx.hash, utx.vout, null, p2tr.toScriptPubKey().toBytes(), keyPair, utx.value); + continue; + } + + inputPrivKeys.add(bitcoin.PrivateKeyInfo( + bitcoin.PrivateKey.fromHex( + bitcoin.getSecp256k1(), + generateKeyPair( + hd: utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: networkType) + .privateKey! + .hex), + utx.isTaproot)); + + bitcoin.ECPair keyPair = generateKeyPair( + hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: networkType); + + if (utx.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData( + hd: utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index), + network: networkType) + .data; + + txb.addInput(utx.hash, utx.vout, null, p2wpkh.output, keyPair, utx.value); + continue; + } + + txb.addInput(utx.hash, utx.vout, null, null, keyPair, utx.value); if (leftAmount <= 0) { break; } } - } - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - if (amount <= 0 || totalInputAmount < totalAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - txb.setVersion(1); - inputs.forEach((input) { - if (input.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData( - hd: input.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index), - network: networkType) - .data; - - txb.addInput(input.hash, input.vout, null, p2wpkh.output); - } else { - txb.addInput(input.hash, input.vout); + if (txb.inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); } - }); - outputs.forEach((item) { - final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; - final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; - txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!); - }); + if (amount <= 0 || totalInputAmount < totalAmount) { + // throw BitcoinTransactionWrongBalanceException(currency); + } - final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1); - var feeAmount = 0; - - if (transactionCredentials.feeRate != null) { - feeAmount = transactionCredentials.feeRate! * estimatedSize; - } else { - feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; - } - - final changeValue = totalInputAmount - amount - feeAmount; - - if (changeValue > minAmount) { - txb.addOutput(changeAddress, changeValue); - } - - for (var i = 0; i < inputs.length; i++) { - final input = inputs[i]; - final keyPair = generateKeyPair( - hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index, - network: networkType); - final witnessValue = input.isP2wpkh ? input.value : null; - - txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); - } - - return PendingBitcoinTransaction(txb.build(), type, - electrumClient: electrumClient, amount: amount, fee: fee) - ..addListener((transaction) async { - transactionHistory.addOne(transaction); - await updateBalance(); + List silentPaymentDestinations = []; + outputs.forEach((item) { + final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; + final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; + if (outputAddress.startsWith('tsp1')) { + silentPaymentDestinations + .add(bitcoin.SilentPaymentDestination.fromAddress(outputAddress, outputAmount!)); + } else { + txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!); + } }); + + if (silentPaymentDestinations.isNotEmpty) { + final outpointsHash = bitcoin.SilentPayment.hashOutpoints(outpoints); + final generatedOutputs = bitcoin.SilentPayment.generateMultipleRecipientPubkeys( + inputPrivKeys, outpointsHash, silentPaymentDestinations); + + generatedOutputs.forEach((recipientSilentAddress, generatedOutput) { + generatedOutput.forEach((output) { + final generatedPubkey = output.$1.toHex(); + // TODO: DRY code: pubkeyToOutputScript (?) + final point = bitcoin.ECPublic.fromHex(generatedPubkey).toTapPoint(); + final p2tr = bitcoin.P2trAddress(program: point); + print([p2tr.toScriptPubKey().toBytes(), output.$2]); + txb.addOutput(p2tr.toScriptPubKey().toBytes(), output.$2); + }); + }); + } + + final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1); + var feeAmount = 0; + + if (transactionCredentials.feeRate != null) { + feeAmount = transactionCredentials.feeRate! * estimatedSize; + } else { + feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; + } + + final changeValue = totalInputAmount - amount - feeAmount; + + if (changeValue > minAmount) { + txb.addOutput(changeAddress, changeValue); + } + + final amounts = txb.inputs.map((utx) => utx.value!).toList(); + final scriptPubKeys = txb.inputs.map((utx) => utx.prevOutScript!).toList(); + for (var i = 0; i < inputs.length; i++) { + txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: inputs); + } + + return PendingBitcoinTransaction(txb.build(), type, + electrumClient: electrumClient, amount: amount, fee: fee) + ..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + rethrow; + } } String toJSON() => json.encode({ @@ -352,7 +482,8 @@ abstract class ElectrumWalletBase 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance[currency]?.toJSON() + 'balance': balance[currency]?.toJSON(), + 'network_type': networkType == bitcoin.bitcoin ? 'mainnet' : 'testnet', }); int feeRate(TransactionPriority priority) { @@ -455,14 +586,23 @@ abstract class ElectrumWalletBase bitcoin.ECPair keyPairFor({required int index}) => generateKeyPair(hd: hd, index: index, network: networkType); + @action @override - Future rescan({required int height}) async => throw UnimplementedError(); + Future rescan({required int height, int? chainTip, ScanData? scanData}) async { + if (height >= walletInfo.restoreHeight) { + syncStatus = SyncedSyncStatus(); + return; + } + + _setListeners(height); + } @override Future close() async { try { await electrumClient.close(); } catch (_) {} + _autoSaveTimer?.cancel(); } Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); @@ -477,10 +617,14 @@ abstract class ElectrumWalletBase return null; } }).whereNotNull()))); - unspentCoins = unspent.expand((e) => e).toList(); - unspentCoins.forEach((coin) async { - final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); - coin.isChange = tx!.direction == TransactionDirection.outgoing; + unspent.expand((e) => e).forEach((newUnspent) { + try { + if (!unspentCoins.any((currentUnspent) => + currentUnspent.address.contains(newUnspent.address) && + currentUnspent.hash.contains(newUnspent.hash))) { + unspentCoins.add(newUnspent); + } + } catch (_) {} }); if (unspentCoinsInfo.isEmpty) { @@ -490,8 +634,10 @@ abstract class ElectrumWalletBase if (unspentCoins.isNotEmpty) { unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.address.contains(coin.address)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -548,37 +694,6 @@ abstract class ElectrumWalletBase } } - Future getTransactionExpanded( - {required String hash, required int height}) async { - final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); - final transactionHex = verboseTransaction['hex'] as String; - final original = bitcoin.Transaction.fromHex(transactionHex); - final ins = []; - final time = verboseTransaction['time'] as int?; - final confirmations = verboseTransaction['confirmations'] as int? ?? 0; - - for (final vin in original.ins) { - final id = HEX.encode(vin.hash!.reversed.toList()); - final txHex = await electrumClient.getTransactionHex(hash: id); - final tx = bitcoin.Transaction.fromHex(txHex); - ins.add(tx); - } - - return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations); - } - - Future fetchTransactionInfo( - {required String hash, required int height}) async { - try { - final tx = await getTransactionExpanded(hash: hash, height: height); - final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); - return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType, - addresses: addresses, height: height); - } catch (_) { - return null; - } - } - @override Future> fetchTransactions() async { final addressHashes = {}; @@ -602,7 +717,12 @@ abstract class ElectrumWalletBase final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { try { return fetchTransactionInfo( - hash: transaction['tx_hash'] as String, height: transaction['height'] as int); + hash: transaction['tx_hash'] as String, + height: transaction['height'] as int, + electrumClient: electrumClient, + addressRecords: walletAddresses.addresses, + walletInfo: walletInfo, + networkType: networkType); } catch (_) { return Future.value(null); } @@ -636,7 +756,7 @@ abstract class ElectrumWalletBase } } - void _subscribeForUpdates() { + void _subscribeForUpdates() async { scriptHashes.forEach((sh) async { await _scripthashesUpdateSubject[sh]?.close(); _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); @@ -645,6 +765,9 @@ abstract class ElectrumWalletBase await updateUnspent(); await updateBalance(); await updateTransactions(); + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + rescan(height: walletInfo.restoreHeight); } catch (e, s) { print(e.toString()); _onError?.call(FlutterErrorDetails( @@ -655,6 +778,20 @@ abstract class ElectrumWalletBase } }); }); + await _chainTipUpdateSubject?.close(); + _chainTipUpdateSubject = electrumClient.chainTipUpdate(); + _chainTipUpdateSubject!.listen((event) async { + try { + rescan(height: walletInfo.restoreHeight); + } catch (e, s) { + print(e.toString()); + _onError?.call(FlutterErrorDetails( + exception: e, + stack: s, + library: this.runtimeType.toString(), + )); + } + }); } Future _fetchBalances() async { @@ -729,4 +866,348 @@ abstract class ElectrumWalletBase ? base64Encode(hd.sign(message)) : base64Encode(hd.derive(index).sign(message)); } + + Future _setInitialHeight() async { + if (walletInfo.isRecovery) { + return; + } + + if (walletInfo.restoreHeight == 0) { + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + } + } +} + +Future fetchTransactionInfo( + {required String hash, + required int height, + required ElectrumClient electrumClient, + required Iterable addressRecords, + required WalletInfo walletInfo, + required bitcoin.NetworkType networkType}) async { + try { + final tx = await getTransactionExpanded( + hash: hash, height: height, electrumClient: electrumClient, networkType: networkType); + final addresses = addressRecords.map((addr) => addr.address).toSet(); + return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType, + addresses: addresses, height: height); + } catch (_) { + return null; + } +} + +Future getTransactionExpanded( + {required String hash, + required int height, + required ElectrumClient electrumClient, + required bitcoin.NetworkType networkType}) async { + final verboseTransaction = + await electrumClient.getTransactionRaw(hash: hash, networkType: networkType); + + String transactionHex; + int? time; + int confirmations = 0; + if (networkType.bech32 == bitcoin.testnet.bech32) { + transactionHex = verboseTransaction as String; + confirmations = 1; + } else { + transactionHex = verboseTransaction['hex'] as String; + time = verboseTransaction['time'] as int?; + confirmations = verboseTransaction['confirmations'] as int? ?? 0; + } + + final original = bitcoin.Transaction.fromHex(transactionHex); + final ins = []; + + for (final vin in original.ins) { + final id = HEX.encode(vin.hash!.reversed.toList()); + final txHex = await electrumClient.getTransactionHex(hash: id); + final tx = bitcoin.Transaction.fromHex(txHex); + ins.add(tx); + } + + return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations); +} + +class ScanData { + final SendPort sendPort; + final Uint8List scanPrivkeyCompressed; + final Uint8List spendPubkeyCompressed; + final String silentAddress; + final int height; + final String node; + final bitcoin.NetworkType networkType; + final int chainTip; + final ElectrumClient electrumClient; + final List transactionHistoryIds; + + ScanData({ + required this.sendPort, + required this.scanPrivkeyCompressed, + required this.spendPubkeyCompressed, + required this.silentAddress, + required this.height, + required this.node, + required this.networkType, + required this.chainTip, + required this.electrumClient, + required this.transactionHistoryIds, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + sendPort: scanData.sendPort, + scanPrivkeyCompressed: scanData.scanPrivkeyCompressed, + spendPubkeyCompressed: scanData.spendPubkeyCompressed, + silentAddress: scanData.silentAddress, + height: newHeight, + node: scanData.node, + networkType: scanData.networkType, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + electrumClient: scanData.electrumClient, + ); + } +} + +class SyncResponse { + final int height; + final SyncStatus syncStatus; + + SyncResponse(this.height, this.syncStatus); +} + +Future startRefresh(ScanData scanData) async { + var cachedBlockchainHeight = scanData.chainTip; + + Future getNodeHeightOrUpdate(int baseHeight) async { + if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) { + final electrumClient = scanData.electrumClient; + if (!electrumClient.isConnected) { + final node = scanData.node; + await electrumClient.connectToUri(Uri.parse(node)); + } + + cachedBlockchainHeight = + await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight; + } + + return cachedBlockchainHeight; + } + + var lastKnownBlockHeight = 0; + var initialSyncHeight = 0; + + var syncHeight = scanData.height; + var currentChainTip = scanData.chainTip; + + if (syncHeight <= 0) { + syncHeight = currentChainTip; + } + + if (initialSyncHeight <= 0) { + initialSyncHeight = syncHeight; + } + + if (lastKnownBlockHeight == syncHeight) { + scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); + return; + } + + // Run this until no more blocks left to scan txs. At first this was recursive + // i.e. re-calling the startRefresh function but this was easier for the above values to retain + // their initial values + while (true) { + lastKnownBlockHeight = syncHeight; + + final syncingStatus = + SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + if (syncingStatus.blocksLeft <= 0) { + scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); + return; + } + + print(["Scanning from height:", syncHeight]); + + try { + final networkPath = + scanData.networkType.network == bitcoin.BtcNetwork.mainnet ? "" : "/testnet"; + + // This endpoint gets up to 10 latest blocks from the given height + final tenNewestBlocks = + (await http.get(Uri.parse("https://blockstream.info$networkPath/api/blocks/$syncHeight"))) + .body; + var decodedBlocks = json.decode(tenNewestBlocks) as List; + + decodedBlocks.sort((a, b) => (a["height"] as int).compareTo(b["height"] as int)); + decodedBlocks = + decodedBlocks.where((element) => (element["height"] as int) >= syncHeight).toList(); + + // for each block, get up to 25 txs + for (var i = 0; i < decodedBlocks.length; i++) { + final blockJson = decodedBlocks[i]; + final blockHash = blockJson["id"]; + final txCount = blockJson["tx_count"] as int; + + // print(["Scanning block index:", i, "with tx count:", txCount]); + + int startIndex = 0; + // go through each tx in block until no more txs are left + while (startIndex < txCount) { + // This endpoint gets up to 25 txs from the given block hash and start index + final twentyFiveTxs = json.decode((await http.get(Uri.parse( + "https://blockstream.info$networkPath/api/block/$blockHash/txs/$startIndex"))) + .body) as List; + + // print(["Scanning txs index:", startIndex]); + + // For each tx, apply silent payment filtering and do shared secret calculation when applied + for (var i = 0; i < twentyFiveTxs.length; i++) { + try { + final tx = twentyFiveTxs[i]; + final txid = tx["txid"] as String; + + // print(["Scanning tx:", txid]); + + // TODO: if tx already scanned & stored skip + // if (scanData.transactionHistoryIds.contains(txid)) { + // // already scanned tx, continue to next tx + // pos++; + // continue; + // } + + List pubkeys = []; + List outpoints = []; + + bool skip = false; + + for (var i = 0; i < (tx["vin"] as List).length; i++) { + final input = tx["vin"][i]; + if (input["witness"] == null) { + skip = true; + // print("Skipping, no witness"); + break; + } + + if (input["witness"].length != 2) { + skip = true; + // print("Skipping, invalid witness"); + break; + } + + final pubkey = input["witness"][1] as String; + pubkeys.add(pubkey); + outpoints.add(bitcoin.Outpoint( + txid: HEX.encode((input["txid"] as String).fromHex.reversed.toList()), + index: input["vout"] as int)); + } + + if (skip) { + // skipped tx, continue to next tx + continue; + } + + Map outpointsByP2TRpubkey = {}; + for (var i = 0; i < (tx["vout"] as List).length; i++) { + final output = tx["vout"][i]; + if (output["scriptpubkey_type"] != "v1_p2tr") { + // print("Skipping, not a v1_p2tr output"); + break; + } + + final script = (output["scriptpubkey"] as String).fromHex; + + // final alreadySpentOutput = (await electrumClient.getHistory( + // scriptHashFromScript(script, networkType: scanData.networkType))) + // .length > + // 1; + + // if (alreadySpentOutput) { + // print("Skipping, invalid witness"); + // break; + // } + + final p2tr = bitcoin.P2trAddress(program: script.sublist(2).hex); + final address = p2tr.toAddress(scanData.networkType); + + // print(["Verifying taproot address:", address]); + + outpointsByP2TRpubkey[p2tr.toScriptPubKey().toBytes()] = + bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int); + } + + if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) { + // skipped tx, continue to next tx + continue; + } + + final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); + + final result = bitcoin.scanOutputs( + bitcoin.PrivateKey.fromHex( + bitcoin.getSecp256k1(), scanData.scanPrivkeyCompressed.hex), + bitcoin.PublicKey.fromHex( + bitcoin.getSecp256k1(), scanData.spendPubkeyCompressed.hex), + bitcoin.getSumInputPubKeys(pubkeys), + outpointHash, + outpointsByP2TRpubkey.keys.toList()); + + if (result.isEmpty) { + // no results tx, continue to next tx + continue; + } + + if (result.length > 1) { + print("MULTIPLE UNSPENT COINS FOUND!"); + } else { + print("UNSPENT COIN FOUND!"); + } + print(result); + + result.forEach((key, value) { + final outpoint = outpointsByP2TRpubkey[key.fromHex]; + + if (outpoint == null) { + return; + } + + // found utxo for tx + scanData.sendPort.send(BitcoinUnspent( + BitcoinAddressRecord( + key, + index: 0, + isHidden: false, + isUsed: true, + silentAddressLabel: null, + silentPaymentTweak: value, + ), + outpoint.txid, + outpoint.value!, + outpoint.index, + silentPaymentTweak: value, + )); + }); + } catch (_) {} + } + + // Finished scanning batch of txs in block, add 25 to start index and continue to next block in loop + startIndex += 25; + } + + // Finished scanning block, add 1 to height and continue to next block in loop + syncHeight += 1; + currentChainTip = await getNodeHeightOrUpdate(syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, + SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight))); + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + + break; + } + } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ab99a875c..58aafa527 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -10,31 +10,33 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase - with _$ElectrumWalletAddresses; +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { - ElectrumWalletAddressesBase(WalletInfo walletInfo, - {required this.mainHd, - required this.sideHd, - required this.electrumClient, - required this.networkType, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : addresses = ObservableList.of( - (initialAddresses ?? []).toSet()), - receiveAddresses = ObservableList.of( - (initialAddresses ?? []) + ElectrumWalletAddressesBase( + WalletInfo walletInfo, { + required this.mainHd, + required this.sideHd, + required this.electrumClient, + required this.networkType, + List? initialAddresses, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0, + bitcoin.SilentPaymentReceiver? silentAddress, + }) : addresses = ObservableList.of((initialAddresses ?? []).toSet()), + silentAddress = silentAddress, + receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of( - (initialAddresses ?? []) + .toSet()), + changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), + .toSet()), + silentAddresses = ObservableList.of((initialAddresses ?? []) + .where((addressRecord) => addressRecord.silentAddressLabel != null) + .toSet()), currentReceiveAddressIndex = initialRegularAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex, - super(walletInfo); + super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -45,14 +47,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList addresses; final ObservableList receiveAddresses; final ObservableList changeAddresses; + final ObservableList silentAddresses; final ElectrumClient electrumClient; final bitcoin.NetworkType networkType; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; - @override + // TODO: labels -> disable edit on receive page + final bitcoin.SilentPaymentReceiver? silentAddress; + + @observable + // ignore: prefer_final_fields + dynamic _addressPageType = bitcoin.AddressType.p2wpkh; @computed - String get address { + dynamic get addressPageType => _addressPageType; + + @observable + String? activeAddress; + + @computed + String get receiveAddress { if (receiveAddresses.isEmpty) { final address = generateNewAddress().address; return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; @@ -63,28 +77,54 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @override - set address(String addr) => null; + @computed + String get address { + if (addressPageType == bitcoin.AddressType.p2sp) { + return silentAddress!.toString(); + } + + if (activeAddress != null) { + return activeAddress!; + } + + if (receiveAddresses.isEmpty) { + return generateNewAddress().address; + } + + try { + receiveAddresses.forEach( + (element) => print('element: ${element.address} ${element.type} $addressPageType')); + return receiveAddresses + .firstWhere((address) => addressPageType == bitcoin.AddressType.p2wpkh + ? address.type == null || address.type == addressPageType + : address.type == addressPageType) + .address; + } catch (_) {} + + return receiveAddresses.first.address; + } + + @override + set address(String addr) => activeAddress = addr; int currentReceiveAddressIndex; int currentChangeAddressIndex; @computed - int get totalCountOfReceiveAddresses => - addresses.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) { + if (!addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); @computed - int get totalCountOfChangeAddresses => - addresses.fold(0, (acc, addressRecord) { - if (addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) { + if (addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); Future discoverAddresses() async { await _discoverAddresses(mainHd, false); @@ -114,12 +154,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (changeAddresses.isEmpty) { final newAddresses = await _createNewAddresses(gap, - hd: sideHd, - startIndex: totalCountOfChangeAddresses > 0 - ? totalCountOfChangeAddresses - 1 - : 0, - isHidden: true); - _addAddresses(newAddresses); + hd: sideHd, + startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, + isHidden: true); + addAddresses(newAddresses); } if (currentChangeAddressIndex >= changeAddresses.length) { @@ -132,19 +170,44 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } + @action BitcoinAddressRecord generateNewAddress( - {bitcoin.HDWallet? hd, bool isHidden = false}) { - currentReceiveAddressIndex += 1; + {bitcoin.HDWallet? hd, bool isHidden = false, String? label}) { + if (label != null && silentAddress != null) { + final address = BitcoinAddressRecord( + bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress( + silentAddress!.scanPubkey, + silentAddress!.spendPubkey, + '0000000000000000000000000000000000000000000000000000000000000002'.fromHex, + hrp: silentAddress!.hrp, + version: silentAddress!.version) + .toString(), + index: currentReceiveAddressIndex, + isHidden: isHidden, + silentAddressLabel: label); + silentAddresses.add(address); + + return address; + } + // FIX-ME: Check logic for whichi HD should be used here ??? final address = BitcoinAddressRecord( - getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd), + getAddress( + index: currentReceiveAddressIndex, + hd: hd ?? sideHd, + addressType: addressPageType as bitcoin.AddressType, + ), index: currentReceiveAddressIndex, isHidden: isHidden); addresses.add(address); return address; + + currentReceiveAddressIndex += 1; } - String getAddress({required int index, required bitcoin.HDWallet hd}) => ''; + String getAddress( + {required int index, required bitcoin.HDWallet hd, bitcoin.AddressType? addressType}) => + ''; @override Future updateAddressesInBox() async { @@ -160,38 +223,37 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAdresses = addresses - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + final newAdresses = + addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); receiveAddresses.addAll(newAdresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAdresses = addresses - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); + final newAdresses = + addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); changeAddresses.addAll(newAdresses); } - Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { + @action + Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden, + {bitcoin.AddressType? addressType}) async { var hasAddrUse = true; List addrs; - if (addresses.isNotEmpty) { - addrs = addresses - .where((addr) => addr.isHidden == isHidden) - .toList(); + if (addresses.where((addr) => addr.type == addressPageType).isNotEmpty) { + addrs = addresses.where((addr) => addr.isHidden == isHidden).toList(); } else { addrs = await _createNewAddresses( - isHidden - ? defaultChangeAddressesCount - : defaultReceiveAddressesCount, + isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount, startIndex: 0, hd: hd, - isHidden: isHidden); + isHidden: isHidden, + addressType: addressType); } - while(hasAddrUse) { + while (hasAddrUse) { final addr = addrs.last.address; hasAddrUse = await _hasAddressUsed(addr); @@ -201,16 +263,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final start = addrs.length; final count = start + gap; - final batch = await _createNewAddresses( - count, - startIndex: start, - hd: hd, - isHidden: isHidden); + final batch = await _createNewAddresses(count, + startIndex: start, hd: hd, isHidden: isHidden, addressType: addressType); addrs.addAll(batch); } - if (addresses.length < addrs.length) { - _addAddresses(addrs); + if (addresses.length < addrs.length || addressPageType != null) { + addAddresses(addrs); } } @@ -229,45 +288,41 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfReceiveAddresses, - hd: mainHd, - isHidden: false); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false); addresses.addAll(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfHiddenAddresses, - hd: sideHd, - isHidden: true); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true); addresses.addAll(newAddresses); } } Future> _createNewAddresses(int count, - {required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false}) async { + {required bitcoin.HDWallet hd, + int startIndex = 0, + bool isHidden = false, + bitcoin.AddressType? addressType}) async { final list = []; for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord( - getAddress(index: i, hd: hd), - index: i, - isHidden: isHidden); + final address = BitcoinAddressRecord(getAddress(index: i, hd: hd, addressType: addressType), + index: i, isHidden: isHidden, type: addressType); list.add(address); } return list; } - void _addAddresses(Iterable addresses) { + @action + void addAddresses(Iterable addresses) { final addressesSet = this.addresses.toSet(); addressesSet.addAll(addresses); this.addresses.removeRange(0, this.addresses.length); - this.addresses.addAll(addressesSet); + this.addresses.addAll(addresses); } Future _hasAddressUsed(String address) async { @@ -275,4 +330,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final transactionHistory = await electrumClient.getHistory(sh); return transactionHistory.isNotEmpty; } -} \ No newline at end of file + + @override + @action + Future setAddressType(dynamic type) async { + _addressPageType = type as bitcoin.AddressType; + + if (addressPageType != bitcoin.AddressType.p2sp) { + await _discoverAddresses(mainHd, false, addressType: addressPageType as bitcoin.AddressType); + updateReceiveAddresses(); + } + } +} diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index def991ebe..e05e09674 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -4,17 +4,20 @@ import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/file.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -class ElectrumWallletSnapshot { - ElectrumWallletSnapshot({ +class ElectrumWalletSnapshot { + ElectrumWalletSnapshot({ required this.name, required this.type, required this.password, required this.mnemonic, required this.addresses, required this.balance, + required this.networkType, required this.regularAddressIndex, - required this.changeAddressIndex}); + required this.changeAddressIndex, + }); final String name; final String password; @@ -23,10 +26,11 @@ class ElectrumWallletSnapshot { String mnemonic; List addresses; ElectrumBalance balance; + bitcoin.NetworkType networkType; int regularAddressIndex; int changeAddressIndex; - static Future load(String name, WalletType type, String password) async { + static Future load(String name, WalletType type, String password) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -38,6 +42,7 @@ class ElectrumWallletSnapshot { .toList(); final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + final networkType = data['network_type'] == 'testnet' ? bitcoin.testnet : bitcoin.bitcoin; var regularAddressIndex = 0; var changeAddressIndex = 0; @@ -46,14 +51,15 @@ class ElectrumWallletSnapshot { changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0'); } catch (_) {} - return ElectrumWallletSnapshot( - name: name, - type: type, - password: password, - mnemonic: mnemonic, - addresses: addresses, - balance: balance, - regularAddressIndex: regularAddressIndex, - changeAddressIndex: changeAddressIndex); + return ElectrumWalletSnapshot( + name: name, + type: type, + password: password, + mnemonic: mnemonic, + addresses: addresses, + balance: balance, + networkType: networkType, + regularAddressIndex: regularAddressIndex, + changeAddressIndex: changeAddressIndex); } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/litecoin_network.dart b/cw_bitcoin/lib/litecoin_network.dart index d7ad2f837..adaf5f7be 100644 --- a/cw_bitcoin/lib/litecoin_network.dart +++ b/cw_bitcoin/lib/litecoin_network.dart @@ -1,9 +1,15 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart'; final litecoinNetwork = NetworkType( - messagePrefix: '\x19Litecoin Signed Message:\n', - bech32: 'ltc', - bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), - pubKeyHash: 0x30, - scriptHash: 0x32, - wif: 0xb0); + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0, + p2pkhPrefix: bitcoin.p2pkhPrefix, + network: bitcoin.network, + p2shPrefix: bitcoin.p2shPrefix, + extendPublic: bitcoin.extendPublic, + extendPrivate: bitcoin.extendPrivate, +); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6bf1c5735..4e64a0027 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -81,7 +81,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load (name, walletInfo.type, password); + final snp = await ElectrumWalletSnapshot.load (name, walletInfo.type, password); return LitecoinWallet( mnemonic: snp.mnemonic, password: password, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index a317fa9f2..447006484 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,31 +9,29 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { - LitecoinWalletAddressesBase( - WalletInfo walletInfo, +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { + LitecoinWalletAddressesBase(WalletInfo walletInfo, {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + required bitcoin.HDWallet sideHd, + required bitcoin.NetworkType networkType, + required ElectrumClient electrumClient, + List? initialAddresses, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) + : super(walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: mainHd, + sideHd: sideHd, + electrumClient: electrumClient, + networkType: networkType); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => + String getAddress( + {required int index, required bitcoin.HDWallet hd, bitcoin.AddressType? addressType}) => generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); -} \ No newline at end of file +} + diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index b13ac7a7f..9968523f5 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -25,7 +25,7 @@ class LitecoinWalletService extends WalletService< WalletType getType() => WalletType.litecoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await LitecoinWalletBase.create( mnemonic: await generateMnemonic(), password: credentials.password!, diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index e2dc10bfb..d539b2cdf 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -9,9 +9,7 @@ import 'package:cw_core/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction(this._tx, this.type, - {required this.electrumClient, - required this.amount, - required this.fee}) + {required this.electrumClient, required this.amount, required this.fee}) : _listeners = []; final WalletType type; @@ -37,7 +35,7 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future commit() async { final result = - await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); + await electrumClient.broadcastTransaction(transactionRaw: _tx.txHex ?? _tx.toHex()); if (result.isEmpty) { throw BitcoinCommitTransactionException(); @@ -46,8 +44,7 @@ class PendingBitcoinTransaction with PendingTransaction { _listeners?.forEach((listener) => listener(transactionInfo())); } - void addListener( - void Function(ElectrumTransactionInfo transaction) listener) => + void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart index 76a1bfcf0..76bbb7ad8 100644 --- a/cw_bitcoin/lib/script_hash.dart +++ b/cw_bitcoin/lib/script_hash.dart @@ -1,9 +1,29 @@ +import 'dart:typed_data'; + import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:crypto/crypto.dart'; String scriptHash(String address, {required bitcoin.NetworkType networkType}) { - final outputScript = - bitcoin.Address.addressToOutputScript(address, networkType); + try { + final outputScript = bitcoin.Address.addressToOutputScript(address, networkType); + final parts = sha256.convert(outputScript).toString().split(''); + var res = ''; + + for (var i = parts.length - 1; i >= 0; i--) { + final char = parts[i]; + i--; + final nextChar = parts[i]; + res += nextChar; + res += char; + } + + return res; + } catch (e) {} + + return ""; +} + +String scriptHashFromScript(Uint8List outputScript, {required bitcoin.NetworkType networkType}) { final parts = sha256.convert(outputScript).toString().split(''); var res = ''; diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 0d5a413b3..8f7db8192 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -4,15 +4,11 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; -bitcoin.PaymentData generatePaymentData( - {required bitcoin.HDWallet hd, required int index}) => - PaymentData( - pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); +bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, required int index}) => + PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); bitcoin.ECPair generateKeyPair( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType network}) => + {required bitcoin.HDWallet hd, required int index, required bitcoin.NetworkType network}) => bitcoin.ECPair.fromWIF(hd.derive(index).wif!, network: network); String generateP2WPKHAddress( @@ -28,28 +24,19 @@ String generateP2WPKHAddress( .data .address!; -String generateP2WPKHAddressByPath( - {required bitcoin.HDWallet hd, - required String path, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2WPKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey!))), - network: networkType) - .data - .address!; - String generateP2PKHAddress( {required bitcoin.HDWallet hd, required int index, required bitcoin.NetworkType networkType}) => bitcoin .P2PKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), + data: PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), network: networkType) .data .address!; + +String generateP2TRAddress( + {required bitcoin.HDWallet hd, + required int index, + required bitcoin.NetworkType networkType}) => + bitcoin.P2trAddress(program: hd.derive(index).pubKey!).toAddress(networkType); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index eb75227b6..cf3f40042 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" async: dependency: transitive description: @@ -45,9 +45,9 @@ packages: dependency: transitive description: path: "." - ref: "cake-0.2.2" - resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" - url: "https://github.com/cake-tech/bech32.git" + ref: bech32m + resolved-ref: "55d966503310fc46bf349e2f56fe1bb513ed429b" + url: "https://github.com/saltrafael/bech32.git" source: git version: "0.2.2" bip32: @@ -75,12 +75,20 @@ packages: url: "https://github.com/cake-tech/bitbox-flutter.git" source: git version: "1.0.1" + bitcoin_base: + dependency: transitive + description: + name: bitcoin_base + sha256: f744eca882f501108639946e1172ab0b2e5553169dffc973cd0bfa78f25986d4 + url: "https://pub.dev" + source: hosted + version: "0.5.0" bitcoin_flutter: dependency: "direct main" description: path: "." - ref: cake-update-v3 - resolved-ref: df9204144011ed9419eff7d9ef3143102a40252d + ref: silent-payments + resolved-ref: "58d78564cf889af7bfea42eee48ebef467619b97" url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.0.2" @@ -104,10 +112,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -120,10 +128,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: @@ -136,18 +144,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.7" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -160,10 +168,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.8.0" characters: dependency: transitive description: @@ -176,10 +184,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -192,10 +200,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.8.0" collection: dependency: transitive description: @@ -216,18 +224,18 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cryptography: dependency: "direct main" description: name: cryptography - sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8 + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.5.0" cw_core: dependency: "direct main" description: @@ -243,14 +251,23 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + elliptic: + dependency: transitive + description: + path: "." + ref: silent-payments + resolved-ref: "09343cf1bc03e74715413972dbec62a2505f5011" + url: "https://github.com/cake-tech/dart-elliptic" + source: git + version: "0.3.10" encrypt: dependency: "direct main" description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -263,10 +280,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -292,10 +309,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: d1d379561fe84675cc099bc42e99456f73a77fff475d0e2e9bda271751a11a91 url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -313,18 +330,18 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hex: dependency: transitive description: @@ -401,18 +418,18 @@ packages: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -449,18 +466,26 @@ packages: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "42ae7277ec5c36fa5ce02aa14551065babce3c38a35947330144ff47bc775c75" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.2.1" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -481,74 +506,74 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.7" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" pool: dependency: transitive description: @@ -557,30 +582,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" rxdart: dependency: "direct main" description: @@ -593,18 +618,18 @@ packages: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -694,10 +719,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unorm_dart: dependency: "direct main" description: @@ -718,42 +743,42 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.0.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.3" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.6 <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 693d5af7a..4530d7ee1 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v3 + ref: silent-payments bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 5f2a33ab6..ee24a196e 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -86,7 +86,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password); return BitcoinCashWallet( mnemonic: snp.mnemonic, password: password, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 1709c4d8f..5487e5598 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -29,6 +29,12 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi networkType: networkType); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => + String getAddress( + {required int index, required bitcoin.HDWallet hd, bitcoin.AddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, networkType: networkType); + + @override + Future setAddressType(dynamic type) { + throw UnimplementedError(); + } } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index 8cc469a3a..b01dca11c 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -31,7 +31,7 @@ class BitcoinCashWalletService extends WalletService create( - credentials) async { + credentials, {bool? isTestnet}) async { final strength = (credentials.seedPhraseLength == 12) ? 128 : (credentials.seedPhraseLength == 24) diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 30ed49e80..8f73b194e 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v3 + ref: silent-payments bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 4983967d0..afddc7c7a 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus { @override String toString() => '$blocksLeft'; + + factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) { + final track = chainTip - initialSyncHeight; + final diff = track - (chainTip - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = chainTip - syncHeight; + + // sum 1 because if at the chain tip, will say "0 blocks left" + return SyncingSyncStatus(left + 1, ptc); + } } class SyncedSyncStatus extends SyncStatus { @@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus { class LostConnectionSyncStatus extends SyncStatus { @override double progress() => 1.0; -} \ No newline at end of file + @override + String toString() => 'Reconnecting'; +} diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index b52daf43c..01b26cdcc 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -16,5 +16,6 @@ class Unspent { bool isFrozen; String note; - bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); + bool get isP2wpkh => + address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc'); } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 632eb1332..70c2bf48f 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -3,8 +3,8 @@ import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) - : addressesMap = {}, - addressInfos = {}; + : addressesMap = {}, + addressInfos = {}; final WalletInfo walletInfo; @@ -38,4 +38,7 @@ abstract class WalletAddresses { } bool containsAddress(String address) => addressesMap.containsKey(address); + + dynamic addressPageType; + Future setAddressType(dynamic type); } diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index f6d0ca192..b1c182e31 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -8,7 +8,7 @@ abstract class WalletService { WalletType getType(); - Future create(N credentials); + Future create(N credentials, {bool? isTestnet}); Future restoreFromSeed(RFS credentials); diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_ethereum/lib/ethereum_wallet_addresses.dart index 4a3492e6f..8ccab7bcd 100644 --- a/cw_ethereum/lib/ethereum_wallet_addresses.dart +++ b/cw_ethereum/lib/ethereum_wallet_addresses.dart @@ -30,4 +30,9 @@ abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { print(e.toString()); } } + + @override + Future setAddressType(dynamic type) { + throw UnimplementedError(); + } } diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 8810d6014..6fc3d4a28 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -19,7 +19,7 @@ class EthereumWalletService extends WalletService walletInfoSource; @override - Future create(EthereumNewWalletCredentials credentials) async { + Future create(EthereumNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = (credentials.seedPhraseLength == 12) ? 128 diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index eeeb763cf..468eedc68 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -88,4 +88,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + Future setAddressType(dynamic type) { + throw UnimplementedError(); + } } diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index 0bc20d2a0..eded06778 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -68,7 +68,7 @@ class HavenWalletService extends WalletService< WalletType getType() => WalletType.haven; @override - Future create(HavenNewWalletCredentials credentials) async { + Future create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.createWallet( diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index f74e7dd5b..6526920b4 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -116,4 +116,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + Future setAddressType(dynamic type) { + throw UnimplementedError(); + } } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 8cb3aa948..1b96e0638 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -65,7 +65,7 @@ class MoneroWalletService extends WalletService< WalletType getType() => WalletType.monero; @override - Future create(MoneroNewWalletCredentials credentials) async { + Future create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.createWallet( diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index cc532d2c7..1e95d4e5d 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -47,4 +47,9 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { print(e.toString()); } } + + @override + Future setAddressType(dynamic type) { + throw UnimplementedError(); + } } diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 2f183d1cc..52dea50ae 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -26,7 +26,7 @@ class NanoWalletService extends WalletService WalletType.nano; @override - Future create(NanoNewWalletCredentials credentials) async { + Future create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { // nano standard: DerivationType derivationType = DerivationType.nano; String seedKey = NanoSeeds.generateSeed(); diff --git a/howto-build-android.md b/howto-build-android.md index a2a4e4d9f..c3fe415ee 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -142,27 +142,9 @@ Then we need to generate localization files. `$ flutter packages pub run tool/generate_localization.dart` -Lastly, we will generate mobx models for the project. - -Generate mobx models for `cw_core`: - -`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_monero`: - -`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_bitcoin`: - -`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_haven`: - -`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - Finally build mobx models for the app: -`$ flutter packages pub run build_runner build --delete-conflicting-outputs` +`$ ./model_generator.sh` ### 9. Build! diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index dd713fd15..c2d894512 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -63,9 +63,9 @@ class CWBitcoin extends Bitcoin { } @override - Future generateNewAddress(Object wallet) async { + Future generateNewAddress(Object wallet, {String? label}) async { final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.walletAddresses.generateNewAddress(); + await bitcoinWallet.walletAddresses.generateNewAddress(label: label); } @override @@ -105,6 +105,21 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.walletAddresses.address; } + String getReceiveAddress(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.receiveAddress; + } + + btc.SilentPaymentAddress? getSilentAddress(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.silentAddress; + } + + List getSilentAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.silentAddresses; + } + @override String formatterBitcoinAmountToString({required int amount}) => bitcoinAmountToString(amount: amount); @@ -155,4 +170,4 @@ class CWBitcoin extends Bitcoin { @override TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; -} \ No newline at end of file +} diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index fcb881943..290177c03 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -8,9 +8,8 @@ class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc - ? bitcoin.Address.validateAddress - : null, + useAdditionalValidation: + type == CryptoCurrency.btc ? bitcoin.Address.validateAddress : null, pattern: getPattern(type), length: getLength(type)); @@ -25,7 +24,9 @@ class AddressValidator extends TextValidator { return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - return '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$'; + final silentpayments = '^tsp1[0-9a-zA-Z]{113}\$'; + return '${bitcoin.P2pkhAddress.REGEX.pattern}|${bitcoin.P2shAddress.REGEX.pattern}|${bitcoin.P2wpkhAddress.REGEX.pattern}|${bitcoin.P2trAddress.REGEX.pattern}|$silentpayments'; + case CryptoCurrency.nano: case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: @@ -88,7 +89,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dai: case CryptoCurrency.dash: case CryptoCurrency.eos: - return '[0-9a-zA-Z]'; + return '[0-9a-zA-Z]'; case CryptoCurrency.bch: return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q[0-9a-zA-Z]{42}\$|^bitcoincash:q[0-9a-zA-Z]{41}\$|^bitcoincash:q[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: @@ -284,4 +285,5 @@ class AddressValidator extends TextValidator { return null; } } -} \ No newline at end of file +} + diff --git a/lib/core/secure_storage.dart b/lib/core/secure_storage.dart new file mode 100644 index 000000000..1e0e99091 --- /dev/null +++ b/lib/core/secure_storage.dart @@ -0,0 +1,23 @@ +final SecureStorage secureStorageShared = FakeSecureStorage(); + +abstract class SecureStorage { + Future read({required String key}); + Future write({required String key, required String? value}); + Future delete({required String key}); + // Legacy + Future readNoIOptions({required String key}); +} + +class FakeSecureStorage extends SecureStorage { + @override + Future read({required String key}) async => null; + + @override + Future write({required String key, required String? value}) async {} + + @override + Future delete({required String key}) async {} + + @override + Future readNoIOptions({required String key}) async => null; +} diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 66094de2b..fbb86fa9f 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart'; String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is SyncingSyncStatus) { - return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + return syncStatus.blocksLeft == 1 + ? S.current.Block_remaining('${syncStatus.blocksLeft}') + : S.current.Blocks_remaining('${syncStatus.blocksLeft}'); } if (syncStatus is SyncedSyncStatus) { @@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) { } return ''; -} \ No newline at end of file +} diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 8548f079f..bfabdce71 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -55,7 +55,7 @@ class WalletCreationService { } } - Future create(WalletCredentials credentials) async { + Future create(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; @@ -63,7 +63,7 @@ class WalletCreationService { credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; } await keyService.saveWalletPassword(password: password, walletName: credentials.name); - final wallet = await _service!.create(credentials); + final wallet = await _service!.create(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { await sharedPreferences.setBool( diff --git a/lib/di.dart b/lib/di.dart index 94f5dbb63..48ec1c429 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -222,6 +222,7 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; import 'core/totp_request_details.dart'; import 'src/screens/settings/desktop_settings/desktop_settings_page.dart'; @@ -341,10 +342,9 @@ Future setup({ getIt.get(), (WalletType type) => getIt.get(param1: type))); - getIt.registerFactoryParam((type, _) => - WalletNewVM(getIt.get(), - getIt.get(param1: type), _walletInfoSource, - type: type)); + getIt.registerFactoryParam((type, _) => WalletNewVM( + getIt.get(), getIt.get(param1: type), _walletInfoSource, + type: type)); getIt.registerFactoryParam((WalletType type, _) { return WalletRestorationFromQRVM(getIt.get(), @@ -524,7 +524,7 @@ Future setup({ getIt.registerFactory(() => DesktopSettingsPage()); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); getIt.registerFactoryParam, void>((args, _) { @@ -642,7 +642,10 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet!; - if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { + if ((wallet.type == WalletType.bitcoin && + wallet.walletAddresses.addressPageType == btc.AddressType.p2sp) || + wallet.type == WalletType.monero || + wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } throw Exception( @@ -900,8 +903,8 @@ Future setup({ (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( - (WalletType type, AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel) - => PreSeedPage(type, advancedPrivacySettingsViewModel)); + (WalletType type, AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel) => + PreSeedPage(type, advancedPrivacySettingsViewModel)); getIt.registerFactoryParam((trade, _) => TradeDetailsViewModel( @@ -995,11 +998,10 @@ Future setup({ getIt.registerFactory(() => YatService()); - getIt.registerFactory(() => - AddressResolver( - yatService: getIt.get(), - wallet: getIt.get().wallet!, - settingsStore: getIt.get())); + getIt.registerFactory(() => AddressResolver( + yatService: getIt.get(), + wallet: getIt.get().wallet!, + settingsStore: getIt.get())); getIt.registerFactoryParam( (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); @@ -1175,6 +1177,5 @@ Future setup({ getIt.registerFactory( () => WalletConnectConnectionsView(web3walletService: getIt.get())); - _isSetupFinished = true; } diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index c57613fa5..0a924c504 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -28,6 +28,7 @@ import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; class AddressPage extends BasePage { AddressPage({ @@ -70,7 +71,7 @@ class AddressPage extends BasePage { size: 16, ); final _closeButton = - currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; @@ -161,10 +162,10 @@ class AddressPage extends BasePage { Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return GestureDetector( - onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled + onTap: () async => !addressListViewModel.hasSilentAddresses && + dashboardViewModel.isAutoGenerateSubaddressesEnabled ? await showPopUp( - context: context, - builder: (_) => getIt.get()) + context: context, builder: (_) => getIt.get()) : Navigator.of(context).pushNamed(Routes.receive), child: Container( height: 50, @@ -184,26 +185,30 @@ class AddressPage extends BasePage { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( - builder: (_) { - String label = addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses; + builder: (_) { + String label = addressListViewModel.hasSilentAddresses + ? S.of(context).labeled_silent_addresses + : addressListViewModel.hasAccounts + ? S.of(context).accounts_subaddresses + : S.of(context).addresses; - if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { - label = addressListViewModel.hasAccounts - ? S.of(context).accounts - : S.of(context).account; - } - return Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .textColor), - ); - },), + if (!addressListViewModel.hasSilentAddresses && + dashboardViewModel.isAutoGenerateSubaddressesEnabled) { + label = addressListViewModel.hasAccounts + ? S.of(context).accounts + : S.of(context).account; + } + return Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .textColor), + ); + }, + ), Icon( Icons.arrow_forward_ios, size: 14, @@ -213,7 +218,8 @@ class AddressPage extends BasePage { ), ), ); - } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || addressListViewModel.showElectrumAddressDisclaimer) { + } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || + addressListViewModel.showElectrumAddressDisclaimer) { return Text(S.of(context).electrum_address_disclaimer, textAlign: TextAlign.center, style: TextStyle( @@ -233,7 +239,7 @@ class AddressPage extends BasePage { return; } - reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { + reaction((_) => receiveOptionViewModel.selectedReceiveOption, (dynamic option) async { switch (option) { case ReceivePageOption.anonPayInvoice: Navigator.pushNamed( @@ -265,6 +271,12 @@ class AddressPage extends BasePage { ); } break; + case bitcoin.AddressType.p2pkh: + case bitcoin.AddressType.p2wpkh: + case bitcoin.AddressType.p2tr: + case bitcoin.AddressType.p2sp: + await addressListViewModel.setAddressType(option); + break; default: } }); diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 5577fcd88..7213504ec 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/routes.dart'; @@ -125,15 +126,20 @@ class _WalletNameFormState extends State { hintStyle: TextStyle( fontSize: 18.0, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.hintTextColor), + color: + Theme.of(context).extension()!.hintTextColor), hintText: S.of(context).wallet_name, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).extension()!.underlineColor, + color: Theme.of(context) + .extension()! + .underlineColor, width: 1.0)), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).extension()!.underlineColor, + color: Theme.of(context) + .extension()! + .underlineColor, width: 1.0), ), suffixIcon: Semantics( @@ -160,7 +166,9 @@ class _WalletNameFormState extends State { height: 34, child: Image.asset( 'assets/images/refresh_icon.png', - color: Theme.of(context).extension()!.textFieldButtonIconColor, + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor, ), ), ), @@ -168,6 +176,12 @@ class _WalletNameFormState extends State { ), validator: WalletNameValidator(), ), + Observer(builder: (context) { + return SettingsSwitcherCell( + title: S.current.use_testnet, + value: widget._walletNewVM.useTestnet, + onValueChange: (_, __) => widget._walletNewVM.toggleUseTestnet()); + }), ], ), ), diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index fc835c72d..41816d89f 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -190,7 +190,7 @@ class AnonPayInvoicePage extends BasePage { return; } - reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { + reaction((_) => receiveOptionViewModel.selectedReceiveOption, (dynamic option) { switch (option) { case ReceivePageOption.mainnet: Navigator.popAndPushNamed(context, Routes.addressPage); diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 87b668e34..04344a769 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -24,6 +24,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; class ReceivePage extends BasePage { ReceivePage({required this.addressListViewModel}) @@ -67,8 +68,7 @@ class ReceivePage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override Widget trailing(BuildContext context) { @@ -99,7 +99,10 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { - return (addressListViewModel.type == WalletType.monero || + return ((addressListViewModel.type == WalletType.bitcoin && + addressListViewModel.wallet.walletAddresses.addressPageType == + bitcoin.AddressType.p2sp) || + addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven || addressListViewModel.type == WalletType.nano || addressListViewModel.type == WalletType.banano) @@ -156,7 +159,8 @@ class ReceivePage extends BasePage { icon: Icon( Icons.arrow_forward_ios, size: 14, - color: Theme.of(context).extension()!.iconsColor, + color: + Theme.of(context).extension()!.iconsColor, )); } @@ -164,11 +168,12 @@ class ReceivePage extends BasePage { cell = HeaderTile( onTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, + title: S.of(context).labeled_silent_addresses, icon: Icon( Icons.add, size: 20, - color: Theme.of(context).extension()!.iconsColor, + color: + Theme.of(context).extension()!.iconsColor, )); } @@ -177,11 +182,19 @@ class ReceivePage extends BasePage { final isCurrent = item.address == addressListViewModel.address.address; final backgroundColor = isCurrent - ? Theme.of(context).extension()!.currentTileBackgroundColor - : Theme.of(context).extension()!.tilesBackgroundColor; + ? Theme.of(context) + .extension()! + .currentTileBackgroundColor + : Theme.of(context) + .extension()! + .tilesBackgroundColor; final textColor = isCurrent - ? Theme.of(context).extension()!.currentTileTextColor - : Theme.of(context).extension()!.tilesTextColor; + ? Theme.of(context) + .extension()! + .currentTileTextColor + : Theme.of(context) + .extension()! + .tilesTextColor; return AddressCell.fromItem(item, isCurrent: isCurrent, @@ -202,6 +215,15 @@ class ReceivePage extends BasePage { child: cell, ); })), + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Text(S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: + Theme.of(context).extension()!.labelTextColor)), + ), ], ), )) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 65069e903..4663e3914 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -164,7 +165,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _presentQRScanner(BuildContext context) async { bool isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; final code = await presentQRScanner(); if (code.isEmpty) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index fbb2fc76f..bd02e10ed 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -31,6 +31,7 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; part 'dashboard_view_model.g.dart'; @@ -260,7 +261,11 @@ abstract class DashboardViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; - bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; + bool get hasRescan => + (wallet.type == WalletType.bitcoin && + wallet.walletAddresses.addressPageType == bitcoin.AddressType.p2sp) || + wallet.type == WalletType.monero || + wallet.type == WalletType.haven; BalanceViewModel balanceViewModel; diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 0eaa2a5f0..a3d6a3d5f 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; part 'receive_option_view_model.g.dart'; @@ -9,26 +10,38 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) - : selectedReceiveOption = initialPageOption ?? ReceivePageOption.mainnet, + : selectedReceiveOption = initialPageOption ?? + (_wallet.type == WalletType.bitcoin + ? _wallet.walletAddresses.addressPageType + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; - _options = - walletType == WalletType.haven ? [ReceivePageOption.mainnet] : ReceivePageOption.values; + _options = walletType == WalletType.haven + ? [ReceivePageOption.mainnet] + : walletType == WalletType.bitcoin + ? [ + bitcoin.AddressType.p2pkh, + bitcoin.AddressType.p2wpkh, + bitcoin.AddressType.p2tr, + bitcoin.AddressType.p2sp, + ...ReceivePageOption.values.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOption.values; } final WalletBase _wallet; - final ReceivePageOption? initialPageOption; + final dynamic initialPageOption; - List _options; + List _options; @observable - ReceivePageOption selectedReceiveOption; + dynamic selectedReceiveOption; - List get options => _options; + List get options => _options; @action - void selectReceiveOption(ReceivePageOption option) { + void selectReceiveOption(dynamic option) { selectedReceiveOption = option; } } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 0fb9a83c6..8c5a2d866 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -12,12 +12,10 @@ import 'package:permission_handler/permission_handler.dart'; part 'node_create_or_edit_view_model.g.dart'; -class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase - with _$NodeCreateOrEditViewModel; +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel; abstract class NodeCreateOrEditViewModelBase with Store { - NodeCreateOrEditViewModelBase( - this._nodeSource, this._walletType, this._settingsStore) + NodeCreateOrEditViewModelBase(this._nodeSource, this._walletType, this._settingsStore) : state = InitialExecutionState(), connectionState = InitialExecutionState(), useSSL = false, @@ -65,6 +63,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool get hasAuthCredentials => _walletType == WalletType.monero || _walletType == WalletType.haven; + bool get hasTestnetSupport => _walletType == WalletType.bitcoin; + String get uri { var uri = address; @@ -181,7 +181,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { Future scanQRCodeForNewNode(BuildContext context) async { try { bool isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; String code = await presentQRScanner(); @@ -196,7 +196,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { } final userInfo = uri.userInfo.split(':'); - + if (userInfo.length < 2) { throw Exception('Unexpected scan QR code value: Value is invalid'); } diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index c973b7b3f..e263f4a12 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -1,4 +1,5 @@ import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'rescan_view_model.g.dart'; @@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none } abstract class RescanViewModelBase with Store { RescanViewModelBase(this._wallet) - : state = RescanWalletState.none, - isButtonEnabled = false; + : state = RescanWalletState.none, + isButtonEnabled = false; final WalletBase _wallet; @@ -23,8 +24,8 @@ abstract class RescanViewModelBase with Store { @action Future rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; - await _wallet.rescan(height: restoreHeight); - _wallet.transactionHistory.clear(); + _wallet.rescan(height: restoreHeight); + if (_wallet.type != WalletType.bitcoin) _wallet.transactionHistory.clear(); state = RescanWalletState.none; } -} \ No newline at end of file +} diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 9e2aa7187..87c8f6bea 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_base.dart'; @@ -27,8 +28,7 @@ class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { } abstract class WalletAddressEditOrCreateViewModelBase with Store { - WalletAddressEditOrCreateViewModelBase( - {required WalletBase wallet, WalletAddressListItem? item}) + WalletAddressEditOrCreateViewModelBase({required WalletBase wallet, WalletAddressListItem? item}) : isEdit = item != null, state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', @@ -68,27 +68,21 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash) { - await bitcoin!.generateNewAddress(wallet); + await bitcoin!.generateNewAddress(wallet, label: label); await wallet.save(); } if (wallet.type == WalletType.monero) { - await monero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: monero!.getCurrentAccount(wallet).id, - label: label); + await monero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); await wallet.save(); } if (wallet.type == WalletType.haven) { - await haven - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - label: label); + await haven! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: haven!.getCurrentAccount(wallet).id, label: label); await wallet.save(); } } @@ -109,9 +103,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { } if (wallet.type == WalletType.haven) { await haven!.getSubaddressList(wallet).setLabelSubaddress(wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - addressIndex: index, - label: label); + accountIndex: haven!.getCurrentAccount(wallet).id, addressIndex: index, label: label); await wallet.save(); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 4d5eefdb7..d1d63a0eb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -15,6 +15,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; part 'wallet_address_list_view_model.g.dart'; @@ -109,7 +110,7 @@ class EthereumURI extends PaymentURI { class BitcoinCashURI extends PaymentURI { BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + : super(amount: amount, address: address); @override String toString() { var base = address; @@ -120,9 +121,7 @@ class BitcoinCashURI extends PaymentURI { return base; } - } - - +} class NanoURI extends PaymentURI { NanoURI({required String amount, required String address}) @@ -147,8 +146,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, amount = '', super(appStore: appStore) { _init(); @@ -159,7 +156,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.haven; + _hasAccounts = + hasSilentAddresses || wallet.type == WalletType.monero || wallet.type == WalletType.haven; } static const String _cryptoNumberPattern = '0.00000000'; @@ -257,13 +255,19 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (wallet.type == WalletType.bitcoin) { - final primaryAddress = bitcoin!.getAddress(wallet); - final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { - final isPrimary = addr == primaryAddress; + final receiveAddress = bitcoin!.getReceiveAddress(wallet); + addressList.add( + WalletAddressListItem(isPrimary: true, name: 'Primary address', address: receiveAddress)); - return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); + final silentAddress = bitcoin!.getSilentAddress(wallet).toString(); + addressList.add( + WalletAddressListItem(isPrimary: false, name: silentAddress, address: silentAddress)); + + final silentAddresses = bitcoin!.getSilentAddresses(wallet); + silentAddresses.forEach((addr) { + addressList.add(WalletAddressListItem( + isPrimary: false, name: addr.silentAddressLabel, address: addr.address)); }); - addressList.addAll(bitcoinAddresses); } if (wallet.type == WalletType.ethereum) { @@ -276,7 +280,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } @observable - bool hasAccounts; + bool _hasAccounts = false; + + @computed + bool get hasAccounts => _hasAccounts; @computed String get accountLabel { @@ -291,18 +298,31 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return ''; } + @observable + // ignore: prefer_final_fields + bool? _hasSilentAddresses = null; + + @computed + bool get hasSilentAddresses => + _hasSilentAddresses ?? + wallet.type == WalletType.bitcoin && + wallet.walletAddresses.addressPageType == btc.AddressType.p2sp; + @computed bool get hasAddressList => + hasSilentAddresses || wallet.type == WalletType.monero || - wallet.type == WalletType.haven;/* || + wallet.type == + WalletType + .haven; /* || wallet.type == WalletType.nano || - wallet.type == WalletType.banano;*/// TODO: nano accounts are disabled for now + wallet.type == WalletType.banano;*/ // TODO: nano accounts are disabled for now @computed bool get showElectrumAddressDisclaimer => - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + (wallet.type == WalletType.bitcoin && !hasSilentAddresses) || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; List _baseItems; @@ -312,13 +332,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void setAddress(WalletAddressListItem address) => wallet.walletAddresses.address = address.address; + @action + Future setAddressType(dynamic option) async { + await wallet.walletAddresses.setAddressType(option); + _hasSilentAddresses = option == btc.AddressType.p2sp; + } + void _init() { _baseItems = []; if (wallet.type == WalletType.monero || - wallet.type == WalletType.haven /*|| + wallet.type == + WalletType + .haven /*|| wallet.type == WalletType.nano || - wallet.type == WalletType.banano*/) { + wallet.type == WalletType.banano*/ + ) { _baseItems.add(WalletAccountListHeader()); } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index aa1efe4d3..b10be6fb8 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -39,7 +39,7 @@ abstract class WalletCreationVMBase with Store { bool typeExists(WalletType type) => walletCreationService.typeExists(type); - Future create({dynamic options, RestoredWallet? restoreWallet}) async { + Future create({dynamic options, RestoredWallet? restoreWallet, bool? isTestnet}) async { final type = restoreWallet?.type ?? this.type; try { state = IsExecutingState(); diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 9b1f0834d..76665ab71 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -27,6 +27,12 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { : selectedMnemonicLanguage = '', super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false); + @observable + bool _useTestnet = false; + + @computed + bool get useTestnet => _useTestnet; + @observable String selectedMnemonicLanguage; @@ -59,6 +65,9 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { @override Future process(WalletCredentials credentials) async { walletCreationService.changeWalletType(type: type); - return walletCreationService.create(credentials); + return walletCreationService.create(credentials, isTestnet: useTestnet); } + + @action + void toggleUseTestnet() => _useTestnet = !_useTestnet; } diff --git a/model_generator.sh b/model_generator.sh index 50cb3d353..deeaaf6c5 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,8 +1,10 @@ -cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -flutter packages pub run build_runner build --delete-conflicting-outputs \ No newline at end of file +#!/bin/bash + +cd cw_core; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_monero; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_haven; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_ethereum; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_nano; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin_cash; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. +dart run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index f43d65400..648cee7db 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -96,7 +96,7 @@ dependencies: bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v3 + ref: silent-payments fluttertoast: 8.1.4 dev_dependencies: diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a5298c343..01e7d2c5b 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -730,5 +730,8 @@ "seed_phrase_length": " ﺭﻭﺬﺒﻟﺍ ﺓﺭﺎﺒﻌﻟﺍ ﻝﻮﻃ", "unavailable_balance": " ﺮﻓﻮﺘﻣ ﺮﻴﻏ ﺪﻴﺻﺭ", "unavailable_balance_description": ".ﺎﻫﺪﻴﻤﺠﺗ ءﺎﻐﻟﺇ ﺭﺮﻘﺗ ﻰﺘﺣ ﺕﻼﻣﺎﻌﻤﻠﻟ ﻝﻮﺻﻮﻠﻟ ﺔﻠﺑﺎﻗ ﺮﻴﻏ ﺓﺪﻤﺠﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﻞﻈﺗ ﺎﻤﻨﻴﺑ ،ﺎﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻣﺎﻌﻤﻟﺍ ﻝﺎﻤﺘﻛﺍ ﺩﺮﺠﻤﺑ ﺔﺣﺎﺘﻣ ﺔﻠﻔﻘﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﺢﺒﺼﺘﺳ .ﻚﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻤﻌﻟﺍ ﻲﻓ ﻢﻜﺤﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻂﺸﻧ ﻞﻜﺸﺑ ﺎﻫﺪﻴﻤﺠﺘﺑ ﺖﻤﻗ", - "unspent_change": "يتغير" -} + "unspent_change": "يتغير", + "Block_remaining": "${status} كتلة متبقية", + "labeled_silent_addresses": "العناوين الصامتة المسمى", + "use_testnet": "استخدم testnet" +} \ No newline at end of file diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 3b085cd59..26a70bbbe 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -726,5 +726,8 @@ "seed_phrase_length": "Дължина на началната фраза", "unavailable_balance": "Неналично салдо", "unavailable_balance_description": "Неналично салдо: Тази обща сума включва средства, които са заключени в чакащи транзакции и тези, които сте замразили активно в настройките за контрол на монетите. Заключените баланси ще станат достъпни, след като съответните им транзакции бъдат завършени, докато замразените баланси остават недостъпни за транзакции, докато не решите да ги размразите.", - "unspent_change": "Промяна" -} + "unspent_change": "Промяна", + "Block_remaining": "${status} останал блок", + "labeled_silent_addresses": "Етикетирани безшумни адреси", + "use_testnet": "Използвайте TestNet" +} \ No newline at end of file diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f6d024957..5a907bccd 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -726,5 +726,8 @@ "seed_phrase_length": "Délka fráze semene", "unavailable_balance": "Nedostupný zůstatek", "unavailable_balance_description": "Nedostupný zůstatek: Tento součet zahrnuje prostředky, které jsou uzamčeny v nevyřízených transakcích a ty, které jste aktivně zmrazili v nastavení kontroly mincí. Uzamčené zůstatky budou k dispozici po dokončení příslušných transakcí, zatímco zmrazené zůstatky zůstanou pro transakce nepřístupné, dokud se nerozhodnete je uvolnit.", - "unspent_change": "Změna" -} + "unspent_change": "Změna", + "Block_remaining": "${status} Blok zbývající", + "labeled_silent_addresses": "Označené tiché adresy", + "use_testnet": "Použijte testNet" +} \ No newline at end of file diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 6519f0ef6..4c9e2a960 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Länge der Seed-Phrase", "unavailable_balance": "Nicht verfügbares Guthaben", "unavailable_balance_description": "Nicht verfügbares Guthaben: Diese Summe umfasst Gelder, die in ausstehenden Transaktionen gesperrt sind, und solche, die Sie in Ihren Münzkontrolleinstellungen aktiv eingefroren haben. Gesperrte Guthaben werden verfügbar, sobald die entsprechenden Transaktionen abgeschlossen sind, während eingefrorene Guthaben für Transaktionen nicht zugänglich bleiben, bis Sie sich dazu entschließen, sie wieder freizugeben.", - "unspent_change": "Wechselgeld" -} + "unspent_change": "Wechselgeld", + "Block_remaining": "${status} Block verbleibend", + "labeled_silent_addresses": "Bezeichnete stille Adressen", + "use_testnet": "TESTNET verwenden" +} \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 2c1d00eb6..68a5f219f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -735,5 +735,8 @@ "seed_phrase_length": "Seed phrase length", "unavailable_balance": "Unavailable balance", "unavailable_balance_description": "Unavailable Balance: This total includes funds that are locked in pending transactions and those you have actively frozen in your coin control settings. Locked balances will become available once their respective transactions are completed, while frozen balances remain inaccessible for transactions until you decide to unfreeze them.", - "unspent_change": "Change" -} + "unspent_change": "Change", + "Block_remaining": "${status} Block Remaining", + "labeled_silent_addresses": "Labeled Silent Addresses", + "use_testnet": "Use testnet" +} \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3629a6d37..94dbf06b4 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Longitud de la frase inicial", "unavailable_balance": "Saldo no disponible", "unavailable_balance_description": "Saldo no disponible: este total incluye fondos que están bloqueados en transacciones pendientes y aquellos que usted ha congelado activamente en su configuración de control de monedas. Los saldos bloqueados estarán disponibles una vez que se completen sus respectivas transacciones, mientras que los saldos congelados permanecerán inaccesibles para las transacciones hasta que usted decida descongelarlos.", - "unspent_change": "Cambiar" -} + "unspent_change": "Cambiar", + "Block_remaining": "${status} bloque restante", + "labeled_silent_addresses": "Direcciones silenciosas etiquetadas", + "use_testnet": "Use TestNet" +} \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 9f8a269d4..b318de3f0 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -734,5 +734,8 @@ "unavailable_balance_description": "Solde indisponible : ce total comprend les fonds bloqués dans les transactions en attente et ceux que vous avez activement gelés dans vos paramètres de contrôle des pièces. Les soldes bloqués deviendront disponibles une fois leurs transactions respectives terminées, tandis que les soldes gelés resteront inaccessibles aux transactions jusqu'à ce que vous décidiez de les débloquer.", "camera_permission_is_required": "L'autorisation d'accès à la caméra est requise.\nVeuillez l'activer depuis les paramètres de l'application.", "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer", - "unspent_change": "Changement" -} + "unspent_change": "Changement", + "Block_remaining": "${status} bloc restant", + "labeled_silent_addresses": "Adresses silencieuses étiquetées", + "use_testnet": "Utiliser TestNet" +} \ No newline at end of file diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 84c3b4b74..794a2812c 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -712,5 +712,8 @@ "seed_phrase_length": "Tsawon jimlar iri", "unavailable_balance": "Ma'aunin da ba ya samuwa", "unavailable_balance_description": "Ma'auni Babu: Wannan jimlar ya haɗa da kuɗi waɗanda ke kulle a cikin ma'amaloli da ke jiran aiki da waɗanda kuka daskare sosai a cikin saitunan sarrafa kuɗin ku. Ma'auni da aka kulle za su kasance da zarar an kammala ma'amalolinsu, yayin da daskararrun ma'auni ba za su iya samun damar yin ciniki ba har sai kun yanke shawarar cire su.", - "unspent_change": "Canza" -} + "unspent_change": "Canza", + "Block_remaining": "${status} toshe ragowar", + "labeled_silent_addresses": "Mai labarar adireshin shiru", + "use_testnet": "Amfani da gwaji" +} \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index af3b888cb..48af5dc1d 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "बीज वाक्यांश की लंबाई", "unavailable_balance": "अनुपलब्ध शेष", "unavailable_balance_description": "अनुपलब्ध शेष राशि: इस कुल में वे धनराशि शामिल हैं जो लंबित लेनदेन में बंद हैं और जिन्हें आपने अपनी सिक्का नियंत्रण सेटिंग्स में सक्रिय रूप से जमा कर रखा है। लॉक किए गए शेष उनके संबंधित लेन-देन पूरे होने के बाद उपलब्ध हो जाएंगे, जबकि जमे हुए शेष लेन-देन के लिए अप्राप्य रहेंगे जब तक कि आप उन्हें अनफ्रीज करने का निर्णय नहीं लेते।", - "unspent_change": "परिवर्तन" -} + "unspent_change": "परिवर्तन", + "Block_remaining": "${status} शेष ब्लॉक", + "labeled_silent_addresses": "मूक पते लेबल", + "use_testnet": "टेस्टनेट का उपयोग करें" +} \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 3c56b8552..9ec57df71 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -732,5 +732,8 @@ "seed_phrase_length": "Duljina početne fraze", "unavailable_balance": "Nedostupno stanje", "unavailable_balance_description": "Nedostupno stanje: Ovaj ukupni iznos uključuje sredstva koja su zaključana u transakcijama na čekanju i ona koja ste aktivno zamrznuli u postavkama kontrole novčića. Zaključani saldi postat će dostupni kada se dovrše njihove transakcije, dok zamrznuti saldi ostaju nedostupni za transakcije sve dok ih ne odlučite odmrznuti.", - "unspent_change": "Promijeniti" -} + "unspent_change": "Promijeniti", + "Block_remaining": "${status} ostao blok", + "labeled_silent_addresses": "Označene tihe adrese", + "use_testnet": "Koristite TestNet" +} \ No newline at end of file diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b3d22780a..ded93e095 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -722,5 +722,8 @@ "seed_phrase_length": "Panjang frase benih", "unavailable_balance": "Saldo tidak tersedia", "unavailable_balance_description": "Saldo Tidak Tersedia: Total ini termasuk dana yang terkunci dalam transaksi yang tertunda dan dana yang telah Anda bekukan secara aktif di pengaturan kontrol koin Anda. Saldo yang terkunci akan tersedia setelah transaksi masing-masing selesai, sedangkan saldo yang dibekukan tetap tidak dapat diakses untuk transaksi sampai Anda memutuskan untuk mencairkannya.", - "unspent_change": "Mengubah" -} + "unspent_change": "Mengubah", + "Block_remaining": "${status} blok tersisa", + "labeled_silent_addresses": "Label alamat diam", + "use_testnet": "Gunakan TestNet" +} \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8fc157f27..461809a9e 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Lunghezza della frase seed", "unavailable_balance": "Saldo non disponibile", "unavailable_balance_description": "Saldo non disponibile: questo totale include i fondi bloccati nelle transazioni in sospeso e quelli che hai congelato attivamente nelle impostazioni di controllo delle monete. I saldi bloccati diventeranno disponibili una volta completate le rispettive transazioni, mentre i saldi congelati rimarranno inaccessibili per le transazioni finché non deciderai di sbloccarli.", - "unspent_change": "Modifica" -} + "unspent_change": "Modifica", + "Block_remaining": "${status} blocco rimanente", + "labeled_silent_addresses": "Indirizzi silenziosi etichettati", + "use_testnet": "Usa TestNet" +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 06d8efd37..93a827f0d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "シードフレーズの長さ", "unavailable_balance": "利用できない残高", "unavailable_balance_description": "利用不可能な残高: この合計には、保留中のトランザクションにロックされている資金と、コイン管理設定でアクティブに凍結した資金が含まれます。ロックされた残高は、それぞれの取引が完了すると利用可能になりますが、凍結された残高は、凍結を解除するまで取引にアクセスできません。", - "unspent_change": "変化" -} + "unspent_change": "変化", + "Block_remaining": "${status}ブロックの残り", + "labeled_silent_addresses": "サイレントアドレスとラベル付けされています", + "use_testnet": "TestNetを使用します" +} \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6830b1183..6e0fb6bc8 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -732,5 +732,8 @@ "seed_phrase_length": "시드 문구 길이", "unavailable_balance": "사용할 수 없는 잔액", "unavailable_balance_description": "사용할 수 없는 잔액: 이 총계에는 보류 중인 거래에 잠겨 있는 자금과 코인 관리 설정에서 적극적으로 동결된 자금이 포함됩니다. 잠긴 잔액은 해당 거래가 완료되면 사용할 수 있게 되며, 동결된 잔액은 동결을 해제하기 전까지 거래에 액세스할 수 없습니다.", - "unspent_change": "변화" -} + "unspent_change": "변화", + "Block_remaining": "${status} 나머지 블록", + "labeled_silent_addresses": "라벨링 된 무음 주소", + "use_testnet": "TestNet을 사용하십시오" +} \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index ff8f6683b..9e1ac4d7e 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -732,5 +732,8 @@ "seed_phrase_length": "မျိုးစေ့စာပိုဒ်တိုအရှည်", "unavailable_balance": "လက်ကျန်ငွေ မရရှိနိုင်ပါ။", "unavailable_balance_description": "မရရှိနိုင်သော လက်ကျန်ငွေ- ဤစုစုပေါင်းတွင် ဆိုင်းငံ့ထားသော ငွေပေးငွေယူများတွင် သော့ခတ်ထားသော ငွေကြေးများနှင့် သင်၏ coin ထိန်းချုပ်မှုဆက်တင်များတွင် သင် တက်ကြွစွာ အေးခဲထားသော ငွေများ ပါဝင်သည်။ သော့ခတ်ထားသော လက်ကျန်ငွေများကို ၎င်းတို့၏ သက်ဆိုင်ရာ ငွေပေးငွေယူများ ပြီးမြောက်သည်နှင့် တပြိုင်နက် ရရှိနိုင်မည်ဖြစ်ပြီး၊ အေးခဲထားသော လက်ကျန်များကို ၎င်းတို့အား ပြန်ဖြုတ်ရန် သင်ဆုံးဖြတ်သည်အထိ ငွေပေးငွေယူများအတွက် ဆက်လက်၍မရနိုင်ပါ။", - "unspent_change": "ပေြာင်းလဲခြင်း" -} + "unspent_change": "ပေြာင်းလဲခြင်း", + "Block_remaining": "ကျန်ရှိသော ${status}", + "labeled_silent_addresses": "အသံတိတ်အသံတိတ်လိပ်စာများတံဆိပ်ကပ်", + "use_testnet": "testnet ကိုသုံးပါ" +} \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 753e28cd5..4f5481bd1 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Lengte van de zaadzin", "unavailable_balance": "Onbeschikbaar saldo", "unavailable_balance_description": "Niet-beschikbaar saldo: Dit totaal omvat het geld dat is vergrendeld in lopende transacties en het geld dat u actief hebt bevroren in uw muntcontrole-instellingen. Vergrendelde saldi komen beschikbaar zodra de betreffende transacties zijn voltooid, terwijl bevroren saldi ontoegankelijk blijven voor transacties totdat u besluit ze weer vrij te geven.", - "unspent_change": "Wijziging" -} + "unspent_change": "Wijziging", + "Block_remaining": "${status} blok resterend", + "labeled_silent_addresses": "Gelabelde stille adressen", + "use_testnet": "Gebruik testnet" +} \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 1900ca590..39f53e40e 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Długość frazy początkowej", "unavailable_balance": "Niedostępne saldo", "unavailable_balance_description": "Niedostępne saldo: Suma ta obejmuje środki zablokowane w transakcjach oczekujących oraz te, które aktywnie zamroziłeś w ustawieniach kontroli monet. Zablokowane salda staną się dostępne po zakończeniu odpowiednich transakcji, natomiast zamrożone salda pozostaną niedostępne dla transakcji, dopóki nie zdecydujesz się ich odblokować.", - "unspent_change": "Zmiana" -} + "unspent_change": "Zmiana", + "Block_remaining": "${status} Block pozostały", + "labeled_silent_addresses": "Oznaczone ciche adresy", + "use_testnet": "Użyj testne" +} \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 86c7a6b0d..d875ba063 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -733,5 +733,8 @@ "seed_phrase_length": "Comprimento da frase-semente", "unavailable_balance": "Saldo indisponível", "unavailable_balance_description": "Saldo Indisponível: Este total inclui fundos bloqueados em transações pendentes e aqueles que você congelou ativamente nas configurações de controle de moedas. Os saldos bloqueados ficarão disponíveis assim que suas respectivas transações forem concluídas, enquanto os saldos congelados permanecerão inacessíveis para transações até que você decida descongelá-los.", - "unspent_change": "Mudar" -} + "unspent_change": "Mudar", + "Block_remaining": "${status} bloco restante", + "labeled_silent_addresses": "Endereços silenciosos rotulados", + "use_testnet": "Use testNet" +} \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index a37b0908b..ffe22037b 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Длина исходной фразы", "unavailable_balance": "Недоступный баланс", "unavailable_balance_description": "Недоступный баланс: в эту сумму входят средства, заблокированные в ожидающих транзакциях, и средства, которые вы активно заморозили в настройках управления монетами. Заблокированные балансы станут доступны после завершения соответствующих транзакций, а замороженные балансы останутся недоступными для транзакций, пока вы не решите их разморозить.", - "unspent_change": "Изменять" -} + "unspent_change": "Изменять", + "Block_remaining": "${status} оставшееся блок", + "labeled_silent_addresses": "Помеченные тихий адреса", + "use_testnet": "Используйте Testnet" +} \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index fa36c04bb..03daf1a93 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -732,5 +732,8 @@ "seed_phrase_length": "ความยาววลีของเมล็ด", "unavailable_balance": "ยอดคงเหลือไม่พร้อมใช้งาน", "unavailable_balance_description": "ยอดคงเหลือที่ไม่พร้อมใช้งาน: ยอดรวมนี้รวมถึงเงินทุนที่ถูกล็อคในการทำธุรกรรมที่รอดำเนินการและที่คุณได้แช่แข็งไว้ในการตั้งค่าการควบคุมเหรียญของคุณ ยอดคงเหลือที่ถูกล็อคจะพร้อมใช้งานเมื่อธุรกรรมที่เกี่ยวข้องเสร็จสมบูรณ์ ในขณะที่ยอดคงเหลือที่แช่แข็งจะไม่สามารถเข้าถึงได้สำหรับธุรกรรมจนกว่าคุณจะตัดสินใจยกเลิกการแช่แข็ง", - "unspent_change": "เปลี่ยน" -} + "unspent_change": "เปลี่ยน", + "Block_remaining": "${status} เหลือบล็อกที่เหลืออยู่", + "labeled_silent_addresses": "ที่อยู่เงียบที่มีป้ายกำกับ", + "use_testnet": "ใช้ testnet" +} \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index b8bf168ce..5734b3133 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -729,5 +729,8 @@ "seed_phrase_length": "Haba ng parirala ng binhi", "unavailable_balance": "Hindi available na balanse", "unavailable_balance_description": "Hindi Available na Balanse: Kasama sa kabuuang ito ang mga pondong naka-lock sa mga nakabinbing transaksyon at ang mga aktibong na-freeze mo sa iyong mga setting ng kontrol ng coin. Magiging available ang mga naka-lock na balanse kapag nakumpleto na ang kani-kanilang mga transaksyon, habang ang mga nakapirming balanse ay nananatiling hindi naa-access para sa mga transaksyon hanggang sa magpasya kang i-unfreeze ang mga ito.", - "unspent_change": "Baguhin" -} + "unspent_change": "Baguhin", + "Block_remaining": "${status} I -block ang natitira", + "labeled_silent_addresses": "May label na tahimik na mga address", + "use_testnet": "Gumamit ng testnet" +} \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index f7962b526..889f3e5a1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -732,5 +732,8 @@ "seed_phrase_length": "Çekirdek cümle uzunluğu", "unavailable_balance": "Kullanılamayan bakiye", "unavailable_balance_description": "Kullanılamayan Bakiye: Bu toplam, bekleyen işlemlerde kilitlenen fonları ve jeton kontrol ayarlarınızda aktif olarak dondurduğunuz fonları içerir. Kilitli bakiyeler, ilgili işlemleri tamamlandıktan sonra kullanılabilir hale gelir; dondurulmuş bakiyeler ise siz onları dondurmaya karar verene kadar işlemler için erişilemez durumda kalır.", - "unspent_change": "Değiştirmek" -} + "unspent_change": "Değiştirmek", + "Block_remaining": "${status} blok kalan blok", + "labeled_silent_addresses": "Etiketli sessiz adresler", + "use_testnet": "TestNet kullanın" +} \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 0e4cc3ecb..3728cc692 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -734,5 +734,8 @@ "seed_phrase_length": "Довжина початкової фрази", "unavailable_balance": "Недоступний баланс", "unavailable_balance_description": "Недоступний баланс: ця сума включає кошти, заблоковані в незавершених транзакціях, і ті, які ви активно заморозили в налаштуваннях контролю монет. Заблоковані баланси стануть доступними після завершення відповідних транзакцій, тоді як заморожені баланси залишаються недоступними для транзакцій, доки ви не вирішите їх розморозити.", - "unspent_change": "Зміна" -} + "unspent_change": "Зміна", + "Block_remaining": "${status} блок залишився", + "labeled_silent_addresses": "Позначені мовчазними адресами", + "use_testnet": "Використовуйте тестову мережу" +} \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index cd05010d7..84a2d3413 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -726,5 +726,8 @@ "seed_phrase_length": " ﯽﺋﺎﺒﻤﻟ ﯽﮐ ﮯﻠﻤﺟ ﮯﮐ ﺞﯿﺑ", "unavailable_balance": " ﺲﻨﻠﯿﺑ ﺏﺎﯿﺘﺳﺩ ﺮﯿﻏ", "unavailable_balance_description": "۔ﮯﺗﺮﮐ ﮟﯿﮩﻧ ﮧﻠﺼﯿﻓ ﺎﮐ ﮯﻧﺮﮐ ﺪﻤﺠﻨﻣ ﻥﺍ ﮟﯿﮩﻧﺍ ﭖﺁ ﮧﮐ ﮏﺗ ﺐﺟ ﮟﯿﮨ ﮯﺘﮨﺭ ﯽﺋﺎﺳﺭ ﻞﺑﺎﻗﺎﻧ ﮏﺗ ﺖﻗﻭ ﺱﺍ ﮯﯿﻟ ﮯﮐ ﻦﯾﺩ ﻦﯿﻟ ﺲﻨﻠﯿﺑ ﺪﻤﺠﻨﻣ ﮧﮐ ﺐﺟ ،ﮯﮔ ﮟﯿﺋﺎﺟ ﻮﮨ ﺏﺎﯿﺘﺳﺩ ﺲﻨﻠﯿﺑ ﻞﻔﻘﻣ ﺪﻌﺑ ﮯﮐ ﮯﻧﻮﮨ ﻞﻤﮑﻣ ﻦﯾﺩ ﻦﯿﻟ ﮧﻘﻠﻌﺘﻣ ﮯﮐ ﻥﺍ ۔ﮯﮨ ﺎﮭﮐﺭ ﺮ", - "unspent_change": "تبدیل کریں" + "unspent_change": "تبدیل کریں", + "Block_remaining": "${status} باقی بلاک", + "labeled_silent_addresses": "خاموش پتے لیبل لگا", + "use_testnet": "ٹیسٹ نیٹ استعمال کریں" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index fc632f8a8..230ea0658 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -728,5 +728,8 @@ "seed_phrase_length": "Gigun gbolohun irugbin", "unavailable_balance": "Iwontunwonsi ti ko si", "unavailable_balance_description": "Iwontunws.funfun ti ko si: Lapapọ yii pẹlu awọn owo ti o wa ni titiipa ni awọn iṣowo isunmọ ati awọn ti o ti didi ni itara ninu awọn eto iṣakoso owo rẹ. Awọn iwọntunwọnsi titiipa yoo wa ni kete ti awọn iṣowo oniwun wọn ba ti pari, lakoko ti awọn iwọntunwọnsi tio tutunini ko ni iraye si fun awọn iṣowo titi iwọ o fi pinnu lati mu wọn kuro.", - "unspent_change": "Yipada" -} + "unspent_change": "Yipada", + "Block_remaining": "${status} Bdund díẹ", + "labeled_silent_addresses": "Awọn adirẹsi ipalọlọ", + "use_testnet": "Lo tele" +} \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e57df96d9..3cfa8fd47 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -733,5 +733,8 @@ "seed_phrase_length": "种子短语长度", "unavailable_balance": "不可用余额", "unavailable_balance_description": "不可用余额:此总额包括锁定在待处理交易中的资金以及您在硬币控制设置中主动冻结的资金。一旦各自的交易完成,锁定的余额将变得可用,而冻结的余额在您决定解冻之前仍然无法进行交易。", - "unspent_change": "改变" -} + "unspent_change": "改变", + "Block_remaining": "${status}块剩余", + "labeled_silent_addresses": "标记为无声地址", + "use_testnet": "使用TestNet" +} \ No newline at end of file diff --git a/scripts/android/shell.nix b/scripts/android/shell.nix new file mode 100644 index 000000000..b89da09c0 --- /dev/null +++ b/scripts/android/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.curl + pkgs.unzip + pkgs.automake + pkgs.file + pkgs.pkg-config + pkgs.git + pkgs.libtool + pkgs.ncurses5 + pkgs.openjdk8 + pkgs.clang + ]; +} diff --git a/tool/configure.dart b/tool/configure.dart index bc32581a6..dcc1fecd3 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -71,6 +71,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ @@ -87,12 +88,15 @@ abstract class Bitcoin { TransactionPriority deserializeBitcoinTransactionPriority(int raw); TransactionPriority deserializeLitecoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); - Future generateNewAddress(Object wallet); + Future generateNewAddress(Object wallet, {String? label}); Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); List getAddresses(Object wallet); String getAddress(Object wallet); + String getReceiveAddress(Object wallet); + btc.SilentPaymentAddress? getSilentAddress(Object wallet); + List getSilentAddresses(Object wallet); String formatterBitcoinAmountToString({required int amount}); double formatterBitcoinAmountToDouble({required int amount});