From a05511589c41daef1578e27edd7d4ec8cde6995c Mon Sep 17 00:00:00 2001 From: rafael_xmr Date: Wed, 2 Jul 2025 17:17:44 -0300 Subject: [PATCH 1/2] feat: electrum and sp only changes --- .../electrum/electrum_ssl_service.dart | 95 --- .../electrum/electrum_tcp_service.dart | 92 --- .../electrum/electrum_websocket_service.dart | 91 -- .../electrum/request_completer.dart | 7 - example/pubspec.yaml | 2 +- lib/bitcoin_base.dart | 12 + lib/src/provider/api_provider.dart | 5 +- .../provider/api_provider/api_provider.dart | 148 ---- .../api_provider/electrum_api_provider.dart | 35 - lib/src/provider/api_provider/providers.dart | 2 - .../provider/electrum_methods/methods.dart | 1 + .../electrum_methods/methods/add_peer.dart | 10 +- .../methods/block_headers.dart | 9 +- .../electrum_methods/methods/broad_cast.dart | 13 +- .../methods/donate_address.dart | 8 +- .../methods/electrum_version.dart | 11 +- .../methods/estimate_fee.dart | 12 +- .../electrum_methods/methods/get_balance.dart | 46 +- .../methods/get_fee_histogram.dart | 12 +- .../electrum_methods/methods/get_history.dart | 47 +- .../electrum_methods/methods/get_mempool.dart | 11 +- .../electrum_methods/methods/get_merkle.dart | 12 +- .../methods/get_transaction.dart | 143 +++- .../electrum_methods/methods/get_unspet.dart | 17 +- .../methods/get_value_proof.dart | 9 +- .../electrum_methods/methods/header.dart | 8 +- .../methods/headers_subscribe.dart | 25 +- .../electrum_methods/methods/id_from_pos.dart | 12 +- .../masternode_announce_broadcast.dart | 11 +- .../methods/masternode_list.dart | 9 +- .../methods/masternode_subscribe.dart | 8 +- .../electrum_methods/methods/ping.dart | 6 +- .../electrum_methods/methods/protx_diff.dart | 10 +- .../electrum_methods/methods/protx_info.dart | 8 +- .../electrum_methods/methods/relay_fee.dart | 7 +- .../methods/scripthash_unsubscribe.dart | 10 +- .../methods/server_banner.dart | 6 +- .../methods/server_features.dart | 6 +- .../methods/server_peer_subscribe.dart | 9 +- .../electrum_methods/methods/status.dart | 45 +- .../methods/tweaks_subscribe.dart | 120 +++ .../block_cypher/block_cypher_models.dart | 19 +- lib/src/provider/models/config.dart | 87 +- .../models/electrum/electrum_utxo.dart | 44 +- .../provider/models/fee_rate/fee_rate.dart | 83 +- .../models/mempool/mempol_models.dart | 16 +- lib/src/provider/providers/electrum.dart | 106 +++ .../provider/service/electrum/electrum.dart | 5 + .../electrum/electrum_ssl_service.dart | 401 +++++++++ .../electrum/electrum_tcp_service.dart | 393 +++++++++ .../service/electrum/electrum_version.dart | 49 ++ .../electrum/electrum_websocket_service.dart | 85 ++ .../provider/service/electrum/methods.dart | 34 +- lib/src/provider/service/electrum/params.dart | 124 ++- .../service/electrum/request_completer.dart | 14 + .../provider/service/electrum/service.dart | 61 +- .../forked_transaction_builder.dart | 127 +-- pubspec.yaml | 9 +- test/fixtures/silent_payments.json | 780 +++++++++--------- test/silent_payments.dart | 120 +-- 60 files changed, 2443 insertions(+), 1264 deletions(-) delete mode 100644 example/lib/services_examples/electrum/electrum_ssl_service.dart delete mode 100644 example/lib/services_examples/electrum/electrum_tcp_service.dart delete mode 100644 example/lib/services_examples/electrum/electrum_websocket_service.dart delete mode 100644 example/lib/services_examples/electrum/request_completer.dart delete mode 100644 lib/src/provider/api_provider/api_provider.dart delete mode 100644 lib/src/provider/api_provider/electrum_api_provider.dart delete mode 100644 lib/src/provider/api_provider/providers.dart create mode 100644 lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart create mode 100644 lib/src/provider/providers/electrum.dart create mode 100644 lib/src/provider/service/electrum/electrum_ssl_service.dart create mode 100644 lib/src/provider/service/electrum/electrum_tcp_service.dart create mode 100644 lib/src/provider/service/electrum/electrum_version.dart create mode 100644 lib/src/provider/service/electrum/electrum_websocket_service.dart create mode 100644 lib/src/provider/service/electrum/request_completer.dart diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart deleted file mode 100644 index 362c306..0000000 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ /dev/null @@ -1,95 +0,0 @@ -/// Simple example how to send request to electurm with secure socket - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumSSLService with BitcoinBaseElectrumRPCService { - ElectrumSSLService._( - this.url, - SecureSocket channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); - } - SecureSocket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final parts = url.split(":"); - final channel = await SecureSocket.connect( - parts[0], - int.parse(parts[1]), - onBadCertificate: (certificate) => true, - ).timeout(connectionTimeOut); - - return ElectrumSSLService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(List event) { - final Map decode = json.decode(utf8.decode(event)); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toTCPParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/electrum_tcp_service.dart b/example/lib/services_examples/electrum/electrum_tcp_service.dart deleted file mode 100644 index 750aad0..0000000 --- a/example/lib/services_examples/electrum/electrum_tcp_service.dart +++ /dev/null @@ -1,92 +0,0 @@ -/// Simple example how to send request to electurm with tcp - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumTCPService with BitcoinBaseElectrumRPCService { - ElectrumTCPService._( - this.url, - Socket channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); - } - Socket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final parts = url.split(":"); - final channel = await Socket.connect(parts[0], int.parse(parts[1])) - .timeout(connectionTimeOut); - - return ElectrumTCPService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(List event) { - final Map decode = json.decode(utf8.decode(event)); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/electrum_websocket_service.dart b/example/lib/services_examples/electrum/electrum_websocket_service.dart deleted file mode 100644 index 6ca10a2..0000000 --- a/example/lib/services_examples/electrum/electrum_websocket_service.dart +++ /dev/null @@ -1,91 +0,0 @@ -/// Simple example how to send request to electurm with websocket -import 'dart:async'; -import 'dart:convert'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/cross_platform_websocket/core.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { - ElectrumWebSocketService._( - this.url, - WebSocketCore channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = channel.stream - .cast() - .listen(_onMessge, onError: _onClose, onDone: _onDone); - } - WebSocketCore? _socket; - StreamSubscription? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.sink(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final channel = - await WebSocketCore.connect(url, protocols: protocols?.toList()); - - return ElectrumWebSocketService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(String event) { - final Map decode = json.decode(event); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/request_completer.dart b/example/lib/services_examples/electrum/request_completer.dart deleted file mode 100644 index e6f00dc..0000000 --- a/example/lib/services_examples/electrum/request_completer.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:async'; - -class AsyncRequestCompleter { - AsyncRequestCompleter(this.params); - final Completer> completer = Completer(); - final Map params; -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 25d16d4..4c53070 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + ref: cake-update-v4 http: ^1.2.0 dev_dependencies: diff --git a/lib/bitcoin_base.dart b/lib/bitcoin_base.dart index cb559d7..204e31a 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -27,3 +27,15 @@ export 'package:bitcoin_base/src/bitcoin_cash/bitcoin_cash.dart'; export 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; export 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; + +export 'src/exception/exception.dart'; + +export 'package:bitcoin_base/src/provider/transaction_builder/builder.dart'; + +export 'src/provider/models/electrum/electrum_utxo.dart'; + +export 'src/provider/service/electrum/electrum.dart'; + +export 'src/provider/service/electrum/electrum_version.dart'; + +export 'src/utils/btc_utils.dart'; diff --git a/lib/src/provider/api_provider.dart b/lib/src/provider/api_provider.dart index c043b56..98a8ce7 100644 --- a/lib/src/provider/api_provider.dart +++ b/lib/src/provider/api_provider.dart @@ -1,5 +1,4 @@ export 'models/models.dart'; -export 'transaction_builder/builder.dart'; -export 'service/services.dart'; -export 'api_provider/providers.dart'; +export 'providers/electrum.dart'; export 'electrum_methods/methods.dart'; +export 'service/services.dart'; diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart deleted file mode 100644 index 3f470df..0000000 --- a/lib/src/provider/api_provider/api_provider.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'dart:convert'; -import 'package:bitcoin_base/src/provider/models/models.dart'; -import 'package:bitcoin_base/src/provider/service/http/http_service.dart'; -import 'package:bitcoin_base/src/models/network.dart'; -import 'package:blockchain_utils/utils/string/string.dart'; - -class ApiProvider { - ApiProvider({required this.api, Map? header, required this.service}) - : _header = header ?? {"Content-Type": "application/json"}; - factory ApiProvider.fromMempool(BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.mempool(network); - return ApiProvider(api: api, header: header, service: service); - } - factory ApiProvider.fromBlocCypher(BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.fromBlockCypher(network); - return ApiProvider(api: api, header: header, service: service); - } - final APIConfig api; - final ApiService service; - - final Map _header; - - Future _getRequest(String url) async { - final response = await service.get(url); - return response; - } - - Future _postReqiest(String url, Object? data) async { - final response = await service.post(url, body: data, headers: _header); - return response; - } - - Future> testmempool(List params) async { - final Map data = { - "jsonrpc": "2.0", - "method": "testmempoolaccept", - "id": DateTime.now().millisecondsSinceEpoch.toString(), - "params": params - }; - final response = await _postReqiest>( - "https://btc.getblock.io/786c97b8-f53f-427b-80f7-9af7bd5bdb84/testnet/", json.encode(data)); - return response; - } - - Future> getAccountUtxo(UtxoAddressDetails owner, - {String Function(String)? tokenize}) async { - final apiUrl = api.getUtxoUrl(owner.address.toAddress(api.network)); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - final utxos = (response as List).map((e) => MempolUtxo.fromJson(e)).toList(); - return utxos.toUtxoWithOwnerList(owner); - default: - final blockCypherUtxo = BlockCypherUtxo.fromJson(response); - return blockCypherUtxo.toUtxoWithOwner(owner); - } - } - - Future sendRawTransaction(String txDigest, {String Function(String)? tokenize}) async { - final apiUrl = api.sendTransaction; - final url = tokenize?.call(apiUrl) ?? apiUrl; - - switch (api.apiType) { - case APIType.mempool: - final response = await _postReqiest(url, txDigest); - return response; - default: - final Map digestData = {"tx": txDigest}; - final response = await _postReqiest>(url, json.encode(digestData)); - BlockCypherTransaction? tr; - if (response["tx"] != null) { - tr = BlockCypherTransaction.fromJson(response["tx"]); - } - - tr ??= BlockCypherTransaction.fromJson(response); - return tr.hash; - } - } - - Future getNetworkFeeRate({String Function(String)? tokenize}) async { - final apiUrl = api.getFeeApiUrl(); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest>(url); - switch (api.apiType) { - case APIType.mempool: - return BitcoinFeeRate.fromMempool(response); - default: - return BitcoinFeeRate.fromBlockCypher(response); - } - } - - Future getTransaction(String transactionId, {String Function(String)? tokenize}) async { - final apiUrl = api.getTransactionUrl(transactionId); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest>(url); - switch (api.apiType) { - case APIType.mempool: - return MempoolTransaction.fromJson(response) as T; - default: - return BlockCypherTransaction.fromJson(response) as T; - } - } - - Future> getAccountTransactions(String address, - {String Function(String)? tokenize}) async { - final apiUrl = api.getTransactionsUrl(address); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - final transactions = - (response as List).map((e) => MempoolTransaction.fromJson(e) as T).toList(); - return transactions; - default: - if (response is Map) { - if (response.containsKey("txs")) { - final transactions = (response["txs"] as List) - .map((e) => BlockCypherTransaction.fromJson(e) as T) - .toList(); - return transactions; - } - return []; - } - final transactions = - (response as List).map((e) => BlockCypherTransaction.fromJson(e) as T).toList(); - return transactions; - } - } - - Future getBlockHeight(int height) async { - final url = api.getBlockHeight(height); - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - return response; - default: - final toJson = StringUtils.toJson>(response); - return toJson["hash"]; - } - } - - Future genesis() async { - return getBlockHeight(0); - } -} diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart deleted file mode 100644 index 4db27db..0000000 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; -import 'dart:async'; - -import 'package:blockchain_utils/exception/exceptions.dart'; - -class ElectrumApiProvider { - final BitcoinBaseElectrumRPCService rpc; - ElectrumApiProvider(this.rpc); - int _id = 0; - - /// Sends a request to the Electrum server using the specified [request] parameter. - /// - /// The [timeout] parameter, if provided, sets the maximum duration for the request. - Future request(ElectrumRequest request, [Duration? timeout]) async { - final id = ++_id; - final params = request.toRequest(id); - final data = await rpc.call(params, timeout); - return request.onResonse(_findResult(data, params)); - } - - dynamic _findResult(Map data, ElectrumRequestDetails request) { - if (data["error"] != null) { - final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; - final message = data["error"]?['message'] ?? ""; - throw RPCError( - errorCode: code, - message: message, - data: data["error"]?["data"], - request: data["request"] ?? request.params, - ); - } - - return data["result"]; - } -} diff --git a/lib/src/provider/api_provider/providers.dart b/lib/src/provider/api_provider/providers.dart deleted file mode 100644 index df92a29..0000000 --- a/lib/src/provider/api_provider/providers.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'api_provider.dart'; -export 'electrum_api_provider.dart'; diff --git a/lib/src/provider/electrum_methods/methods.dart b/lib/src/provider/electrum_methods/methods.dart index 052a047..39733df 100644 --- a/lib/src/provider/electrum_methods/methods.dart +++ b/lib/src/provider/electrum_methods/methods.dart @@ -27,3 +27,4 @@ export 'methods/relay_fee.dart'; export 'methods/scripthash_unsubscribe.dart'; export 'methods/server_peer_subscribe.dart'; export 'methods/status.dart'; +export 'methods/tweaks_subscribe.dart'; diff --git a/lib/src/provider/electrum_methods/methods/add_peer.dart b/lib/src/provider/electrum_methods/methods/add_peer.dart index fb23bba..c5f69da 100644 --- a/lib/src/provider/electrum_methods/methods/add_peer.dart +++ b/lib/src/provider/electrum_methods/methods/add_peer.dart @@ -1,10 +1,10 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// A newly-started server uses this call to get itself into other servers’ peers lists. /// It should not be used by wallet clients. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumAddPeer extends ElectrumRequest { - ElectrumAddPeer({required this.features}); +class ElectrumRequestAddPeer extends ElectrumRequest { + ElectrumRequestAddPeer({required this.features}); /// The same information that a call to the sender’s server.features() RPC call would return. final Map features; @@ -14,14 +14,14 @@ class ElectrumAddPeer extends ElectrumRequest { String get method => ElectrumRequestMethods.serverAddPeer.method; @override - List toJson() { + List toParams() { return [features]; } /// A boolean indicating whether the request was tentatively accepted /// The requesting server will appear in server.peers.subscribe() when further sanity checks complete successfully. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/block_headers.dart b/lib/src/provider/electrum_methods/methods/block_headers.dart index ef75150..ceff2fb 100644 --- a/lib/src/provider/electrum_methods/methods/block_headers.dart +++ b/lib/src/provider/electrum_methods/methods/block_headers.dart @@ -1,10 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a concatenated chunk of block headers from the main chain. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeaders +class ElectrumRequestBlockHeaders extends ElectrumRequest, Map> { - ElectrumBlockHeaders( + ElectrumRequestBlockHeaders( {required this.startHeight, required this.count, required this.cpHeight}); /// The height of the first header requested, a non-negative integer. @@ -21,13 +22,13 @@ class ElectrumBlockHeaders String get method => ElectrumRequestMethods.blockHeaders.method; @override - List toJson() { + List toParams() { return [startHeight, count, cpHeight]; } /// A dictionary @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/broad_cast.dart b/lib/src/provider/electrum_methods/methods/broad_cast.dart index fef43d0..b89cce1 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -1,26 +1,25 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Broadcast a transaction to the network. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBroadCastTransaction extends ElectrumRequest { - ElectrumBroadCastTransaction({required this.transactionRaw}); +class ElectrumRequestBroadCastTransaction extends ElectrumRequest { + ElectrumRequestBroadCastTransaction({required this.transactionRaw}); /// The raw transaction as a hexadecimal string. final String transactionRaw; /// blockchain.transaction.broadcast @override - String get method => ElectrumRequestMethods.broadCast.method; + String get method => ElectrumRequestMethods.broadcast.method; @override - List toJson() { + List toParams() { return [transactionRaw]; } /// The transaction hash as a hexadecimal string. @override - String onResonse(result) { + String? onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/donate_address.dart b/lib/src/provider/electrum_methods/methods/donate_address.dart index 4f59989..1195514 100644 --- a/lib/src/provider/electrum_methods/methods/donate_address.dart +++ b/lib/src/provider/electrum_methods/methods/donate_address.dart @@ -1,19 +1,19 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a server donation address. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumDonationAddress extends ElectrumRequest { +class ElectrumRequestDonationAddress extends ElectrumRequest { /// server.donation_address @override String get method => ElectrumRequestMethods.serverDontionAddress.method; @override - List toJson() { + List toParams() { return []; } @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index 1a1f858..6924d3c 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -2,27 +2,26 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumVersion extends ElectrumRequest, List> { - ElectrumVersion({required this.clientName, required this.protocolVersion}); +class ElectrumRequestVersion extends ElectrumRequest, List> { + ElectrumRequestVersion({required this.clientName, required this.protocolVersion}); /// A string identifying the connecting client software. final String clientName; - /// An array [protocol_min, protocol_max], each of which is a string. - final List protocolVersion; + final String protocolVersion; /// blockchain.version @override String get method => ElectrumRequestMethods.version.method; @override - List toJson() { + List toParams() { return [clientName, protocolVersion]; } /// identifying the server and the protocol version that will be used for future communication. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/estimate_fee.dart b/lib/src/provider/electrum_methods/methods/estimate_fee.dart index 9e3b2f5..103676a 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -3,8 +3,8 @@ import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumEstimateFee extends ElectrumRequest { - ElectrumEstimateFee({this.numberOfBlock = 2}); +class ElectrumRequestEstimateFee extends ElectrumRequest { + ElectrumRequestEstimateFee({this.numberOfBlock = 2}); /// The number of blocks to target for confirmation. final int numberOfBlock; @@ -14,13 +14,15 @@ class ElectrumEstimateFee extends ElectrumRequest { String get method => ElectrumRequestMethods.estimateFee.method; @override - List toJson() { + List toParams() { return [numberOfBlock]; } /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResonse(result) { - return BtcUtils.toSatoshi(result.toString()).abs(); + BigInt? onResponse(result) { + final fee = BtcUtils.toSatoshi(result.toString()); + if (fee.isNegative) return null; + return fee; } } diff --git a/lib/src/provider/electrum_methods/methods/get_balance.dart b/lib/src/provider/electrum_methods/methods/get_balance.dart index c9a49e0..c75504e 100644 --- a/lib/src/provider/electrum_methods/methods/get_balance.dart +++ b/lib/src/provider/electrum_methods/methods/get_balance.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the confirmed and unconfirmed balances of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetScriptHashBalance +class ElectrumRequestGetScriptHashBalance extends ElectrumRequest, Map> { - ElectrumGetScriptHashBalance({required this.scriptHash}); + ElectrumRequestGetScriptHashBalance({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,14 +14,49 @@ class ElectrumGetScriptHashBalance String get method => ElectrumRequestMethods.getBalance.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// A dictionary with keys confirmed and unconfirmed. /// The value of each is the appropriate balance in minimum coin units (satoshis). @override - Map onResonse(Map result) { + Map onResponse(Map result) { return result; } } + +class ElectrumBatchRequestGetScriptHashBalance + extends ElectrumBatchRequest, Map> { + ElectrumBatchRequestGetScriptHashBalance({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.getBalance.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: Map.from(data['result']), + ); + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart index 8c89c23..4371d93 100644 --- a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart +++ b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart @@ -1,16 +1,14 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a histogram of the fee rates paid by transactions in the memory pool, weighted by transaction size. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetFeeHistogram - extends ElectrumRequest>, List> { +class ElectrumRequestGetFeeHistogram extends ElectrumRequest>, List> { /// mempool.get_fee_histogram @override String get method => ElectrumRequestMethods.getFeeHistogram.method; @override - List toJson() { + List toParams() { return []; } @@ -19,7 +17,7 @@ class ElectrumGetFeeHistogram /// fee uses sat/vbyte as unit, and must be a non-negative integer or float. /// vsize uses vbyte as unit, and must be a non-negative integer. @override - List> onResonse(result) { - return result.map((e) => List.from(e)).toList(); + List> onResponse(result) { + return result.map((e) => (e as List).map((e) => (e as num).toDouble()).toList()).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_history.dart b/lib/src/provider/electrum_methods/methods/get_history.dart index 9f8efa2..bf1ea72 100644 --- a/lib/src/provider/electrum_methods/methods/get_history.dart +++ b/lib/src/provider/electrum_methods/methods/get_history.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the confirmed and unconfirmed history of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetHistory +class ElectrumRequestScriptHashGetHistory extends ElectrumRequest>, List> { - ElectrumScriptHashGetHistory({required this.scriptHash}); + ElectrumRequestScriptHashGetHistory({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,7 +14,7 @@ class ElectrumScriptHashGetHistory String get method => ElectrumRequestMethods.getHistory.method; @override - List toJson() { + List toParams() { return [scriptHash]; } @@ -23,7 +22,43 @@ class ElectrumScriptHashGetHistory /// with the output of blockchain.scripthash.get_mempool() appended to the list. /// Each confirmed transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } + +class ElectrumBatchRequestScriptHashGetHistory + extends ElectrumBatchRequest>, Map> { + ElectrumBatchRequestScriptHashGetHistory({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.getHistory.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult>> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + final result = data['result'] as List; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: result.map((e) => Map.from(e)).toList(), + ); + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_mempool.dart b/lib/src/provider/electrum_methods/methods/get_mempool.dart index fcd33a2..d416626 100644 --- a/lib/src/provider/electrum_methods/methods/get_mempool.dart +++ b/lib/src/provider/electrum_methods/methods/get_mempool.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the unconfirmed transactions of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetMempool +class ElectrumRequestScriptHashGetMempool extends ElectrumRequest>, List> { - ElectrumScriptHashGetMempool({required this.scriptHash}); + ElectrumRequestScriptHashGetMempool({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,13 +14,13 @@ class ElectrumScriptHashGetMempool String get method => ElectrumRequestMethods.getMempool.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// A list of mempool transactions in arbitrary order. Each mempool transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_merkle.dart b/lib/src/provider/electrum_methods/methods/get_merkle.dart index a39cd8a..6f84e1b 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -1,11 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the merkle branch to a confirmed transaction given its hash and height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetMerkle - extends ElectrumRequest, Map> { - ElectrumGetMerkle({required this.transactionHash, required this.height}); +class ElectrumRequestGetMerkle extends ElectrumRequest, Map> { + ElectrumRequestGetMerkle({required this.transactionHash, required this.height}); /// The transaction hash as a hexadecimal string. final String transactionHash; @@ -18,12 +16,12 @@ class ElectrumGetMerkle String get method => ElectrumRequestMethods.getMerkle.method; @override - List toJson() { + List toParams() { return [transactionHash, height]; } @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_transaction.dart b/lib/src/provider/electrum_methods/methods/get_transaction.dart index f18dd84..efaa4b9 100644 --- a/lib/src/provider/electrum_methods/methods/get_transaction.dart +++ b/lib/src/provider/electrum_methods/methods/get_transaction.dart @@ -1,24 +1,51 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; /// Return a raw transaction. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetTransaction extends ElectrumRequest { - ElectrumGetTransaction({required this.transactionHash, this.verbose = false}); +class ElectrumRequestGetTransactionHex extends ElectrumRequest { + ElectrumRequestGetTransactionHex({required this.transactionHash}); /// The transaction hash as a hexadecimal string. final String transactionHash; - /// Whether a verbose coin-specific response is required. - final bool verbose; + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, false]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + String onResponse(result) { + return result; + } +} + +class ElectrumBatchRequestGetTransactionHex + extends ElectrumBatchRequest> { + ElectrumBatchRequestGetTransactionHex({required this.transactionHashes}); + + /// The transaction hash as a hexadecimal string. + final List transactionHashes; /// blockchain.transaction.get @override String get method => ElectrumRequestMethods.getTransaction.method; @override - List toJson() { - return [transactionHash, verbose]; + List toParams() { + return [ + ...transactionHashes.map((e) => [e]) + ]; } /// If verbose is false: @@ -27,7 +54,105 @@ class ElectrumGetTransaction extends ElectrumRequest { /// If verbose is true: /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. @override - dynamic onResonse(result) { + ElectrumBatchRequestResult onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: data['result'] as String, + ); + } +} + +class ElectrumRequestGetTransactionVerbose + extends ElectrumRequest, Map> { + ElectrumRequestGetTransactionVerbose({required this.transactionHash}); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, true]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + Map onResponse(result) { return result; } } + +class ElectrumBatchRequestGetTransactionVerbose + extends ElectrumBatchRequest, Map> { + ElectrumBatchRequestGetTransactionVerbose({required this.transactionHashes}); + + /// The transaction hash as a hexadecimal string. + final List transactionHashes; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [ + ...transactionHashes.map((e) => [e, true]) + ]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + ElectrumBatchRequestResult> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: data['result'] as Map, + ); + } +} + +/// Return a raw transaction. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumRequestGetRawTransaction extends ElectrumRequest { + ElectrumRequestGetRawTransaction(this.transactionHash); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, false]; + } + + @override + BtcTransaction onResponse(String result) { + final txBytes = BytesUtils.fromHexString(result); + final tx = BtcTransaction.fromRaw(result); + assert(BytesUtils.bytesEqual(tx.toBytes(), txBytes), result); + return tx; + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index 2a957d1..ffcc08e 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -1,13 +1,11 @@ import 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return an ordered list of UTXOs sent to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashListUnspent +class ElectrumRequestScriptHashListUnspent extends ElectrumRequest, List> { - ElectrumScriptHashListUnspent( - {required this.scriptHash, this.includeTokens = false}); + ElectrumRequestScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -20,8 +18,8 @@ class ElectrumScriptHashListUnspent String get method => ElectrumRequestMethods.listunspent.method; @override - List toJson() { - return [scriptHash, if (includeTokens) "include_tokens"]; + List toParams() { + return [scriptHash, if (includeTokens) 'include_tokens']; } /// A list of unspent outputs in blockchain order. @@ -29,9 +27,8 @@ class ElectrumScriptHashListUnspent /// Mempool transactions paying to the address are included at the end of the list in an undefined order. /// Any output that is spent in the mempool does not appear. @override - List onResonse(result) { - final List utxos = - result.map((e) => ElectrumUtxo.fromJson(e)).toList(); + List onResponse(result) { + final utxos = result.map((e) => ElectrumUtxo.fromJson(e)).toList(); return utxos; } } diff --git a/lib/src/provider/electrum_methods/methods/get_value_proof.dart b/lib/src/provider/electrum_methods/methods/get_value_proof.dart index bebe814..5e0a8ab 100644 --- a/lib/src/provider/electrum_methods/methods/get_value_proof.dart +++ b/lib/src/provider/electrum_methods/methods/get_value_proof.dart @@ -2,9 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns a name resolution proof, suitable for low-latency (single round-trip) resolution. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetValueProof - extends ElectrumRequest, dynamic> { - ElectrumGetValueProof({required this.scriptHash, required this.cpHeight}); +class ElectrumRequestGetValueProof extends ElectrumRequest, dynamic> { + ElectrumRequestGetValueProof({required this.scriptHash, required this.cpHeight}); /// Script hash of the name being resolved. final String scriptHash; @@ -17,7 +16,7 @@ class ElectrumGetValueProof String get method => ElectrumRequestMethods.getValueProof.method; @override - List toJson() { + List toParams() { return [scriptHash, cpHeight]; } @@ -25,7 +24,7 @@ class ElectrumGetValueProof /// from the most recent update back to either the registration transaction or a /// checkpointed transaction (whichever is later). @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/header.dart b/lib/src/provider/electrum_methods/methods/header.dart index 97120b5..3000c3f 100644 --- a/lib/src/provider/electrum_methods/methods/header.dart +++ b/lib/src/provider/electrum_methods/methods/header.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the block header at the given height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeader extends ElectrumRequest { - ElectrumBlockHeader({required this.startHeight, required this.cpHeight}); +class ElectrumRequestBlockHeader extends ElectrumRequest { + ElectrumRequestBlockHeader({required this.startHeight, required this.cpHeight}); final int startHeight; final int cpHeight; @@ -12,7 +12,7 @@ class ElectrumBlockHeader extends ElectrumRequest { String get method => ElectrumRequestMethods.blockHeader.method; @override - List toJson() { + List toParams() { return [startHeight, cpHeight]; } @@ -22,7 +22,7 @@ class ElectrumBlockHeader extends ElectrumRequest { /// This provides a proof that the given header is present in the blockchain; /// presumably the client has the merkle root hard-coded as a checkpoint. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart index 651c695..50c1760 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -1,21 +1,36 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +class ElectrumHeaderResponse { + final String hex; + final int height; + + ElectrumHeaderResponse(this.hex, this.height); + + factory ElectrumHeaderResponse.fromJson(Map json) { + return ElectrumHeaderResponse(json['hex'], json['height']); + } + + Map toJson() { + return {'hex': hex, 'height': height}; + } +} + /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumHeaderSubscribe - extends ElectrumRequest, Map> { +class ElectrumRequestHeaderSubscribe + extends ElectrumRequest> { /// blockchain.headers.subscribe @override String get method => ElectrumRequestMethods.headersSubscribe.method; @override - List toJson() { + List toParams() { return []; } /// The header of the current block chain tip. @override - Map onResonse(result) { - return result; + ElectrumHeaderResponse onResponse(result) { + return ElectrumHeaderResponse.fromJson(result); } } diff --git a/lib/src/provider/electrum_methods/methods/id_from_pos.dart b/lib/src/provider/electrum_methods/methods/id_from_pos.dart index 865f0b2..5739a48 100644 --- a/lib/src/provider/electrum_methods/methods/id_from_pos.dart +++ b/lib/src/provider/electrum_methods/methods/id_from_pos.dart @@ -1,11 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a transaction hash and optionally a merkle proof, given a block height and a position in the block. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumIdFromPos extends ElectrumRequest { - ElectrumIdFromPos( - {required this.height, required this.txPos, this.merkle = false}); +class ElectrumRequestIdFromPos extends ElectrumRequest { + ElectrumRequestIdFromPos({required this.height, required this.txPos, this.merkle = false}); /// The main chain block height, a non-negative integer. final int height; @@ -21,13 +19,13 @@ class ElectrumIdFromPos extends ElectrumRequest { String get method => ElectrumRequestMethods.idFromPos.method; @override - List toJson() { + List toParams() { return [height, txPos, merkle]; } /// If merkle is false, the transaction hash as a hexadecimal string. If true, a dictionary @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart index f3169d3..00004da 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart @@ -4,23 +4,22 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Whenever a masternode comes online or a client is syncing, /// they will send this message which describes the masternode entry and how to validate messages from it. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeAnnounceBroadcast extends ElectrumRequest { - ElectrumMasternodeAnnounceBroadcast({required this.signmnb}); +class ElectrumRequestMasternodeAnnounceBroadcast extends ElectrumRequest { + ElectrumRequestMasternodeAnnounceBroadcast({required this.signmnb}); final String signmnb; /// masternode.announce.broadcast @override - String get method => - ElectrumRequestMethods.masternodeAnnounceBroadcast.method; + String get method => ElectrumRequestMethods.masternodeAnnounceBroadcast.method; @override - List toJson() { + List toParams() { return [signmnb]; } /// true if the message was broadcasted successfully otherwise false. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_list.dart b/lib/src/provider/electrum_methods/methods/masternode_list.dart index 437dc3b..62b2ac7 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_list.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_list.dart @@ -2,9 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns the list of masternodes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeList - extends ElectrumRequest, List> { - ElectrumMasternodeList({required this.payees}); +class ElectrumRequestMasternodeList extends ElectrumRequest, List> { + ElectrumRequestMasternodeList({required this.payees}); /// An array of masternode payee addresses. final List payees; @@ -14,13 +13,13 @@ class ElectrumMasternodeList String get method => ElectrumRequestMethods.masternodeList.method; @override - List toJson() { + List toParams() { return [payees]; } /// An array with the masternodes information. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart index 2734415..f5bebd2 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns the status of masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeSubscribe extends ElectrumRequest { - ElectrumMasternodeSubscribe({required this.collateral}); +class ElectrumRequestMasternodeSubscribe extends ElectrumRequest { + ElectrumRequestMasternodeSubscribe({required this.collateral}); /// The txId and the index of the collateral. Example ("8c59133e714797650cf69043d05e409bbf45670eed7c4e4a386e52c46f1b5e24-0") final String collateral; @@ -13,7 +13,7 @@ class ElectrumMasternodeSubscribe extends ElectrumRequest { String get method => ElectrumRequestMethods.masternodeSubscribe.method; @override - List toJson() { + List toParams() { return [collateral]; } @@ -22,7 +22,7 @@ class ElectrumMasternodeSubscribe extends ElectrumRequest { /// the internet connection, the offline time and even the collateral /// amount, so this subscription notice these changes to the user. @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/ping.dart b/lib/src/provider/electrum_methods/methods/ping.dart index 4d41704..f767b31 100644 --- a/lib/src/provider/electrum_methods/methods/ping.dart +++ b/lib/src/provider/electrum_methods/methods/ping.dart @@ -2,17 +2,17 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Ping the server to ensure it is responding, and to keep the session alive. The server may disconnect clients that have sent no requests for roughly 10 minutes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumPing extends ElectrumRequest { +class ElectrumRequestPing extends ElectrumRequest { @override String get method => ElectrumRequestMethods.ping.method; @override - List toJson() { + List toParams() { return []; } @override - dynamic onResonse(result) { + dynamic onResponse(result) { return null; } } diff --git a/lib/src/provider/electrum_methods/methods/protx_diff.dart b/lib/src/provider/electrum_methods/methods/protx_diff.dart index cca5d7d..c7d99d5 100644 --- a/lib/src/provider/electrum_methods/methods/protx_diff.dart +++ b/lib/src/provider/electrum_methods/methods/protx_diff.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns a diff between two deterministic masternode lists. The result also contains proof data.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXDiff extends ElectrumRequest, dynamic> { - ElectrumProtXDiff({required this.baseHeight, required this.height}); +class ElectrumRequestProtXDiff + extends ElectrumRequest, dynamic> { + ElectrumRequestProtXDiff({required this.baseHeight, required this.height}); /// The starting block height final int baseHeight; @@ -16,13 +18,13 @@ class ElectrumProtXDiff extends ElectrumRequest, dynamic> { String get method => ElectrumRequestMethods.protxDiff.method; @override - List toJson() { + List toParams() { return [baseHeight, height]; } /// A dictionary with deterministic masternode lists diff plus proof data. @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/protx_info.dart b/lib/src/provider/electrum_methods/methods/protx_info.dart index 7685699..a5dcac2 100644 --- a/lib/src/provider/electrum_methods/methods/protx_info.dart +++ b/lib/src/provider/electrum_methods/methods/protx_info.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns detailed information about a deterministic masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXInfo extends ElectrumRequest, dynamic> { - ElectrumProtXInfo({required this.protxHash}); +class ElectrumRequestProtXInfo extends ElectrumRequest, dynamic> { + ElectrumRequestProtXInfo({required this.protxHash}); /// The hash of the initial ProRegTx final String protxHash; @@ -13,13 +13,13 @@ class ElectrumProtXInfo extends ElectrumRequest, dynamic> { String get method => ElectrumRequestMethods.protxInfo.method; @override - List toJson() { + List toParams() { return [protxHash]; } /// A dictionary with detailed deterministic masternode data @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/relay_fee.dart b/lib/src/provider/electrum_methods/methods/relay_fee.dart index 267dccf..e641901 100644 --- a/lib/src/provider/electrum_methods/methods/relay_fee.dart +++ b/lib/src/provider/electrum_methods/methods/relay_fee.dart @@ -1,21 +1,22 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumRelayFee extends ElectrumRequest { +class ElectrumRequestRelayFee extends ElectrumRequest { /// blockchain.relayfee @override String get method => ElectrumRequestMethods.relayFee.method; @override - List toJson() { + List toParams() { return []; } /// relay fee in Bigint(satoshi) @override - BigInt onResonse(result) { + BigInt onResponse(result) { return BtcUtils.toSatoshi(result.toString()); } } diff --git a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart index 4990b6d..51c4349 100644 --- a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart +++ b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart @@ -1,9 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Unsubscribe from a script hash, preventing future notifications if its status changes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashUnSubscribe extends ElectrumRequest { - ElectrumScriptHashUnSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashUnSubscribe extends ElectrumRequest { + ElectrumRequestScriptHashUnSubscribe({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -13,7 +13,7 @@ class ElectrumScriptHashUnSubscribe extends ElectrumRequest { String get method => ElectrumRequestMethods.scriptHashUnSubscribe.method; @override - List toJson() { + List toParams() { return [scriptHash]; } @@ -21,7 +21,7 @@ class ElectrumScriptHashUnSubscribe extends ElectrumRequest { /// otherwise False. Note that False might be returned even /// for something subscribed to earlier, because the server can drop subscriptions in rare circumstances. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_banner.dart b/lib/src/provider/electrum_methods/methods/server_banner.dart index 92e3ab4..d41eed8 100644 --- a/lib/src/provider/electrum_methods/methods/server_banner.dart +++ b/lib/src/provider/electrum_methods/methods/server_banner.dart @@ -2,17 +2,17 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a banner to be shown in the Electrum console. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerBanner extends ElectrumRequest { +class ElectrumRequestServerBanner extends ElectrumRequest { @override String get method => ElectrumRequestMethods.serverBanner.method; @override - List toJson() { + List toParams() { return []; } @override - String onResonse(result) { + String onResponse(result) { return result.toString(); } } diff --git a/lib/src/provider/electrum_methods/methods/server_features.dart b/lib/src/provider/electrum_methods/methods/server_features.dart index 7e9c939..5b36d72 100644 --- a/lib/src/provider/electrum_methods/methods/server_features.dart +++ b/lib/src/provider/electrum_methods/methods/server_features.dart @@ -2,20 +2,20 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a list of features and services supported by the server. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerFeatures extends ElectrumRequest { +class ElectrumRequestServerFeatures extends ElectrumRequest { /// server.features @override String get method => ElectrumRequestMethods.serverFeatures.method; @override - List toJson() { + List toParams() { return []; } /// A dictionary of keys and values. Each key represents a feature or service of the server, /// and the value gives additional information. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart index d1a3db5..f4a77bf 100644 --- a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart @@ -1,21 +1,20 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a list of peer servers. Despite the name this is not a subscription and the server must send no notifications.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerPeersSubscribe - extends ElectrumRequest, List> { +class ElectrumRequestServerPeersSubscribe extends ElectrumRequest, List> { /// server.peers.subscribe @override String get method => ElectrumRequestMethods.serverPeersSubscribe.method; @override - List toJson() { + List toParams() { return []; } /// An array of peer servers, each returned as a 3-element array @override - List onResonse(result) { + List onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/status.dart b/lib/src/provider/electrum_methods/methods/status.dart index f12a0fa..95e09fa 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/status.dart @@ -1,10 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashSubscribe - extends ElectrumRequest, dynamic> { - ElectrumScriptHashSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashSubscribe extends ElectrumRequest { + ElectrumRequestScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -14,13 +13,45 @@ class ElectrumScriptHashSubscribe String get method => ElectrumRequestMethods.scriptHashSubscribe.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// The status of the script hash. @override - Map onResonse(result) { - return Map.from(result); + String? onResponse(result) { + return result; + } +} + +class ElectrumBatchRequestScriptHashSubscribe + extends ElectrumBatchRequest> { + ElectrumBatchRequestScriptHashSubscribe({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.scriptHashSubscribe.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + final result = data['result'] as String?; + return ElectrumBatchRequestResult(request: request, id: id, result: result); } } diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart new file mode 100644 index 0000000..4a940f7 --- /dev/null +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -0,0 +1,120 @@ +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + +class TweakOutputData { + final int vout; + final int amount; + final dynamic spendingInput; + + TweakOutputData({ + required this.vout, + required this.amount, + this.spendingInput, + }); +} + +class TweakData { + final String tweak; + final Map outputPubkeys; + + TweakData({required this.tweak, required this.outputPubkeys}); +} + +class ElectrumTweaksSubscribeResponse { + final String? message; + final int block; + final Map blockTweaks; + + ElectrumTweaksSubscribeResponse({ + required this.block, + required this.blockTweaks, + this.message, + }); + + static ElectrumTweaksSubscribeResponse? fromJson(Map json) { + if (json.isEmpty) { + return null; + } + + if (json.containsKey('params')) { + final params = json['params'] as List; + final message = params.first["message"]; + + if (message != null) { + return null; + } + } + + late int block; + final blockTweaks = {}; + + try { + for (final key in json.keys) { + block = int.parse(key); + final txs = json[key] as Map; + + for (final txid in txs.keys) { + final tweakResponseData = txs[txid] as Map; + + final tweakHex = tweakResponseData["tweak"].toString(); + final outputPubkeys = (tweakResponseData["output_pubkeys"] as Map); + + final tweakOutputData = {}; + + for (final vout in outputPubkeys.keys) { + final outputData = outputPubkeys[vout]; + tweakOutputData[outputData[0]] = TweakOutputData( + vout: int.parse(vout.toString()), + amount: outputData[1], + spendingInput: outputData.length > 2 ? outputData[2] : null, + ); + } + + final tweakData = TweakData(tweak: tweakHex, outputPubkeys: tweakOutputData); + blockTweaks[txid] = tweakData; + } + } + } catch (_) { + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: 0, + blockTweaks: {}, + ); + } + + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: block, + blockTweaks: blockTweaks, + ); + } +} + +/// Subscribe to receive block headers when a new block is found. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumTweaksSubscribe + extends ElectrumRequest> { + /// blockchain.tweaks.subscribe + ElectrumTweaksSubscribe({ + required this.height, + required this.count, + required this.historicalMode, + }); + + final int height; + final int count; + final bool historicalMode; + + @override + String get method => ElectrumRequestMethods.tweaksSubscribe.method; + + @override + List toParams() { + return [height, count, historicalMode]; + } + + /// The header of the current block chain tip. + @override + ElectrumTweaksSubscribeResponse? onResponse(result) { + return ElectrumTweaksSubscribeResponse.fromJson(result); + } +} diff --git a/lib/src/provider/models/block_cypher/block_cypher_models.dart b/lib/src/provider/models/block_cypher/block_cypher_models.dart index 5d4761b..09f994c 100644 --- a/lib/src/provider/models/block_cypher/block_cypher_models.dart +++ b/lib/src/provider/models/block_cypher/block_cypher_models.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; class TxRef implements UTXO { final String txHash; @@ -32,7 +33,7 @@ class TxRef implements UTXO { blockHeight: json['block_height'], txInputN: json['tx_input_n'], txOutputN: json['tx_output_n'], - value: BigInt.from(json['value']), + value: BigintUtils.parse(json['value']), refBalance: json['ref_balance'], spent: json['spent'], confirmations: json['confirmations'], @@ -40,6 +41,20 @@ class TxRef implements UTXO { script: json['script'], ); } + @override + Map toJson() { + return { + "tx_hash": txHash, + "block_height": blockHeight, + "tx_input_n": txInputN, + "value": value.toString(), + "tx_output_n": txOutputN, + "spent": spent, + "confirmations": confirmations, + "confirmed": confirmed.toString(), + "script": script + }; + } @override BitcoinUtxo toUtxo(BitcoinAddressType addressType) { @@ -99,7 +114,7 @@ class BlockCypherUtxo { } List toUtxoWithOwner(UtxoAddressDetails owner) { - List utxos = txRefs.map((ref) { + final utxos = txRefs.map((ref) { return UtxoWithAddress( utxo: ref.toUtxo(owner.address.type), ownerDetails: owner, diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 9369398..01b77ce 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -1,5 +1,5 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; +import 'package:bitcoin_base/src/models/network.dart'; import 'package:bitcoin_base/src/provider/constant/constant.dart'; enum APIType { mempool, blockCypher } @@ -12,7 +12,10 @@ class APIConfig { final String sendTransaction; final String blockHeight; final APIType apiType; + final String rawTransaction; final BasedUtxoNetwork network; + final String? block; + final String? blockTimestamp; factory APIConfig.selectApi(APIType apiType, BasedUtxoNetwork network) { switch (apiType) { @@ -24,8 +27,8 @@ class APIConfig { } String getUtxoUrl(String address) { - String baseUrl = url; - return baseUrl.replaceAll("###", address); + final baseUrl = url; + return baseUrl.replaceAll('###', address); } String getFeeApiUrl() { @@ -33,18 +36,41 @@ class APIConfig { } String getTransactionUrl(String transactionId) { - String baseUrl = transaction; - return baseUrl.replaceAll("###", transactionId); + final baseUrl = transaction; + return baseUrl.replaceAll('###', transactionId); + } + + String getRawTransactionUrl(String transactionId) { + final baseUrl = rawTransaction; + return baseUrl.replaceAll('###', transactionId); + } + + String getBlockUrl(String blockHash) { + if (block == null) { + throw const BitcoinBasePluginException("block url is not available"); + } + + String baseUrl = block!; + return baseUrl.replaceAll("###", blockHash); + } + + String getBlockTimestampUrl(int timestamp) { + if (blockTimestamp == null) { + throw const BitcoinBasePluginException("block timestamp url is not available"); + } + + String baseUrl = blockTimestamp!; + return baseUrl.replaceAll("###", timestamp.toString()); } String getTransactionsUrl(String address) { - String baseUrl = transactions; - return baseUrl.replaceAll("###", address); + final baseUrl = transactions; + return baseUrl.replaceAll('###', address); } String getBlockHeight(int blockHaight) { - String baseUrl = blockHeight; - return baseUrl.replaceAll("###", "$blockHaight"); + final baseUrl = blockHeight; + return baseUrl.replaceAll('###', '$blockHaight'); } factory APIConfig.fromBlockCypher(BasedUtxoNetwork network) { @@ -67,14 +93,14 @@ class APIConfig { break; default: throw BitcoinBasePluginException( - "blockcypher does not support ${network.conf.coinName.name}, u must use your own provider"); + 'blockcypher does not support ${network.conf.coinName.name}, u must use your own provider'); } return APIConfig( - url: - "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", + url: "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", feeRate: baseUrl, transaction: "$baseUrl/txs/###", + rawTransaction: '$baseUrl/txs/###', sendTransaction: "$baseUrl/txs/push", apiType: APIType.blockCypher, transactions: "$baseUrl/addrs/###/full?limit=200", @@ -82,29 +108,33 @@ class APIConfig { blockHeight: "$baseUrl/blocks/###"); } - factory APIConfig.mempool(BasedUtxoNetwork network) { - String baseUrl; - switch (network) { - case BitcoinNetwork.mainnet: - baseUrl = BtcApiConst.mempoolMainBaseURL; - break; - case BitcoinNetwork.testnet: - baseUrl = BtcApiConst.mempoolBaseURL; - break; - default: - throw BitcoinBasePluginException( - "mempool does not support ${network.conf.coinName.name}"); + factory APIConfig.mempool(BasedUtxoNetwork network, [String? baseUrl]) { + if (baseUrl == null) { + switch (network) { + case BitcoinNetwork.mainnet: + baseUrl = BtcApiConst.mempoolMainBaseURL; + break; + case BitcoinNetwork.testnet: + baseUrl = BtcApiConst.mempoolBaseURL; + break; + default: + throw BitcoinBasePluginException( + "mempool does not support ${network.conf.coinName.name}"); + } } return APIConfig( url: "$baseUrl/address/###/utxo", - feeRate: "$baseUrl/v1/fees/recommended", + feeRate: "$baseUrl/fees/recommended", transaction: "$baseUrl/tx/###", + rawTransaction: '$baseUrl/tx/###/hex', sendTransaction: "$baseUrl/tx", apiType: APIType.mempool, transactions: "$baseUrl/address/###/txs", network: network, - blockHeight: "$baseUrl/block-height/###"); + blockHeight: "$baseUrl/block-height/###", + block: "$baseUrl/block/###", + blockTimestamp: "$baseUrl/mining/blocks/timestamp/###"); } APIConfig( @@ -115,5 +145,8 @@ class APIConfig { required this.sendTransaction, required this.apiType, required this.network, - required this.blockHeight}); + required this.blockHeight, + required this.rawTransaction, + this.block, + this.blockTimestamp}); } diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart index 9fd9abc..9b3825f 100644 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ b/lib/src/provider/models/electrum/electrum_utxo.dart @@ -1,19 +1,17 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/cash_token/cash_token.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; +import 'package:blockchain_utils/utils/numbers/utils/int_utils.dart'; class ElectrumUtxo implements UTXO { factory ElectrumUtxo.fromJson(Map json) { - CashToken? token; - if (json.containsKey("token_data")) { - token = CashToken.fromJson(json["token_data"]); - } return ElectrumUtxo._( - height: json["height"], - txId: json["tx_hash"], - vout: json["tx_pos"], - value: BigInt.parse((json["value"].toString())), - token: token); + height: IntUtils.parse(json['height']), + txId: json['tx_hash'], + vout: IntUtils.parse(json['tx_pos']), + value: BigintUtils.parse(json['value']), + token: json["token_data"] == null ? null : CashToken.fromJson(json['token_data'])); } const ElectrumUtxo._( {required this.height, @@ -30,11 +28,27 @@ class ElectrumUtxo implements UTXO { @override BitcoinUtxo toUtxo(BitcoinAddressType addressType) { return BitcoinUtxo( - txHash: txId, - value: value, - vout: vout, - scriptType: addressType, - blockHeight: height, - token: token); + txHash: txId, + value: value, + vout: vout, + scriptType: addressType, + blockHeight: height, + token: token, + ); + } + + static List fromJsonList(List json) { + return json.map((e) => ElectrumUtxo.fromJson(e)).toList(); + } + + @override + Map toJson() { + return { + "token_data": '', + "height": height, + "tx_hash": txId, + "tx_pos": vout, + "value": value.toString() + }; } } diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 09c6312..910a825 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -2,30 +2,45 @@ import 'package:bitcoin_base/src/exception/exception.dart'; enum BitcoinFeeRateType { low, medium, high } +class BitcoinFee { + BitcoinFee({int? satoshis, BigInt? bytes}) + : satoshis = satoshis ?? _parseKbFees(bytes!), + bytes = bytes ?? _parseMempoolFees(satoshis!); + + final int satoshis; + final BigInt bytes; + + @override + String toString() { + return 'satoshis: $satoshis, bytes: $bytes'; + } +} + class BitcoinFeeRate { - BitcoinFeeRate( - {required this.high, - required this.medium, - required this.low, - this.economyFee, - this.hourFee}); + BitcoinFeeRate({ + required this.high, + required this.medium, + required this.low, + this.economyFee, + this.minimumFee, + }); /// High fee rate in satoshis per kilobyte - final BigInt high; + final BitcoinFee high; /// Medium fee rate in satoshis per kilobyte - final BigInt medium; + final BitcoinFee medium; /// low fee rate in satoshis per kilobyte - final BigInt low; + final BitcoinFee low; - /// only mnenpool api - final BigInt? economyFee; + /// only mempool api + final BitcoinFee? economyFee; - /// only mnenpool api - final BigInt? hourFee; + /// only mempool api + final BitcoinFee? minimumFee; - BigInt _feeRatrete(BitcoinFeeRateType feeRateType) { + BitcoinFee _feeRate(BitcoinFeeRateType feeRateType) { switch (feeRateType) { case BitcoinFeeRateType.low: return low; @@ -36,6 +51,10 @@ class BitcoinFeeRate { } } + int toSat(BigInt feeRate) { + return _parseKbFees(feeRate); + } + /// GetEstimate calculates the estimated fee in satoshis for a given transaction size /// and fee rate (in satoshis per kilobyte) using the formula: // @@ -48,16 +67,15 @@ class BitcoinFeeRate { /// Returns: /// - BigInt: A BigInt containing the estimated fee in satoshis. BigInt getEstimate(int trSize, - {BigInt? customFeeRatePerKb, - BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { - BigInt feeRate = customFeeRatePerKb ?? _feeRatrete(feeRateType); + {BigInt? customFeeRatePerKb, BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { + BigInt feeRate = customFeeRatePerKb ?? _feeRate(feeRateType).bytes; final trSizeBigInt = BigInt.from(trSize); return (trSizeBigInt * feeRate) ~/ BigInt.from(1000); } @override String toString() { - return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee hourFee: $hourFee'; + return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee minimumFee: $minimumFee'; } /// NewBitcoinFeeRateFromMempool creates a BitcoinFeeRate structure from JSON data retrieved @@ -65,14 +83,11 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromMempool(Map json) { return BitcoinFeeRate( - high: _parseMempoolFees(json['fastestFee']), - medium: _parseMempoolFees(json['halfHourFee']), - low: _parseMempoolFees(json['minimumFee']), - economyFee: json['economyFee'] == null - ? null - : _parseMempoolFees(json['economyFee']), - hourFee: - json['hourFee'] == null ? null : _parseMempoolFees(json['hourFee']), + high: BitcoinFee(satoshis: json['fastestFee']), + medium: BitcoinFee(satoshis: json['halfHourFee']), + low: BitcoinFee(satoshis: json['hourFee']), + economyFee: json['economyFee'] == null ? null : BitcoinFee(satoshis: json['economyFee']), + minimumFee: json['minimumFee'] == null ? null : BitcoinFee(satoshis: json['minimumFee']), ); } @@ -81,9 +96,10 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromBlockCypher(Map json) { return BitcoinFeeRate( - high: BigInt.from((json['high_fee_per_kb'] as int)), - medium: BigInt.from((json['medium_fee_per_kb'] as int)), - low: BigInt.from((json['low_fee_per_kb'] as int))); + high: BitcoinFee(bytes: BigInt.from((json['high_fee_per_kb'] as int))), + medium: BitcoinFee(bytes: BigInt.from((json['medium_fee_per_kb'] as int))), + low: BitcoinFee(bytes: BigInt.from((json['low_fee_per_kb'] as int))), + ); } } @@ -103,3 +119,12 @@ BigInt _parseMempoolFees(dynamic data) { "cannot parse mempool fees excepted double, string got ${data.runtimeType}"); } } + +/// ParseMempoolFees takes a data dynamic and converts it to a BigInt representing +/// mempool fees in satoshis per kilobyte (sat/KB). The function performs the conversion +/// based on the type of the input data, which can be either a double (floating-point +/// fee rate) or an int (integer fee rate in satoshis per byte). +int _parseKbFees(BigInt fee) { + const kb = 1024; + return (fee.toInt() / kb).round(); +} diff --git a/lib/src/provider/models/mempool/mempol_models.dart b/lib/src/provider/models/mempool/mempol_models.dart index b1ea5b5..d891541 100644 --- a/lib/src/provider/models/mempool/mempol_models.dart +++ b/lib/src/provider/models/mempool/mempol_models.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; class MempoolPrevOut { final String scriptPubKey; @@ -139,10 +140,10 @@ class MempoolTransaction { txID: json['txid'], version: json['version'], locktime: json['locktime'], - vin: - List.from(json['vin'].map((x) => MempoolVin.fromJson(x))), + vin: List.from( + (json['vin'] as List).map((x) => MempoolVin.fromJson(x))), vout: List.from( - json['vout'].map((x) => MempoolVout.fromJson(x))), + (json['vout'] as List).map((x) => MempoolVout.fromJson(x))), size: json['size'], weight: json['weight'], fee: json['fee'], @@ -169,7 +170,7 @@ class MempolUtxo implements UTXO { txid: json['txid'], vout: json['vout'], status: MempoolStatus.fromJson(json['status']), - value: BigInt.parse(json['value'].toString()), + value: BigintUtils.parse(json['value']), ); } @@ -182,11 +183,16 @@ class MempolUtxo implements UTXO { scriptType: addressType, blockHeight: 1); } + + @override + Map toJson() { + return {"txid": txid, "vout": vout, "status": status, "value": value}; + } } extension MempoolUtxoExtentions on List { List toUtxoWithOwnerList(UtxoAddressDetails owner) { - List utxos = map((e) => UtxoWithAddress( + final utxos = map((e) => UtxoWithAddress( utxo: BitcoinUtxo( txHash: e.txid, value: e.value, diff --git a/lib/src/provider/providers/electrum.dart b/lib/src/provider/providers/electrum.dart new file mode 100644 index 0000000..e964b04 --- /dev/null +++ b/lib/src/provider/providers/electrum.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:rxdart/rxdart.dart'; + +class BatchSubscription { + final BehaviorSubject subscription; + final ElectrumBatchRequestDetails params; + + BatchSubscription(this.subscription, this.params); +} + +typedef ListenerCallback = StreamSubscription Function( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, +}); + +class ElectrumProvider { + final BitcoinBaseElectrumRPCService rpc; + ElectrumProvider._(this.rpc); + int _id = 0; + int get id => _id; + Timer? _aliveTimer; + + static Future connect(Future rpc) async { + final provider = ElectrumProvider._(await rpc); + provider.keepAlive(); + return provider; + } + + Future>> batchRequest( + ElectrumBatchRequest request, [ + Duration? timeout, + ]) async { + final id = ++_id; + final params = request.toRequest(id) as ElectrumBatchRequestDetails; + _id = request.finalId; + + final results = await rpc.batchCall(params, timeout); + return results.map((r) => request.onResponse(r, params)).toList(); + } + + /// Sends a request to the Electrum server using the specified [request] parameter. + /// + /// The [timeout] parameter, if provided, sets the maximum duration for the request. + Future request(ElectrumRequest request, [Duration? timeout]) async { + final id = ++_id; + final params = request.toRequest(id); + final result = await rpc.call(params, timeout); + return request.onResponse(result); + } + + Future>?> batchSubscribe( + ElectrumBatchRequest request, [ + Duration? timeout, + ]) async { + final id = ++_id; + final params = request.toRequest(id) as ElectrumBatchRequestDetails; + _id = request.finalId; + final subscriptions = rpc.batchSubscribe(params); + + if (subscriptions == null) return null; + + return subscriptions.map((s) => BatchSubscription(s.subscription, params)).toList(); + } + + // Preserving generic type T in subscribe method + BehaviorSubject? subscribe(ElectrumRequest request) { + final id = ++_id; + final params = request.toRequest(id); + final subscription = rpc.subscribe(params); + + if (subscription == null) return null; + + return subscription.subscription; + } + + Future> getFeeRates() async { + try { + final topDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 1)); + final middleDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 5)); + final bottomDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 10)); + + final top = (topDoubleString!.toInt() / 1000).round(); + final middle = (middleDoubleString!.toInt() / 1000).round(); + final bottom = (bottomDoubleString!.toInt() / 1000).round(); + + return [bottom, middle, top]; + } catch (_) { + return []; + } + } + + void keepAlive() { + _aliveTimer?.cancel(); + _aliveTimer = Timer.periodic(const Duration(seconds: 6), (_) async => ping()); + } + + void ping() async { + try { + return await request(ElectrumRequestPing()); + } catch (_) {} + } +} diff --git a/lib/src/provider/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index c8b56be..f8bbc94 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -1,3 +1,8 @@ export 'methods.dart'; export 'params.dart'; export 'service.dart'; +export 'electrum_version.dart'; +export 'request_completer.dart'; +export 'electrum_ssl_service.dart'; +export 'electrum_tcp_service.dart'; +// export 'electrum_websocket_service.dart'; diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart new file mode 100644 index 0000000..74d2a51 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -0,0 +1,401 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumSSLService implements BitcoinBaseElectrumRPCService { + ElectrumSSLService._( + this.url, + SecureSocket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + SecureSocket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + await _socket?.close(); + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + _setConnectionStatus(ConnectionStatus.disconnected); + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await SecureSocket.connect( + uri.host, + uri.port, + onBadCertificate: (_) => true, + ).timeout(connectionTimeOut); + + return ElectrumSSLService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + + unterminatedString = ''; + } + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + unterminatedString = ''; + } + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + + if (error != null) { + final message = error["message"]; + + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + + if (id == null && method != null) { + _tasks.forEach((key, value) { + if (value.request.method == method) { + id = key; + } + }); + } + } + + final result = _findResult(response, _tasks[id]?.request); + _finish(id!, result); + } + + void _onMessage(List event) { + try { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } catch (_) {} + } + + dynamic _findResult(dynamic data, BaseElectrumRequestDetails? request) { + final error = data["error"]; + int? id; + try { + id = request?.params["id"]; + } catch (_) {} + final requestParams = data["request"] ?? request?.params; + + if (error != null) { + if (error is String) { + if (id != null) { + _errors[id] = RPCError( + data: error, + errorCode: 0, + message: error, + request: requestParams, + ); + } + } else { + final code = int.tryParse(((error['code']?.toString()) ?? "0")) ?? 0; + final message = error['message'] ?? ""; + + if (id != null) { + _errors[id] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + } + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } + + if (message.toLowerCase().contains("batch limit")) { + for (final k in _tasks.keys) { + final task = _tasks[k]; + + if (task?.isBatchRequest ?? false) { + _errors[k] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + + if (task!.isSubscription) { + task.subject?.addError(_errors[k]!); + } else { + task.completer?.completeError(_errors[k]!); + } + } + } + } + } + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + if (!task.isSubscription) { + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + if (task.isBatchRequest) { + task.completer!.complete({"id": id, "result": result}); + } else { + task.completer!.complete(result); + } + } + + _tasks.remove(id); + } else { + if (task.isBatchRequest) { + task.subject?.add({"id": id, "result": result}); + } else { + task.subject?.add(result); + } + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + List> _registerBatchSubscription( + ElectrumBatchRequestDetails mainParams, + ) { + return mainParams.params.map((params) { + final subscription = AsyncBehaviorSubject(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + subject: subscription.subscription, + request: mainParams, + isSubscription: true, + isBatchRequest: true, + ); + + return subscription; + }).toList(); + } + + @override + List>? batchSubscribe(ElectrumBatchRequestDetails params) { + try { + final subscriptions = _registerBatchSubscription(params); + add(params.toTCPParams()); + + return subscriptions; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + List> _registerBatchTask(ElectrumBatchRequestDetails mainParams) { + return mainParams.params.map((params) { + final completer = AsyncRequestCompleter(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + completer: completer.completer, + request: mainParams, + isSubscription: false, + isBatchRequest: true, + ); + + return completer; + }).toList(); + } + + @override + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]) async { + try { + final completers = _registerBatchTask(params); + add(params.toTCPParams()); + final result = await Future.wait(completers.map((e) => e.completer.future)) + .timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + for (final id in params.paramsById.keys) { + _tasks.remove(id); + } + } + } + + RPCError? getError(int id) => _errors[id]; +} diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart new file mode 100644 index 0000000..0fef7e8 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -0,0 +1,393 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/service.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { + ElectrumTCPService._( + this.url, + Socket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + Socket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + await _socket?.close(); + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + _setConnectionStatus(ConnectionStatus.disconnected); + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await Socket.connect(uri.host, uri.port).timeout(connectionTimeOut); + + return ElectrumTCPService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + + unterminatedString = ''; + } + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + unterminatedString = ''; + } + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + if (response["error"] != null) { + final message = error["message"]; + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + + _tasks.forEach((key, value) { + if (value.request.method == method) { + id = key; + } + }); + } + + final result = _findResult(response, _tasks[id]!.request); + _finish(id!, result); + } + + void _onMessage(List event) { + try { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } catch (_) {} + } + + dynamic _findResult(dynamic data, BaseElectrumRequestDetails? request) { + final error = data["error"]; + int? id; + try { + id = request?.params["id"]; + } catch (_) {} + final requestParams = data["request"] ?? request?.params; + + if (error != null) { + if (error is String) { + if (id != null) { + _errors[id] = RPCError( + data: error, + errorCode: 0, + message: error, + request: requestParams, + ); + } + } else { + final code = int.tryParse(((error['code']?.toString()) ?? "0")) ?? 0; + final message = error['message'] ?? ""; + + if (id != null) { + _errors[id] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + } + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } + + if (message.toLowerCase().contains("batch limit")) { + for (final k in _tasks.keys) { + final task = _tasks[k]; + + if (task?.isBatchRequest ?? false) { + _errors[k] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + + if (task!.isSubscription) { + task.subject?.addError(_errors[k]!); + } else { + task.completer?.completeError(_errors[k]!); + } + } + } + } + } + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + if (!task.isSubscription) { + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + if (task.isBatchRequest) { + task.completer!.complete({"id": id, "result": result}); + } else { + task.completer!.complete(result); + } + } + + _tasks.remove(id); + } else { + if (task.isBatchRequest) { + task.subject?.add({"id": id, "result": result}); + } else { + task.subject?.add(result); + } + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + List> _registerBatchSubscription( + ElectrumBatchRequestDetails mainParams, + ) { + return mainParams.params.map((params) { + final subscription = AsyncBehaviorSubject(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + subject: subscription.subscription, + request: mainParams, + isSubscription: true, + isBatchRequest: true, + ); + + return subscription; + }).toList(); + } + + @override + List>? batchSubscribe(ElectrumBatchRequestDetails params) { + try { + final subscriptions = _registerBatchSubscription(params); + add(params.toTCPParams()); + + return subscriptions; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + List> _registerBatchTask(ElectrumBatchRequestDetails mainParams) { + return mainParams.params.map((params) { + final completer = AsyncRequestCompleter(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + completer: completer.completer, + request: mainParams, + isSubscription: false, + ); + + return completer; + }).toList(); + } + + @override + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]) async { + try { + final completers = _registerBatchTask(params); + add(params.toTCPParams()); + final result = await Future.wait(completers.map((e) => e.completer.future)) + .timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + for (final id in params.paramsById.keys) { + _tasks.remove(id); + } + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} diff --git a/lib/src/provider/service/electrum/electrum_version.dart b/lib/src/provider/service/electrum/electrum_version.dart new file mode 100644 index 0000000..ef485c7 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_version.dart @@ -0,0 +1,49 @@ +import 'package:bitcoin_base/src/exception/exception.dart'; + +class ElectrumVersion { + final int major; + final int minor; + final int patch; + + const ElectrumVersion(this.major, this.minor, this.patch); + + factory ElectrumVersion.fromStr(String version) { + final parts = version.split('.'); + if (parts.length != 3) { + throw BitcoinBasePluginException('Invalid version string: $version'); + } + + return ElectrumVersion(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); + } + + int compareTo(ElectrumVersion other) { + if (major > other.major) { + return 1; + } else if (major < other.major) { + return -1; + } + + if (major == other.major) { + if (minor > other.minor) { + return 1; + } else if (minor < other.minor) { + return -1; + } + } + + if (minor == other.minor) { + if (patch > other.patch) { + return 1; + } else if (patch < other.patch) { + return -1; + } + } + + return 0; + } + + @override + String toString() { + return '$major.$minor.$patch'; + } +} diff --git a/lib/src/provider/service/electrum/electrum_websocket_service.dart b/lib/src/provider/service/electrum/electrum_websocket_service.dart new file mode 100644 index 0000000..1a460a6 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_websocket_service.dart @@ -0,0 +1,85 @@ +// import 'dart:async'; +// import 'dart:convert'; +// import 'package:bitcoin_base/bitcoin_base.dart'; +// import 'package:example/services_examples/cross_platform_websocket/core.dart'; + +// class ElectrumWebSocketService implements BitcoinBaseElectrumRPCService { +// ElectrumWebSocketService._( +// this.url, +// WebSocketCore channel, { +// this.defaultRequestTimeOut = const Duration(seconds: 30), +// }) : _socket = channel { +// _subscription = +// channel.stream.cast().listen(_onMessage, onError: _onClose, onDone: _onDone); +// } +// WebSocketCore? _socket; +// StreamSubscription? _subscription; +// final Duration defaultRequestTimeOut; + +// Map requests = {}; +// bool _isDisconnected = false; + +// bool get isConnected => !_isDisconnected; + +// @override +// final String url; + +// void add(List params) { +// if (_isDisconnected) { +// throw StateError("socket has been disconnected"); +// } +// _socket?.sink(params); +// } + +// void _onClose(Object? error) { +// _isDisconnected = true; + +// _socket?.close(); +// _socket = null; +// _subscription?.cancel().catchError((e) {}); +// _subscription = null; +// } + +// void _onDone() { +// _onClose(null); +// } + +// @override +// void disconnect() { +// _onClose(null); +// } + +// static Future connect( +// String url, { +// Iterable? protocols, +// Duration defaultRequestTimeOut = const Duration(seconds: 30), +// final Duration connectionTimeOut = const Duration(seconds: 30), +// }) async { +// final channel = await WebSocketCore.connect(url, protocols: protocols?.toList()); + +// return ElectrumWebSocketService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); +// } + +// void _onMessage(String event) { +// final Map decode = json.decode(event); +// if (decode.containsKey("id")) { +// final int id = int.parse(decode["id"]!.toString()); +// final request = requests.remove(id); +// request?.completer.complete(decode); +// } +// } + +// @override +// Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { +// final AsyncRequestCompleter compeleter = AsyncRequestCompleter(params.params); + +// try { +// requests[params.id] = compeleter; +// add(params.toWebSocketParams()); +// final result = await compeleter.completer.future.timeout(timeout ?? defaultRequestTimeOut); +// return result; +// } finally { +// requests.remove(params.id); +// } +// } +// } diff --git a/lib/src/provider/service/electrum/methods.dart b/lib/src/provider/service/electrum/methods.dart index 66984e5..bd4121a 100644 --- a/lib/src/provider/service/electrum/methods.dart +++ b/lib/src/provider/service/electrum/methods.dart @@ -15,20 +15,21 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods serverAddPeer = ElectrumRequestMethods._("server.add_peer"); /// Subscribe to a script hash. + static const String scripthashesSubscribeMethod = "blockchain.scripthash.subscribe"; static const ElectrumRequestMethods scriptHashSubscribe = - ElectrumRequestMethods._("blockchain.scripthash.subscribe"); + ElectrumRequestMethods._(scripthashesSubscribeMethod); /// Unsubscribe from a script hash, preventing future notifications if its status changes. static const ElectrumRequestMethods scriptHashUnSubscribe = ElectrumRequestMethods._("blockchain.scripthash.unsubscribe"); /// Return an ordered list of UTXOs sent to a script hash. - static const ElectrumRequestMethods listunspent = - ElectrumRequestMethods._("blockchain.scripthash.listunspent"); + static const String listunspentMethod = "blockchain.scripthash.listunspent"; + static const ElectrumRequestMethods listunspent = ElectrumRequestMethods._(listunspentMethod); /// Return the confirmed and unconfirmed balances of a script hash. - static const ElectrumRequestMethods getBalance = - ElectrumRequestMethods._("blockchain.scripthash.get_balance"); + static const String getBalanceMethod = "blockchain.scripthash.get_balance"; + static const ElectrumRequestMethods getBalance = ElectrumRequestMethods._(getBalanceMethod); /// Return a raw transaction. static const ElectrumRequestMethods getTransaction = @@ -55,20 +56,20 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("blockchain.block.headers"); /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. - static const ElectrumRequestMethods estimateFee = - ElectrumRequestMethods._("blockchain.estimatefee"); + static const String estimateFeeMethod = "blockchain.estimatefee"; + static const ElectrumRequestMethods estimateFee = ElectrumRequestMethods._(estimateFeeMethod); /// Return the confirmed and unconfirmed history of a script hash. - static const ElectrumRequestMethods getHistory = - ElectrumRequestMethods._("blockchain.scripthash.get_history"); + static const String getHistoryMethod = "blockchain.scripthash.get_history"; + static const ElectrumRequestMethods getHistory = ElectrumRequestMethods._(getHistoryMethod); /// Return the unconfirmed transactions of a script hash. static const ElectrumRequestMethods getMempool = ElectrumRequestMethods._("blockchain.scripthash.get_mempool"); /// Broadcast a transaction to the network. - static const ElectrumRequestMethods broadCast = - ElectrumRequestMethods._("blockchain.transaction.broadcast"); + static const String broadcastMethod = "blockchain.transaction.broadcast"; + static const ElectrumRequestMethods broadcast = ElectrumRequestMethods._(broadcastMethod); /// Return a banner to be shown in the Electrum console. static const ElectrumRequestMethods serverBanner = ElectrumRequestMethods._("server.banner"); @@ -80,11 +81,18 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods ping = ElectrumRequestMethods._("server.ping"); /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. - static const ElectrumRequestMethods version = ElectrumRequestMethods._("server.version"); + static const String versionMethod = "server.version"; + static const ElectrumRequestMethods version = ElectrumRequestMethods._(versionMethod); /// Subscribe to receive block headers when a new block is found. + static const String headersSubscribeMethod = "blockchain.headers.subscribe"; static const ElectrumRequestMethods headersSubscribe = - ElectrumRequestMethods._("blockchain.headers.subscribe"); + ElectrumRequestMethods._(headersSubscribeMethod); + + /// Subscribe to receive block headers when a new block is found. + static const String tweaksSubscribeMethod = "blockchain.tweaks.subscribe"; + static const ElectrumRequestMethods tweaksSubscribe = + ElectrumRequestMethods._(tweaksSubscribeMethod); /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. static const ElectrumRequestMethods relayFee = ElectrumRequestMethods._("blockchain.relayfee"); diff --git a/lib/src/provider/service/electrum/params.dart b/lib/src/provider/service/electrum/params.dart index 2945af7..58f608c 100644 --- a/lib/src/provider/service/electrum/params.dart +++ b/lib/src/provider/service/electrum/params.dart @@ -1,13 +1,26 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// Abstract class representing parameters for Electrum requests. -abstract class ElectrumRequestParams { +abstract class BaseElectrumRequestParams { abstract final String method; - List toJson(); +} + +abstract class ElectrumRequestParams implements BaseElectrumRequestParams { + List toParams(); } /// Represents details of an Electrum request, including id, method, and parameters. -class ElectrumRequestDetails { +abstract class BaseElectrumRequestDetails { + const BaseElectrumRequestDetails({required this.method, required this.params}); + + final String method; + final dynamic params; + + List toTCPParams(); + List toWebSocketParams(); +} + +class ElectrumRequestDetails implements BaseElectrumRequestDetails { const ElectrumRequestDetails({ required this.id, required this.method, @@ -15,38 +28,127 @@ class ElectrumRequestDetails { }); final int id; - + @override final String method; + @override final Map params; + @override List toTCPParams() { final param = "${StringUtils.fromJson(params)}\n"; return StringUtils.encode(param); } + @override List toWebSocketParams() { return StringUtils.encode(StringUtils.fromJson(params)); } } /// Abstract class representing an Electrum request with generic result and response types. -abstract class ElectrumRequest implements ElectrumRequestParams { +abstract class BaseElectrumRequest implements BaseElectrumRequestParams { String? get validate => null; - RESULT onResonse(RESPONSE result) { + BaseElectrumRequestDetails toRequest(int requestId); +} + +abstract class ElectrumRequest extends BaseElectrumRequest + implements ElectrumRequestParams { + RESULT onResponse(RESPONSE result) { return result as RESULT; } + @override ElectrumRequestDetails toRequest(int requestId) { - List inJson = toJson(); - inJson.removeWhere((v) => v == null); - final params = { + final params = toParams(); + params.removeWhere((v) => v == null); + final json = { "jsonrpc": "2.0", "method": method, - "params": inJson, + "params": params, "id": requestId, }; - return ElectrumRequestDetails(id: requestId, params: params, method: method); + return ElectrumRequestDetails(id: requestId, params: json, method: method); + } +} + +abstract class ElectrumBatchRequestParams implements BaseElectrumRequestParams { + List> toParams(); +} + +class ElectrumBatchRequestDetails implements BaseElectrumRequestDetails { + const ElectrumBatchRequestDetails({ + required this.paramsById, + required this.method, + required this.params, + }); + + final Map> paramsById; + + @override + final String method; + + @override + final List> params; + + @override + List toTCPParams() { + final param = "${StringUtils.fromJson(params)}\n"; + return StringUtils.encode(param); + } + + @override + List toWebSocketParams() { + return StringUtils.encode(StringUtils.fromJson(params)); + } +} + +class ElectrumBatchRequestResult { + final ElectrumBatchRequestDetails request; + final RESULT result; + final int id; + + List? get paramForRequest => request.paramsById[id]; + + ElectrumBatchRequestResult({ + required this.request, + required this.id, + required this.result, + }); +} + +abstract class ElectrumBatchRequest extends BaseElectrumRequest + implements ElectrumBatchRequestParams { + ElectrumBatchRequestResult onResponse( + RESPONSE result, + ElectrumBatchRequestDetails request, + ) { + throw UnimplementedError(); + } + + int finalId = 0; + + @override + BaseElectrumRequestDetails toRequest(int requestId) { + List> params = toParams(); + final paramsById = >{}; + + final json = params.map((e) { + final json = { + "jsonrpc": "2.0", + "method": method, + "params": e, + "id": requestId, + }; + paramsById[requestId] = e; + + requestId++; + return json; + }).toList(); + + finalId = requestId; + + return ElectrumBatchRequestDetails(paramsById: paramsById, params: json, method: method); } } diff --git a/lib/src/provider/service/electrum/request_completer.dart b/lib/src/provider/service/electrum/request_completer.dart new file mode 100644 index 0000000..d238dbc --- /dev/null +++ b/lib/src/provider/service/electrum/request_completer.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +class AsyncRequestCompleter { + AsyncRequestCompleter(this.params); + final Completer completer = Completer(); + final Map params; +} + +class AsyncBehaviorSubject { + AsyncBehaviorSubject(this.params); + final BehaviorSubject subscription = BehaviorSubject(); + final Map params; +} diff --git a/lib/src/provider/service/electrum/service.dart b/lib/src/provider/service/electrum/service.dart index 03e4ca2..826a398 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -1,12 +1,65 @@ +import 'dart:convert'; + import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +enum ConnectionStatus { connected, disconnected, connecting, failed } + +class SocketTask { + SocketTask({ + required this.isSubscription, + required this.request, + this.isBatchRequest = false, + this.completer, + this.subject, + }); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; + final bool isBatchRequest; + final BaseElectrumRequestDetails request; +} + +/// Abstract class for providing JSON-RPC service functionality. +abstract class BitcoinBaseElectrumRPCService { + BitcoinBaseElectrumRPCService(); -/// A mixin for providing JSON-RPC service functionality. -mixin BitcoinBaseElectrumRPCService { /// Represents the URL endpoint for JSON-RPC calls. String get url; - /// Makes an HTTP GET request to the Tron network with the specified [params]. + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params); + + List>? batchSubscribe(ElectrumBatchRequestDetails params); + + /// Makes an HTTP GET request with the specified [params]. /// /// The optional [timeout] parameter sets the maximum duration for the request. - Future> call(ElectrumRequestDetails params, [Duration? timeout]); + Future call(ElectrumRequestDetails params, [Duration? timeout]); + + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]); + + bool get isConnected; + void disconnect(); + void reconnect(); + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) { + throw UnimplementedError(); + } +} + +bool isJSONStringCorrect(String source) { + try { + json.decode(source); + return true; + } catch (_) { + return false; + } } diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/provider/transaction_builder/forked_transaction_builder.dart index 3403ffb..6559a39 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/provider/transaction_builder/forked_transaction_builder.dart @@ -1,5 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_base/src/exception/exception.dart'; +import 'package:bitcoin_base/src/provider/transaction_builder/core.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; /// A transaction builder specifically designed for the Bitcoin Cash (BCH) and Bitcoin SV (BSV) networks. @@ -103,8 +103,8 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { const String fakeECDSASignatureBytes = "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; - final transaction = transactionBuilder - .buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { + final transaction = + transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { return fakeECDSASignatureBytes; }); @@ -177,8 +177,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { script: scriptPubKeys, amount: utox.utxo.value, token: utox.utxo.token, - sighash: - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); + sighash: BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); } /// buildP2wshOrP2shScriptSig constructs and returns a script signature (represented as a List of strings) @@ -191,8 +190,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { // /// Returns: /// - List: A List of strings representing the script signature for the P2WSH or P2SH input. - List _buildMiltisigUnlockingScript( - List signedDigest, UtxoWithAddress utx) { + List _buildMiltisigUnlockingScript(List signedDigest, UtxoWithAddress utx) { /// The constructed script signature consists of the signed digest elements followed by /// the script details of the multi-signature address. return ['', ...signedDigest, utx.multiSigAddress.multiSigScript.toHex()]; @@ -249,11 +247,10 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } List inputs = sortedUtxos.map((e) => e.utxo.toInput()).toList(); if (enableRBF && inputs.isNotEmpty) { - inputs[0] = inputs[0] - .copyWith(sequence: BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); + inputs[0] = inputs[0].copyWith(sequence: BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); } - return Tuple(List.unmodifiable(inputs), - List.unmodifiable(sortedUtxos)); + return Tuple( + List.unmodifiable(inputs), List.unmodifiable(sortedUtxos)); } List _buildOutputs() { @@ -263,8 +260,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi .toList(); if (memo != null) { - builtOutputs - .add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); + builtOutputs.add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); } if (outputOrdering == BitcoinOrdering.shuffle) { @@ -275,8 +271,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi (a, b) { final valueComparison = a.amount.compareTo(b.amount); if (valueComparison == 0) { - return BytesUtils.compareBytes( - a.scriptPubKey.toBytes(), b.scriptPubKey.toBytes()); + return BytesUtils.compareBytes(a.scriptPubKey.toBytes(), b.scriptPubKey.toBytes()); } return valueComparison; }, @@ -329,11 +324,7 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumOutputAmounts}) { if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { throw BitcoinBasePluginException('Sum value of utxo not spending', - details: { - "inputAmount": sumUtxoAmount, - "fee": fee, - "outputAmount": sumOutputAmounts - }); + details: {"inputAmount": sumUtxoAmount, "fee": fee, "outputAmount": sumOutputAmounts}); } if (!isFakeTransaction) { /// sum of token amounts @@ -347,19 +338,13 @@ be retrieved by anyone who examines the blockchain's history. amount += outputs .whereType() .where((element) => element.categoryID == i.key) - .fold( - BigInt.zero, - (previousValue, element) => - previousValue + (element.value ?? BigInt.zero)); + .fold(BigInt.zero, + (previousValue, element) => previousValue + (element.value ?? BigInt.zero)); if (amount != i.value) { throw BitcoinBasePluginException( 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', - details: { - "token": i.key, - "inputValue": i.value, - "outputValue": amount - }); + details: {"token": i.key, "inputValue": i.value, "outputValue": amount}); } } } @@ -368,16 +353,11 @@ be retrieved by anyone who examines the blockchain's history. final token = i.utxo.token!; if (token.hasAmount) continue; if (!token.hasNFT) continue; - final hasOneoutput = outputs.whereType().any( - (element) => - element.utxoHash == i.utxo.txHash && - element.token.category == token.category); + final hasOneoutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.token.category == token.category); if (hasOneoutput) continue; - final hasBurnableOutput = outputs - .whereType() - .any((element) => - element.utxoHash == i.utxo.txHash && - element.categoryID == token.category); + final hasBurnableOutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.categoryID == token.category); if (hasBurnableOutput) continue; throw BitcoinBasePluginException( 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', @@ -401,12 +381,8 @@ be retrieved by anyone who examines the blockchain's history. final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -420,8 +396,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } continue; } @@ -460,11 +435,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -474,24 +447,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -503,12 +471,10 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } @@ -521,8 +487,7 @@ be retrieved by anyone who examines the blockchain's history. } @override - Future buildTransactionAsync( - BitcoinSignerCallBackAsync sign) async { + Future buildTransactionAsync(BitcoinSignerCallBackAsync sign) async { /// build inputs final sortedInputs = _buildInputs(); @@ -550,11 +515,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -564,24 +527,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = await sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + await sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -593,18 +551,15 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } /// now we need sign the transaction digest - final sig = - await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); + final sig = await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } diff --git a/pubspec.yaml b/pubspec.yaml index a4d6530..fd44642 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,12 +23,15 @@ dependencies: git: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 + rxdart: ^0.28.0 + intl: ^0.19.0 + http: ^1.1.0 dev_dependencies: - test: ^1.25.5 - lints: ^4.0.0 - flutter_lints: ^4.0.0 + lints: ^5.0.0 + test: ^1.25.9 + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the diff --git a/test/fixtures/silent_payments.json b/test/fixtures/silent_payments.json index a54c584..264f7be 100644 --- a/test/fixtures/silent_payments.json +++ b/test/fixtures/silent_payments.json @@ -31,17 +31,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", - 1.0 + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" ] ] } @@ -89,8 +85,8 @@ ], "outputs": [ { - "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" } ] @@ -130,17 +126,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", - 1.0 + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" ] ] } @@ -188,8 +180,8 @@ ], "outputs": [ { - "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" } ] @@ -229,17 +221,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", - 1.0 + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" ] ] } @@ -287,8 +275,8 @@ ], "outputs": [ { - "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" } ] @@ -328,17 +316,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", - 1.0 + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" ] ] } @@ -386,8 +370,8 @@ ], "outputs": [ { - "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" } ] @@ -395,6 +379,101 @@ } ] }, + { + "comment": "Outpoint ordering byte-lexicographically vs. vout-integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "c8ac0292997b5bca98b3ebd99a57e253071137550f270452cd3df8a3e2266d36", + "pub_key": "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", + "signature": "c036ee38bfe46aba03234339ae7219b31b824b52ef9d5ce05810a0d6f62330dedc2b55652578aa5bdabf930fae941acd839d5a66f8fce7caa9710ccb446bddd1" + } + ] + } + } + ] + }, { "comment": "Single recipient: multiple UTXOs from the same public key", "sending": [ @@ -427,17 +506,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", - 1.0 + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" ] ] } @@ -485,8 +560,8 @@ ], "outputs": [ { - "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" } ] @@ -526,17 +601,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", - 1.0 + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" ] ] } @@ -584,8 +655,8 @@ ], "outputs": [ { - "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" } ] @@ -625,17 +696,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", - 1.0 + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" ] ] } @@ -683,8 +750,8 @@ ], "outputs": [ { - "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" } ] @@ -724,17 +791,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", - 1.0 + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" ] ] } @@ -782,8 +845,8 @@ ], "outputs": [ { - "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" } ] @@ -823,17 +886,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", - 1.0 + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" ] ] } @@ -881,8 +940,8 @@ ], "outputs": [ { - "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" } ] @@ -922,25 +981,15 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 2.0 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 3.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ - [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 2.0 - ], [ "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", - 3.0 + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ] ] } @@ -974,10 +1023,8 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", - "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", - "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", @@ -991,14 +1038,14 @@ ], "outputs": [ { - "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", - "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" - }, - { - "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" } ] } @@ -1037,41 +1084,17 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 2.0 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 3.0 - ], - [ - "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", - 4.0 - ], - [ - "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", - 5.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" ] }, "expected": { "outputs": [ - [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 2.0 - ], - [ - "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", - 3.0 - ], - [ - "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", - 4.0 - ], [ "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", - 5.0 + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ] ] } @@ -1105,66 +1128,9 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", - "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", - "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" - ], - "key_material": { - "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", - "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" - }, - "labels": [] - }, - "expected": { - "addresses": [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" - ], - "outputs": [ - { - "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", - "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" - }, - { - "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", - "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", - "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" - } - ] - } - }, - { - "given": { - "vin": [ - { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - "vout": 0, - "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", - "txinwitness": "", - "prevout": { - "scriptPubKey": { - "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" - } - } - }, - { - "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - "vout": 0, - "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", - "txinwitness": "", - "prevout": { - "scriptPubKey": { - "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" - } - } - } - ], - "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", - "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a" + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], "key_material": { "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", @@ -1178,14 +1144,14 @@ ], "outputs": [ { - "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", - "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", - "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" - }, - { - "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + }, + { + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" } ] } @@ -1193,7 +1159,7 @@ ] }, { - "comment": "Receiving with labels: label with odd parity", + "comment": "Receiving with labels: label with even parity", "sending": [ { "given": { @@ -1224,17 +1190,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq" ] }, "expected": { "outputs": [ [ - "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", - 1.0 + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" ] ] } @@ -1289,8 +1251,8 @@ ], "outputs": [ { - "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" } ] @@ -1330,17 +1292,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n" ] }, "expected": { "outputs": [ [ - "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", - 1.0 + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" ] ] } @@ -1395,8 +1353,8 @@ ], "outputs": [ { - "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" } ] @@ -1405,7 +1363,7 @@ ] }, { - "comment": "Receiving with labels: label with odd parity", + "comment": "Receiving with labels: large label integer", "sending": [ { "given": { @@ -1436,17 +1394,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" ] }, "expected": { "outputs": [ [ - "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", - 1.0 + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" ] ] } @@ -1501,8 +1455,8 @@ ], "outputs": [ { - "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" } ] @@ -1511,7 +1465,7 @@ ] }, { - "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", "sending": [ { "given": { @@ -1542,25 +1496,19 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", - 2.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 1.0 + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], [ - "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", - 2.0 + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" ] ] } @@ -1594,8 +1542,8 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c" + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", @@ -1612,14 +1560,14 @@ ], "outputs": [ { - "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", - "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" - }, - { - "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" } ] } @@ -1658,25 +1606,15 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", - 3.0 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", - 4.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" ] }, "expected": { "outputs": [ - [ - "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", - 3.0 - ], [ "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", - 4.0 + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" ] ] } @@ -1710,8 +1648,8 @@ } ], "outputs": [ - "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", - "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c" + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", @@ -1728,14 +1666,14 @@ ], "outputs": [ { - "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", - "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", - "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" - }, - { - "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" } ] } @@ -1743,7 +1681,7 @@ ] }, { - "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; same recipients", "sending": [ { "given": { @@ -1774,41 +1712,85 @@ } ], "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ] + }, + "expected": { + "outputs": [ [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 5.0 + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" ], [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", - 6.0 + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" ], [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", - 7.0 + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701" ], [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", - 8.0 - ] - ] - }, - "expected": { - "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 5.0 + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" ], [ - "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", - 6.0 + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" ], [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", - 7.0 + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], [ - "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b", - 8.0 + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" ] ] } @@ -1842,34 +1824,46 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", - "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" }, "labels": [ - 1 + 1, + 1337 ] }, "expected": { "addresses": [ "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" ], "outputs": [ { - "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", - "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + "priv_key_tweak": "4e3352fbe0505c25e718d96007c259ef08db34f8c844e4ff742d9855ff03805a", + "pub_key": "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "signature": "6eeae1ea9eb826e3d0e812f65937100e0836ea188c04f36fabc4981eda29de8d3d3529390a0a8b3d830f7bca4f5eae5994b9788ddaf05ad259ffe26d86144b4b" }, { - "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "bf709f98d4418f8a67e738154ae48818dad44689cd37fbc070891a396dd1c633", + "pub_key": "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "signature": "42a19fd8a63dde1824966a95d65a28203e631e49bf96ca5dae1b390e7a0ace2cc8709c9b0c5715047032f57f536a3c80273cbecf4c05be0b5456c183fa122c06" + }, + { + "priv_key_tweak": "736f05e4e3072c3b8656bedef2e9bf54cbcaa2b6fe5320d3e86f5b96874dda71", + "pub_key": "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "signature": "2e61bb3d79418ecf55f68847cf121bfc12d397b39d1da8643246b2f0a9b96c3daa4bfe9651beb5c9ce20e1f29282c4566400a4b45ee6657ec3b18fdc554da0b4" } ] } @@ -1908,25 +1902,15 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ], - [ - "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr", - 2.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" ] }, "expected": { "outputs": [ - [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 1.0 - ], [ "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", - 2.0 + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ] ] } @@ -1960,8 +1944,8 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff" + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], "key_material": { "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", @@ -1978,8 +1962,8 @@ ], "outputs": [ { - "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" } ] @@ -2012,8 +1996,8 @@ } ], "outputs": [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff" + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", @@ -2027,8 +2011,8 @@ ], "outputs": [ { - "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" } ] @@ -2037,7 +2021,7 @@ ] }, { - "comment": "Single receipient: taproot input with NUMS point", + "comment": "Single recipient: taproot input with NUMS point", "sending": [ { "given": { @@ -2080,17 +2064,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", - 1.0 + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" ] ] } @@ -2149,8 +2129,8 @@ ], "outputs": [ { - "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" } ] @@ -2202,17 +2182,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", - 1.0 + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" ] ] } @@ -2271,8 +2247,8 @@ ], "outputs": [ { - "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" } ] @@ -2287,7 +2263,7 @@ "given": { "vin": [ { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", "vout": 0, "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", "txinwitness": "", @@ -2299,7 +2275,7 @@ "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" }, { - "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", "vout": 0, "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", "txinwitness": "", @@ -2324,17 +2300,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", - 1.0 + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" ] ] } @@ -2345,7 +2317,7 @@ "given": { "vin": [ { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", "vout": 0, "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", "txinwitness": "", @@ -2356,7 +2328,7 @@ } }, { - "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", "vout": 0, "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", "txinwitness": "", @@ -2393,8 +2365,8 @@ ], "outputs": [ { - "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" } ] @@ -2446,73 +2418,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] - ] - }, - "expected": { - "outputs": [ - [ - "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", - 1.0 - ] - ] - } - }, - { - "given": { - "vin": [ - { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - "vout": 0, - "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", - "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", - "prevout": { - "scriptPubKey": { - "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" - } - }, - "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" - }, - { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - "vout": 1, - "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", - "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", - "prevout": { - "scriptPubKey": { - "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" - } - }, - "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" - }, - { - "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - "vout": 2, - "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", - "txinwitness": "", - "prevout": { - "scriptPubKey": { - "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" - } - }, - "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" - } - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { "outputs": [ [ - "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", - 1.0 + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" ] ] } @@ -2571,8 +2483,8 @@ ], "outputs": [ { - "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" } ] @@ -2612,17 +2524,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" ] }, "expected": { "outputs": [ [ - "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", - 1.0 + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" ] ] } @@ -2656,8 +2564,8 @@ } ], "outputs": [ - "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", - "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" ], "key_material": { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", @@ -2706,14 +2614,13 @@ } ], "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1.0 - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" ] }, "expected": { - "outputs": [] + "outputs": [ + [] + ] } } ], @@ -2762,5 +2669,92 @@ } } ] + }, + { + "comment": "Input keys sum up to zero / point at infinity: sending fails, receiver skips tx", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + } + ], + "recipients": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ] + }, + "expected": { + "outputs": [ + [] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + } + ], + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "key_material": { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ], + "outputs": [] + } + } + ] } ] diff --git a/test/silent_payments.dart b/test/silent_payments.dart index 1593f9c..8c8d0ba 100644 --- a/test/silent_payments.dart +++ b/test/silent_payments.dart @@ -59,42 +59,51 @@ main() { continue; } - inputPrivKeyInfos.add(ECPrivateInfo( - privkey, - prevoutScript.getAddressType() == SegwitAddresType.p2tr, - tweak: false, - )); + inputPrivKeyInfos.add( + ECPrivateInfo( + privkey, + prevoutScript.getAddressType() == SegwitAddressType.p2tr, + tweak: false, + ), + ); inputPubKeys.add(pubkey); } if (inputPubKeys.isNotEmpty) { - List silentPaymentDestinations = - (given['recipients'] as List) - .map((recipient) => - SilentPaymentDestination.fromAddress(recipient[0], recipient[1].floor())) - .toList(); - - final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); - sendingOutputs = spb.createOutputs(inputPrivKeyInfos, silentPaymentDestinations); + final silentPaymentDestinations = (given['recipients'] as List) + .map((recipient) => SilentPaymentDestination.fromAddress(recipient)) + .toList(); - List expectedDestinations = sendingTest['expected']['outputs']; + try { + final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); + sendingOutputs = spb.createOutputs(inputPrivKeyInfos, silentPaymentDestinations); - for (final destination in silentPaymentDestinations) { - expect(sendingOutputs[destination.toString()] != null, true); - } + List expectedDestinations = sendingTest['expected']['outputs']; - final generatedOutputs = sendingOutputs.values.expand((element) => element).toList(); - for (final expected in expectedDestinations) { - final expectedPubkey = expected[0]; - final generatedPubkey = generatedOutputs.firstWhere((output) => - BytesUtils.toHexString(output.address.pubkey!.toCompressedBytes().sublist(1)) == - expectedPubkey); + for (final destination in silentPaymentDestinations) { + expect(sendingOutputs[destination.toString()] != null, true); + } - expect( + final generatedOutputs = sendingOutputs.values.expand((element) => element).toList(); + for (final expected in expectedDestinations) { + final expectedPubkey = expected[0]; + final generatedPubkey = generatedOutputs.firstWhere( + (output) { + return BytesUtils.toHexString( + output.address.pubkey!.toCompressedBytes().sublist(1), + ) == + expectedPubkey; + }, + ); + + expect( BytesUtils.toHexString( - generatedPubkey.address.pubkey!.toCompressedBytes().sublist(1)), - expectedPubkey); - } + generatedPubkey.address.pubkey!.toCompressedBytes().sublist(1), + ), + expectedPubkey, + ); + } + } catch (_) {} } } @@ -107,16 +116,16 @@ main() { List inputPubKeys = []; final given = receivingTest["given"]; - final expected = receivingTest['expected']; - List outputsToCheck = - (given['outputs'] as List).map((output) => output.toString()).toList(); + final outputsToCheck = + (given['outputs'] as List).map((o) => o.toString()).toList(); final List receivingAddresses = []; final silentPaymentOwner = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(given["key_material"]["scan_priv_key"]), - b_spend: ECPrivate.fromHex(given["key_material"]["spend_priv_key"])); + b_scan: ECPrivate.fromHex(given["key_material"]["scan_priv_key"]), + b_spend: ECPrivate.fromHex(given["key_material"]["spend_priv_key"]), + ); // Add change address receivingAddresses.add(silentPaymentOwner); @@ -131,6 +140,8 @@ main() { BytesUtils.toHexString(generatedLabel); } + final expected = receivingTest['expected']; + for (var address in expected['addresses']) { expect(receivingAddresses.indexWhere((sp) => sp.toString() == address.toString()), isNot(-1)); @@ -143,15 +154,17 @@ main() { outpoint: Outpoint(txid: input['txid'], index: input['vout']), scriptSig: BytesUtils.fromHexString(input['scriptSig']), txinwitness: TxWitnessInput( - stack: [], - scriptWitness: ScriptWitness( - stack: deserStringVector( + stack: [], + scriptWitness: ScriptWitness( + stack: deserStringVector( ByteData.sublistView( Uint8List.fromList( BytesUtils.fromHexString(input['txinwitness']), ), ), - ))), + ), + ), + ), prevOutScript: prevoutScript, ); @@ -167,22 +180,33 @@ main() { } if (inputPubKeys.isNotEmpty) { - final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); + Map addToWallet; + + try { + final spb = SilentPaymentBuilder(pubkeys: inputPubKeys, vinOutpoints: vinOutpoints); - final addToWallet = spb.scanOutputs(silentPaymentOwner.b_scan, silentPaymentOwner.B_spend, + addToWallet = spb.scanOutputs( + silentPaymentOwner.b_scan, + silentPaymentOwner.B_spend, outputsToCheck.map((o) => getScriptFromOutput(o, 0)).toList(), - precomputedLabels: preComputedLabels); + precomputedLabels: preComputedLabels, + ); + } catch (_) { + addToWallet = {}; + } final expectedDestinations = expected['outputs']; // Check that the private key is correct for the found output public key for (int i = 0; i < expectedDestinations.length; i++) { - final output = addToWallet.entries.elementAt(i); - final pubkey = output.key; final expectedPubkey = expectedDestinations[i]["pub_key"]; - expect(pubkey, expectedPubkey); + final output = addToWallet[expectedPubkey]; + + if (output == null) { + continue; + } - final privKeyTweak = output.value.tweak; + final privKeyTweak = output!.tweak; final expectedPrivKeyTweak = expectedDestinations[i]["priv_key_tweak"]; expect(privKeyTweak, expectedPrivKeyTweak); @@ -195,16 +219,18 @@ main() { // Sign the message with schnorr final btcSigner = BitcoinSigner.fromKeyBytes(fullPrivateKey.toBytes()); - List sig = - btcSigner.signSchnorrTransaction(msg, tapScripts: [], tweak: false, aux: aux); + List sig = btcSigner.signSchnorrTransaction( + msg, + tapScripts: [], + tweak: false, + auxRand: aux, + ); // Verify the message is correct expect(btcSigner.verifyKey.verifySchnorr(msg, sig, isTweak: false), true); // Verify the signature is correct expect(BytesUtils.toHexString(sig), expectedDestinations[i]["signature"]); - - i++; } } } From 545bcade1e8c7691553bbd811f51bc54c333b19f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 3 Jul 2025 01:04:26 +0300 Subject: [PATCH 2/2] fix sp tests --- example/pubspec.lock | 117 +++++++++++++++++++++++++++++++------- example/pubspec.yaml | 2 +- test/silent_payments.dart | 12 ++-- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 318e7a4..77038f1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bitcoin_base: dependency: "direct main" description: @@ -33,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -53,10 +69,26 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -91,6 +123,14 @@ packages: description: flutter source: sdk version: "0.0.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" http: dependency: "direct main" description: @@ -107,30 +147,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -151,18 +207,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" path: dependency: transitive description: @@ -171,11 +227,27 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -188,10 +260,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -204,10 +276,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -220,10 +292,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.3" typed_data: dependency: transitive description: @@ -244,10 +316,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.3.0" web: dependency: transitive description: @@ -257,4 +329,5 @@ packages: source: hosted version: "0.5.1" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4c53070..25d16d4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v4 + ref: cake-update-v2 http: ^1.2.0 dev_dependencies: diff --git a/test/silent_payments.dart b/test/silent_payments.dart index 8c8d0ba..cb0d6f9 100644 --- a/test/silent_payments.dart +++ b/test/silent_payments.dart @@ -3,10 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; -import 'package:bitcoin_base/src/bitcoin/address/address.dart'; -import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; -import 'package:bitcoin_base/src/crypto/crypto.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/crypto/crypto/cdsa/point/base.dart'; import 'package:test/test.dart'; @@ -62,7 +59,7 @@ main() { inputPrivKeyInfos.add( ECPrivateInfo( privkey, - prevoutScript.getAddressType() == SegwitAddressType.p2tr, + prevoutScript.getAddressType() == SegwitAddresType.p2tr, tweak: false, ), ); @@ -71,7 +68,7 @@ main() { if (inputPubKeys.isNotEmpty) { final silentPaymentDestinations = (given['recipients'] as List) - .map((recipient) => SilentPaymentDestination.fromAddress(recipient)) + .map((recipient) => SilentPaymentDestination.fromAddress(recipient, 5)) .toList(); try { @@ -125,6 +122,7 @@ main() { final silentPaymentOwner = SilentPaymentOwner.fromPrivateKeys( b_scan: ECPrivate.fromHex(given["key_material"]["scan_priv_key"]), b_spend: ECPrivate.fromHex(given["key_material"]["spend_priv_key"]), + network: BitcoinNetwork.mainnet, ); // Add change address @@ -223,7 +221,7 @@ main() { msg, tapScripts: [], tweak: false, - auxRand: aux, + aux: aux, ); // Verify the message is correct