From bedd81e8f5e457465a8581e112489861bac3bc43 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:10:01 -0300 Subject: [PATCH 1/7] feat: worker changes --- .../global/old_examples/bitcoin_example.dart | 100 +++---- .../spending_single_type.dart | 109 +++----- .../multi_sig_transactions.dart | 39 +-- .../transaction_builder_example.dart | 34 +-- .../electrum/electrum_ssl_service.dart | 107 +++++--- .../electrum/electrum_tcp_service.dart | 97 +++++-- .../electrum/electrum_websocket_service.dart | 39 ++- .../electrum/request_completer.dart | 7 - example/pubspec.lock | 90 +++++- example/pubspec.yaml | 4 +- lib/bitcoin_base.dart | 4 + lib/src/bitcoin/address/address.dart | 2 +- lib/src/bitcoin/address/core.dart | 5 +- lib/src/bitcoin/address/derivations.dart | 216 +++++++++++++++ lib/src/bitcoin/address/legacy_address.dart | 99 ++++--- lib/src/bitcoin/address/network_address.dart | 16 +- lib/src/bitcoin/address/segwit_address.dart | 87 ++++-- lib/src/bitcoin/address/util.dart | 71 +++++ lib/src/bitcoin/amount/amount.dart | 5 + lib/src/bitcoin/amount/utils.dart | 33 +++ lib/src/bitcoin/script/transaction.dart | 21 +- lib/src/bitcoin/silent_payments/address.dart | 70 +++-- lib/src/crypto/keypair/ec_private.dart | 14 +- lib/src/crypto/keypair/ec_public.dart | 4 + .../provider/api_provider/api_provider.dart | 13 +- .../api_provider/electrum_api_provider.dart | 93 +++++-- .../provider/electrum_methods/methods.dart | 1 + .../electrum_methods/methods/add_peer.dart | 2 +- .../methods/block_headers.dart | 8 +- .../electrum_methods/methods/broad_cast.dart | 4 +- .../methods/donate_address.dart | 2 +- .../methods/electrum_version.dart | 2 +- .../methods/estimate_fee.dart | 2 +- .../electrum_methods/methods/get_balance.dart | 2 +- .../methods/get_fee_histogram.dart | 5 +- .../electrum_methods/methods/get_history.dart | 2 +- .../electrum_methods/methods/get_mempool.dart | 2 +- .../electrum_methods/methods/get_merkle.dart | 5 +- .../methods/get_transaction.dart | 36 ++- .../electrum_methods/methods/get_unspet.dart | 11 +- .../methods/get_value_proof.dart | 2 +- .../electrum_methods/methods/header.dart | 2 +- .../methods/headers_subscribe.dart | 21 +- .../electrum_methods/methods/id_from_pos.dart | 2 +- .../masternode_announce_broadcast.dart | 2 +- .../methods/masternode_list.dart | 5 +- .../methods/masternode_subscribe.dart | 2 +- .../electrum_methods/methods/ping.dart | 2 +- .../electrum_methods/methods/protx_diff.dart | 2 +- .../electrum_methods/methods/protx_info.dart | 2 +- .../electrum_methods/methods/relay_fee.dart | 2 +- .../methods/scripthash_unsubscribe.dart | 2 +- .../methods/server_banner.dart | 2 +- .../methods/server_features.dart | 2 +- .../methods/server_peer_subscribe.dart | 5 +- .../electrum_methods/methods/status.dart | 7 +- .../methods/tweaks_subscribe.dart | 98 +++++++ lib/src/provider/models/config.dart | 28 +- .../models/electrum/electrum_utxo.dart | 26 +- .../provider/models/fee_rate/fee_rate.dart | 79 ++++-- .../provider/service/electrum/electrum.dart | 4 + .../electrum/electrum_ssl_service.dart | 258 ++++++++++++++++++ .../electrum/electrum_tcp_service.dart | 258 ++++++++++++++++++ .../electrum/electrum_websocket_service.dart | 85 ++++++ .../provider/service/electrum/methods.dart | 27 +- lib/src/provider/service/electrum/params.dart | 2 +- .../service/electrum/request_completer.dart | 14 + .../provider/service/electrum/service.dart | 46 +++- .../provider/service/http/http_service.dart | 70 +++++ pubspec.yaml | 7 +- test/encode_decode_transaction_test.dart | 15 +- 71 files changed, 1995 insertions(+), 547 deletions(-) delete mode 100644 example/lib/services_examples/electrum/request_completer.dart create mode 100644 lib/src/bitcoin/address/derivations.dart create mode 100644 lib/src/bitcoin/amount/amount.dart create mode 100644 lib/src/bitcoin/amount/utils.dart create mode 100644 lib/src/provider/electrum_methods/methods/tweaks_subscribe.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_websocket_service.dart create mode 100644 lib/src/provider/service/electrum/request_completer.dart diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index 3ce647d..a24aaf2 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -2,7 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; /// Calculates the change value based on the sum of all provided values. /// @@ -19,8 +18,7 @@ import 'package:example/services_examples/explorer_service/explorer_service.dart /// Returns: /// - The change value. BigInt _changeValue(BigInt sum, List all) { - final sumAll = all.fold( - BigInt.zero, (previousValue, element) => previousValue + element); + final sumAll = all.fold(BigInt.zero, (previousValue, element) => previousValue + element); final remind = sum - sumAll; if (remind < BigInt.zero) { @@ -61,27 +59,25 @@ void _spendFromP2pkhTo10DifferentType() async { final examplePublicKey2 = examplePrivateKey.getPublic(); /// Define transaction outputs - final out1 = P2pkhAddress.fromAddress( - address: "msxiCJXD2WB43wK2PpTUvoqQLF7ZP98qqM", network: network); + final out1 = + P2pkhAddress.fromAddress(address: "msxiCJXD2WB43wK2PpTUvoqQLF7ZP98qqM", network: network); final out2 = P2trAddress.fromAddress( - address: "tb1plq65drqavf93wf63d8g7d8ypuzaargd5h9d35u05ktrcwxq4a6ss0gpvrt", - network: network); + address: "tb1plq65drqavf93wf63d8g7d8ypuzaargd5h9d35u05ktrcwxq4a6ss0gpvrt", network: network); final out3 = P2wpkhAddress.fromAddress( address: "tb1q3zqgu9j368wgk8u5f9vtmkdwq8geetdxry690d", network: network); - final out4 = P2pkAddress.fromPubkey(pubkey: examplePublicKey.publicKey.toHex()); - final out5 = P2shAddress.fromAddress( - address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); - final out6 = P2shAddress.fromAddress( - address: "2NDAUpeUB1kGAQET8SojF8seXNrk3uudtCb", network: network); - final out7 = P2shAddress.fromAddress( - address: "2NE9CYdxju2iEAfR4FMdKPUcZbnKcfCiLhM", network: network); - final out8 = P2shAddress.fromAddress( - address: "2MwGRf8wNJsaYKdigqPwikPpg9JAT2faaPB", network: network); + final out4 = P2pkAddress.fromPubkey(pubkey: ECPublic.fromBip32(examplePublicKey.publicKey)); + final out5 = + P2shAddress.fromAddress(address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); + final out6 = + P2shAddress.fromAddress(address: "2NDAUpeUB1kGAQET8SojF8seXNrk3uudtCb", network: network); + final out7 = + P2shAddress.fromAddress(address: "2NE9CYdxju2iEAfR4FMdKPUcZbnKcfCiLhM", network: network); + final out8 = + P2shAddress.fromAddress(address: "2MwGRf8wNJsaYKdigqPwikPpg9JAT2faaPB", network: network); final out9 = P2wshAddress.fromAddress( - address: "tb1qes3upam2nv3rc6s38tqgk0cqh6dlycvk6cjydyvpx9zlumh4h4lsjq26p8", - network: network); - final out10 = P2shAddress.fromAddress( - address: "2N2aRKjTQ3uzgUSLWFQAUDvKLnKCiBfCSAh", network: network); + address: "tb1qes3upam2nv3rc6s38tqgk0cqh6dlycvk6cjydyvpx9zlumh4h4lsjq26p8", network: network); + final out10 = + P2shAddress.fromAddress(address: "2N2aRKjTQ3uzgUSLWFQAUDvKLnKCiBfCSAh", network: network); /// Calculate the change value for the transaction final change = _changeValue( @@ -130,8 +126,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// Create a UTXO using a BitcoinUtxo with specific details utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction - txHash: - "b06f4ed0b49a5092a9ea206553ddc5fc469be694d0d28c95598c653e66cdeb5e", + txHash: "b06f4ed0b49a5092a9ea206553ddc5fc469be694d0d28c95598c653e66cdeb5e", /// Value represents the amount of the UTXO in satoshis. value: BigInt.from(250000), @@ -145,19 +140,16 @@ void _spendFromP2pkhTo10DifferentType() async { /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toP2pkhAddress())), + publicKey: examplePublicKey2.toHex(), address: examplePublicKey2.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "6ff0bdb2966f62f5e202c924e1cab1368b0258833e48986cc0a70fbca624ba93", + txHash: "6ff0bdb2966f62f5e202c924e1cab1368b0258833e48986cc0a70fbca624ba93", value: BigInt.from(812830), vout: 0, scriptType: examplePublicKey2.toP2pkhAddress().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toP2pkhAddress())), + publicKey: examplePublicKey2.toHex(), address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder @@ -249,8 +241,8 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// outputs /// make sure pass network to address for validate, before sending create transaction - final out1 = P2pkhAddress.fromAddress( - address: "n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR", network: network); + final out1 = + P2pkhAddress.fromAddress(address: "n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR", network: network); final builder = BitcoinTransactionBuilder( @@ -272,8 +264,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { UtxoWithAddress( utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", /// Value represents the amount of the UTXO in satoshis. value: BtcUtils.toSatoshi("0.001"), @@ -291,8 +282,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 1, scriptType: childKey1PublicKey.toTaprootAddress().type, @@ -302,8 +292,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toTaprootAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 2, scriptType: childKey1PublicKey.toP2wpkhAddress().type, @@ -313,53 +302,44 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toP2wpkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 3, scriptType: examplePublicKey.toP2pkAddress().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkAddress())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 4, scriptType: examplePublicKey.toP2pkInP2sh().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 5, scriptType: examplePublicKey.toP2pkhInP2sh().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkhInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkhInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 6, scriptType: examplePublicKey.toP2wpkhInP2sh().type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2wpkhInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2wpkhInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 7, scriptType: msig.toP2shAddress().type, @@ -371,28 +351,24 @@ void _spendFrom10DifferentTypeToP2pkh() async { multiSigAddress: msig, address: msig.toP2shAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 8, scriptType: msig.toP2wshAddress(network: network).type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails.multiSigAddress( - multiSigAddress: msig, - address: msig.toP2wshAddress(network: network))), + multiSigAddress: msig, address: msig.toP2wshAddress(network: network))), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.0015783"), vout: 9, scriptType: msig2.toP2wshInP2shAddress(network: network).type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails.multiSigAddress( - multiSigAddress: msig2, - address: msig2.toP2wshInP2shAddress(network: network))), + multiSigAddress: msig2, address: msig2.toP2wshInP2shAddress(network: network))), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder diff --git a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart index 4f16538..12b0f72 100644 --- a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart +++ b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart @@ -1,16 +1,14 @@ // ignore_for_file: unused_local_variable import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; import 'spending_builders.dart'; // Define the network as the Testnet (used for testing and development purposes). const network = BitcoinNetwork.testnet; -final service = BitcoinApiService(); // Initialize an API provider for interacting with the Testnet's blockchain data. -final api = ApiProvider.fromMempool(network, service); +final api = ApiProvider.fromMempool(network); // In these tutorials, you will learn how to spend various types of UTXOs. // Each method is specific to a type of UTXO. @@ -28,16 +26,15 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // P2WPKH final sender = publicKey.toP2wpkhAddress(); // Read UTXOs of accounts from the BlockCypher API. - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: publicKey.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: publicKey.toHex())); // The total amount of UTXOs that we can spend. final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } // Receive network fees - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); // feeRate.medium, feeRate.high ,feeRate.low P/KB // In this section, we select the transaction outputs; the number and type of addresses are not important @@ -64,8 +61,7 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // Now that we've determined the transaction size, let's calculate the transaction fee // based on the transaction size and the desired fee rate. - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); // We subtract the fee from the total amount of UTXOs to calculate // the actual amount we can spend in this transaction. @@ -74,9 +70,8 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // We specify the desired amount for each address. Here, I have divided the desired total // amount by the number of outputs to ensure an equal amount for each. final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); // I use the 'buildP2wpkTransaction' method to create a transaction. @@ -112,15 +107,14 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2WSH ADDRESS final sender = addr.toP2wshAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -133,13 +127,11 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2WSHTransaction( receiver: outPutWithValue, @@ -161,15 +153,14 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2PKH final sender = addr.toP2pkhAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -181,13 +172,11 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2pkhTransaction( @@ -205,23 +194,21 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { // Spend P2SH(P2PKH) or P2SH(P2PK): Please note that all input addresses must be of P2SH(P2PKH) or P2SH(P2PK) type; otherwise, the transaction will fail. // This method is for standard 1-1 Multisig P2SH. // For standard n-of-m multi-signature scripts, please refer to the 'multi_sig_transactions.dart' tutorial. -Future spendingP2SHNoneSegwit( - ECPrivate sWallet, ECPrivate rWallet) async { +Future spendingP2SHNoneSegwit(ECPrivate sWallet, ECPrivate rWallet) async { // All the steps are the same as in the first tutorial; // the only difference is the transaction input type, // and we use method `buildP2shNoneSegwitTransaction` to create the transaction. final addr = sWallet.getPublic(); // P2SH(P2PK) final sender = addr.toP2pkInP2sh(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -233,13 +220,11 @@ Future spendingP2SHNoneSegwit( ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2shNoneSegwitTransaction( receiver: outPutWithValue, @@ -263,15 +248,14 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2SH(P2PWKH) final sender = addr.toP2wpkhInP2sh(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -284,13 +268,11 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); // return; @@ -315,15 +297,14 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2TR address final sender = addr.toTaprootAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -335,13 +316,11 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2trTransaction( diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart index 794deb6..98d0e9c 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart @@ -4,7 +4,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; void main() async { final service = BitcoinApiService(); @@ -13,7 +12,7 @@ void main() async { // select api for read accounts UTXOs and send transaction // Mempool or BlockCypher - final api = ApiProvider.fromMempool(network, service); + final api = ApiProvider.fromMempool(network); final mnemonic = Bip39SeedGenerator(Mnemonic.fromString( "spy often critic spawn produce volcano depart fire theory fog turn retire")) @@ -59,18 +58,14 @@ void main() async { // P2WSH Multisig 4-6 // tb1qxt3c7849m0m6cv3z3s35c3zvdna3my3yz0r609qd9g0dcyyk580sgyldhe - final p2wshMultiSigAddress = - multiSignatureAddress.toP2wshAddress(network: network).toP2pkhAddress(network); + final p2wshMultiSigAddress = multiSignatureAddress.toP2wshAddress(network: network); // p2sh(p2wsh) multisig - final signerP2sh1 = - MultiSignatureSigner(publicKey: public5.toHex(), weight: 1); + final signerP2sh1 = MultiSignatureSigner(publicKey: public5.toHex(), weight: 1); - final signerP2sh2 = - MultiSignatureSigner(publicKey: public6.toHex(), weight: 1); + final signerP2sh2 = MultiSignatureSigner(publicKey: public6.toHex(), weight: 1); - final signerP2sh3 = - MultiSignatureSigner(publicKey: public1.toHex(), weight: 1); + final signerP2sh3 = MultiSignatureSigner(publicKey: public1.toHex(), weight: 1); final MultiSignatureAddress p2shMultiSignature = MultiSignatureAddress( threshold: 2, @@ -78,9 +73,7 @@ void main() async { ); // P2SH(P2WSH) miltisig 2-3 // 2N8co8bth9CNKtnWGfHW6HuUNgnNPNdpsMj - final p2shMultisigAddress = p2shMultiSignature - .toP2wshInP2shAddress(network: network) - .toP2pkhAddress(network); + final p2shMultisigAddress = p2shMultiSignature.toP2wshInP2shAddress(network: network); // P2TR final exampleAddr2 = public2.toTaprootAddress(); @@ -149,11 +142,9 @@ void main() async { utxos: utxos, outputs: [ BitcoinOutput( - address: p2shMultiSignature.toP2wshInP2shAddress(network: network), - value: BigInt.zero), + address: p2shMultiSignature.toP2wshInP2shAddress(network: network), value: BigInt.zero), BitcoinOutput( - address: multiSignatureAddress.toP2wshAddress(network: network), - value: BigInt.zero), + address: multiSignatureAddress.toP2wshAddress(network: network), value: BigInt.zero), BitcoinOutput(address: exampleAddr2, value: BigInt.zero), BitcoinOutput(address: exampleAddr4, value: BigInt.zero) ], @@ -168,7 +159,7 @@ void main() async { // That's my perspective, of course. final blockCypher = ApiProvider.fromBlocCypher(network, service); - final feeRate = await blockCypher.getNetworkFeeRate(); + final feeRate = await blockCypher.getRecommendedFeeRate(); // fee rate inKB // feeRate.medium: 32279 P/KB // feeRate.high: 43009 P/KB @@ -190,12 +181,9 @@ void main() async { address: p2shMultiSignature.toP2wshInP2shAddress(network: network), value: BigInt.from(365449)); final output2 = BitcoinOutput( - address: multiSignatureAddress.toP2wshAddress(network: network), - value: BigInt.from(365449)); - final output3 = - BitcoinOutput(address: exampleAddr2, value: BigInt.from(365448)); - final output4 = - BitcoinOutput(address: exampleAddr4, value: BigInt.from(365448)); + address: multiSignatureAddress.toP2wshAddress(network: network), value: BigInt.from(365449)); + final output3 = BitcoinOutput(address: exampleAddr2, value: BigInt.from(365448)); + final output4 = BitcoinOutput(address: exampleAddr4, value: BigInt.from(365448)); // Well, now it is clear to whom we are going to pay the amount // Now let's create the transaction @@ -233,8 +221,7 @@ void main() async { // I've added a method for signing the transaction as a parameter. // This method sends you the public key for each UTXO, // allowing you to sign the desired input with the associated private key - final transaction = - transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { + final transaction = transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { late ECPrivate key; // ok we have the public key of the current UTXO and we use some conditions to find private key and sign transaction diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart index 851259f..701f72b 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart @@ -1,6 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; // spend from 8 different address type to 10 different output void main() async { @@ -116,26 +115,16 @@ void main() async { // now we have 1,174,140 satoshi for spending let do it // we create 10 different output with different address type like (pt2r, p2sh(p2wpkh), p2sh(p2wsh), p2pkh, etc.) // We consider the spendable amount for 10 outputs and divide by 10, each output 117,414 - final output1 = - BitcoinOutput(address: exampleAddr4, value: BigInt.from(117414)); - final output2 = - BitcoinOutput(address: exampleAddr9, value: BigInt.from(117414)); - final output3 = - BitcoinOutput(address: exampleAddr10, value: BigInt.from(117414)); - final output4 = - BitcoinOutput(address: exampleAddr1, value: BigInt.from(117414)); - final output5 = - BitcoinOutput(address: exampleAddr3, value: BigInt.from(117414)); - final output6 = - BitcoinOutput(address: exampleAddr2, value: BigInt.from(117414)); - final output7 = - BitcoinOutput(address: exampleAddr7, value: BigInt.from(117414)); - final output8 = - BitcoinOutput(address: exampleAddr8, value: BigInt.from(117414)); - final output9 = - BitcoinOutput(address: exampleAddr5, value: BigInt.from(117414)); - final output10 = - BitcoinOutput(address: exampleAddr6, value: BigInt.from(117414)); + final output1 = BitcoinOutput(address: exampleAddr4, value: BigInt.from(117414)); + final output2 = BitcoinOutput(address: exampleAddr9, value: BigInt.from(117414)); + final output3 = BitcoinOutput(address: exampleAddr10, value: BigInt.from(117414)); + final output4 = BitcoinOutput(address: exampleAddr1, value: BigInt.from(117414)); + final output5 = BitcoinOutput(address: exampleAddr3, value: BigInt.from(117414)); + final output6 = BitcoinOutput(address: exampleAddr2, value: BigInt.from(117414)); + final output7 = BitcoinOutput(address: exampleAddr7, value: BigInt.from(117414)); + final output8 = BitcoinOutput(address: exampleAddr8, value: BigInt.from(117414)); + final output9 = BitcoinOutput(address: exampleAddr5, value: BigInt.from(117414)); + final output10 = BitcoinOutput(address: exampleAddr6, value: BigInt.from(117414)); // Well, now it is clear to whom we are going to pay the amount // Now let's create the transaction @@ -183,8 +172,7 @@ void main() async { // parameters // utxo infos with owner details // trDigest transaction digest of current UTXO (must be sign with correct privateKey) - final transaction = - transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { + final transaction = transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { late ECPrivate key; // ok we have the public key of the current UTXO and we use some conditions to find private key and sign transaction diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart index 362c306..1313aa7 100644 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ b/example/lib/services_examples/electrum/electrum_ssl_service.dart @@ -1,41 +1,49 @@ -/// Simple example how to send request to electurm with secure socket +/// 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'; +import 'package:rxdart/rxdart.dart'; -class ElectrumSSLService with BitcoinBaseElectrumRPCService { - ElectrumSSLService._( +class SocketTask { + SocketTask({required this.isSubscription, this.completer, this.subject}); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; +} + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { + ElectrumTCPService._( this.url, SecureSocket channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = _socket!.listen(_onMessage, onError: _onClose, onDone: _onDone); } SecureSocket? _socket; StreamSubscription>? _subscription; final Duration defaultRequestTimeOut; - Map requests = {}; - bool _isDiscounnect = false; + final Map _tasks = {}; - bool get isConnected => _isDiscounnect; + bool _isDisconnected = false; + @override + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.add(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; _socket = null; _subscription?.cancel().catchError((e) {}); @@ -46,50 +54,83 @@ class ElectrumSSLService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } - static Future connect( + 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); + final channel = + await SecureSocket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); + + return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(List event) { + void _onMessage(List event) { final Map decode = json.decode(utf8.decode(event)); if (decode.containsKey("id")) { + _finish(decode["id"]!.toString(), decode); final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); + final request = _tasks.remove(id); + request?.completer?.complete(decode); } } + void _finish(String id, Map decode) { + final int id = int.parse(decode["id"]!.toString()); + if (_tasks[id] == null) { + return; + } + + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer!.complete(decode); + } + + final isSubscription = _tasks[id]?.isSubscription ?? false; + if (!isSubscription) { + _tasks.remove(id); + } else { + _tasks[id]?.subject?.add(decode); + } + } + + void _registerSubscription(int id, BehaviorSubject subject) => + _tasks[id] = SocketTask(subject: subject, isSubscription: true); + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + + try { + _registerSubscription(params.id, subscription.subscription); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + void _registerTask(int id, Completer completer) => + _tasks[id] = SocketTask(completer: completer, isSubscription: false); + @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { + final completer = AsyncRequestCompleter(params.params); try { - requests[params.id] = compeleter; + _registerTask(params.id, completer.completer); add(params.toTCPParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); return result; } finally { - requests.remove(params.id); + _tasks.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 index 750aad0..efc2506 100644 --- a/example/lib/services_examples/electrum/electrum_tcp_service.dart +++ b/example/lib/services_examples/electrum/electrum_tcp_service.dart @@ -4,38 +4,46 @@ 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'; +import 'package:rxdart/rxdart.dart'; -class ElectrumTCPService with BitcoinBaseElectrumRPCService { +class SocketTask { + SocketTask({required this.isSubscription, this.completer, this.subject}); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; +} + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { ElectrumTCPService._( this.url, Socket channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = _socket!.listen(_onMessage, onError: _onClose, onDone: _onDone); } Socket? _socket; StreamSubscription>? _subscription; final Duration defaultRequestTimeOut; - Map requests = {}; - bool _isDiscounnect = false; + final Map _tasks = {}; - bool get isConnected => _isDiscounnect; + bool _isDisconnected = false; + @override + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.add(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; _socket = null; _subscription?.cancel().catchError((e) {}); @@ -46,7 +54,8 @@ class ElectrumTCPService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } @@ -57,36 +66,70 @@ class ElectrumTCPService with BitcoinBaseElectrumRPCService { 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); + final channel = await Socket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); - return ElectrumTCPService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); + return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(List event) { + void _onMessage(List event) { final Map decode = json.decode(utf8.decode(event)); if (decode.containsKey("id")) { + _finish(decode["id"]!.toString(), decode); final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); + final request = _tasks.remove(id); + request?.completer?.complete(decode); } } + void _finish(String id, Map decode) { + final int id = int.parse(decode["id"]!.toString()); + if (_tasks[id] == null) { + return; + } + + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer!.complete(decode); + } + + final isSubscription = _tasks[id]?.isSubscription ?? false; + if (!isSubscription) { + _tasks.remove(id); + } else { + _tasks[id]?.subject?.add(decode); + } + } + + void _registerSubscription(int id, BehaviorSubject subject) => + _tasks[id] = SocketTask(subject: subject, isSubscription: true); + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + + try { + _registerSubscription(params.id, subscription.subscription); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + void _registerTask(int id, Completer completer) => + _tasks[id] = SocketTask(completer: completer, isSubscription: false); + @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { + final completer = AsyncRequestCompleter(params.params); try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); + _registerTask(params.id, completer.completer); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); return result; } finally { - requests.remove(params.id); + _tasks.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 index 6ca10a2..2ec9bb7 100644 --- a/example/lib/services_examples/electrum/electrum_websocket_service.dart +++ b/example/lib/services_examples/electrum/electrum_websocket_service.dart @@ -3,40 +3,39 @@ 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 { +class ElectrumWebSocketService implements BitcoinBaseElectrumRPCService { ElectrumWebSocketService._( this.url, WebSocketCore channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = channel.stream - .cast() - .listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = + channel.stream.cast().listen(_onMessage, onError: _onClose, onDone: _onDone); } WebSocketCore? _socket; StreamSubscription? _subscription; final Duration defaultRequestTimeOut; Map requests = {}; - bool _isDiscounnect = false; + bool _isDisconnected = false; - bool get isConnected => _isDiscounnect; + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.sink(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; + _socket?.close(); _socket = null; _subscription?.cancel().catchError((e) {}); _subscription = null; @@ -46,7 +45,8 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } @@ -56,14 +56,12 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { Duration defaultRequestTimeOut = const Duration(seconds: 30), final Duration connectionTimeOut = const Duration(seconds: 30), }) async { - final channel = - await WebSocketCore.connect(url, protocols: protocols?.toList()); + final channel = await WebSocketCore.connect(url, protocols: protocols?.toList()); - return ElectrumWebSocketService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); + return ElectrumWebSocketService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(String event) { + void _onMessage(String event) { final Map decode = json.decode(event); if (decode.containsKey("id")) { final int id = int.parse(decode["id"]!.toString()); @@ -73,16 +71,13 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { } @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + 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); + 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.lock b/example/pubspec.lock index 318e7a4..0aa5320 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: @@ -19,11 +27,9 @@ packages: blockchain_utils: dependency: "direct main" description: - path: "." - ref: cake-update-v2 - resolved-ref: "2767a54ed2b0a23494e4e96a3fe5b5022b834b70" - url: "https://github.com/cake-tech/blockchain_utils" - source: git + path: "/home/rafael/Working/blockchain_utils/" + relative: false + source: path version: "3.3.0" boolean_selector: dependency: transitive @@ -33,6 +39,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: @@ -57,6 +71,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -91,14 +121,22 @@ 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: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" http_parser: dependency: transitive description: @@ -107,6 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: @@ -171,6 +225,22 @@ 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 @@ -252,9 +322,9 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 25d16d4..11a339d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,9 +38,7 @@ dependencies: bitcoin_base: path: ../ blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ http: ^1.2.0 dev_dependencies: diff --git a/lib/bitcoin_base.dart b/lib/bitcoin_base.dart index cb559d7..a2c93a8 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -12,12 +12,16 @@ export 'package:bitcoin_base/src/bitcoin/address/util.dart'; export 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +export 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; + export 'package:bitcoin_base/src/crypto/crypto.dart'; export 'package:bitcoin_base/src/models/network.dart'; export 'package:bitcoin_base/src/provider/api_provider.dart'; +export 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; + export 'package:bitcoin_base/src/utils/utils.dart'; export 'package:bitcoin_base/src/cash_token/cash_token.dart'; diff --git a/lib/src/bitcoin/address/address.dart b/lib/src/bitcoin/address/address.dart index 4dd7d7e..c644cf8 100644 --- a/lib/src/bitcoin/address/address.dart +++ b/lib/src/bitcoin/address/address.dart @@ -10,7 +10,6 @@ library bitcoin_base.address; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; -import 'package:bitcoin_base/src/utils/enumerate.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bitcoin_base/src/utils/script.dart'; part 'core.dart'; @@ -18,3 +17,4 @@ part 'legacy_address.dart'; part 'utils/address_utils.dart'; part 'segwit_address.dart'; part 'network_address.dart'; +part 'derivations.dart'; diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index f5daaba..368e198 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -60,14 +60,13 @@ abstract class BitcoinAddressType implements Enumerate { } abstract class BitcoinBaseAddress { - BitcoinBaseAddress({this.network}); + BitcoinBaseAddress(); BitcoinAddressType get type; - String toAddress([BasedUtxoNetwork? network]); + String toAddress(BasedUtxoNetwork network); Script toScriptPubKey(); String pubKeyHash(); String get addressProgram; - BasedUtxoNetwork? network; static BitcoinBaseAddress fromString( String address, [ diff --git a/lib/src/bitcoin/address/derivations.dart b/lib/src/bitcoin/address/derivations.dart new file mode 100644 index 0000000..303a889 --- /dev/null +++ b/lib/src/bitcoin/address/derivations.dart @@ -0,0 +1,216 @@ +// ignore_for_file: constant_identifier_names +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +class BitcoinDerivationInfo { + BitcoinDerivationInfo({ + required this.derivationType, + required String derivationPath, + required this.scriptType, + this.description, + }) : derivationPath = Bip32PathParser.parse(derivationPath); + final BitcoinDerivationType derivationType; + final Bip32Path derivationPath; + final BitcoinAddressType scriptType; + final String? description; + + factory BitcoinDerivationInfo.fromJSON(Map json) { + return BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.values[json['derivationType']], + derivationPath: json['derivationPath'], + scriptType: BitcoinAddressType.values.firstWhere( + (type) => type.toString() == json['scriptType'], + ), + description: json['description'], + ); + } + + Map toJSON() { + return { + 'derivationType': derivationType.index, + 'derivationPath': derivationPath.toString(), + 'scriptType': scriptType.toString(), + 'description': description, + }; + } +} + +enum BitcoinDerivationType { bip39, electrum } + +// Define constant paths +abstract class BitcoinDerivationPaths { + static const String ELECTRUM = "m/0'"; + static const String BIP44 = "m/44'/0'/0'"; + static const String BIP49 = "m/49'/0'/0'"; + static const String BIP84 = "m/84'/0'/0'"; + static const String BIP86 = "m/86'/0'/0'"; + static const String NON_STANDARD = "m/0'"; + + static const String SILENT_PAYMENTS_SCAN = "m/352'/0'/0'/1'"; + static const String SILENT_PAYMENTS_SPEND = "m/352'/0'/0'/0'"; + + static const String LITECOIN = "m/84'/2'/0'"; + + static const String SAMOURAI_BAD_BANK = "m/84'/0'/2147483644'"; + static const String SAMOURAI_WHIRLPOOL_PREMIX = "m/84'/0'/2147483645'"; + static const String SAMOURAI_WHIRLPOOL_POSTMIX = "m/84'/0'/2147483646'"; + static const String SAMOURAI_RICOCHET_LEGACY = "m/44'/0'/2147483647'"; + static const String SAMOURAI_RICOCHET_COMPATIBILITY_SEGWIT = "m/49'/0'/2147483647'"; + static const String SAMOURAI_RICOCHET_NATIVE_SEGWIT = "m/84'/0'/2147483647'"; +} + +abstract class BitcoinDerivationInfos { + static final BitcoinDerivationInfo ELECTRUM = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.electrum, + derivationPath: BitcoinDerivationPaths.ELECTRUM, + description: "Electrum", + scriptType: SegwitAddresType.p2wpkh, + ); + + static final BitcoinDerivationInfo BIP44 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Standard BIP44", + scriptType: P2pkhAddressType.p2pkh, + ); + static final BitcoinDerivationInfo BIP49 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Standard BIP49 compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ); + static final BitcoinDerivationInfo BIP84 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP84, + description: "Standard BIP84 native segwit", + scriptType: SegwitAddresType.p2wpkh, + ); + static final BitcoinDerivationInfo BIP86 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP86, + description: "Standard BIP86 Taproot", + scriptType: SegwitAddresType.p2tr, + ); + + static final BitcoinDerivationInfo LITECOIN = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.LITECOIN, + description: "Default Litecoin", + scriptType: SegwitAddresType.p2wpkh, + ); + + static final BitcoinDerivationInfo SILENT_PAYMENTS_SCAN = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SILENT_PAYMENTS_SCAN, + description: "Silent Payments Scan", + scriptType: SilentPaymentsAddresType.p2sp, + ); + + static final BitcoinDerivationInfo SILENT_PAYMENTS_SPEND = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, + description: "Silent Payments Spend", + scriptType: SilentPaymentsAddresType.p2sp, + ); +} + +final Map> BITCOIN_DERIVATIONS = { + BitcoinDerivationType.electrum: [BitcoinDerivationInfos.ELECTRUM], + BitcoinDerivationType.bip39: [ + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Standard BIP44", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Standard BIP49 compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP84, + description: "Standard BIP84 native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP86, + description: "Standard BIP86 Taproot", + scriptType: SegwitAddresType.p2tr, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard legacy", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Samourai Deposit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Samourai Deposit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_BAD_BANK, + description: "Samourai Bad Bank (toxic change)", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_PREMIX, + description: "Samourai Whirlpool Pre Mix", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_POSTMIX, + description: "Samourai Whirlpool Post Mix", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_LEGACY, + description: "Samourai Ricochet legacy", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_COMPATIBILITY_SEGWIT, + description: "Samourai Ricochet compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_NATIVE_SEGWIT, + description: "Samourai Ricochet native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfos.LITECOIN, + BitcoinDerivationInfos.SILENT_PAYMENTS_SCAN, + BitcoinDerivationInfos.SILENT_PAYMENTS_SPEND, + ], +}; + +const String ELECTRUM_PATH = BitcoinDerivationPaths.ELECTRUM; diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index c2b9798..f998c15 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -9,12 +9,11 @@ abstract class LegacyAddress extends BitcoinBaseAddress { LegacyAddress.fromHash160({ required String h160, required BitcoinAddressType type, - super.network, }) : _addressProgram = _BitcoinAddressUtils.validateAddressProgram(h160, type), super(); LegacyAddress.fromAddress({required String address, required BasedUtxoNetwork network}) - : super(network: network) { + : super() { final decode = _BitcoinAddressUtils.decodeLegacyAddressWithNetworkAndType( address: address, type: type, @@ -28,14 +27,14 @@ abstract class LegacyAddress extends BitcoinBaseAddress { _addressProgram = decode; } - LegacyAddress.fromPubkey({required ECPublic pubkey, super.network}) + LegacyAddress.fromPubkey({required ECPublic pubkey}) : _pubkey = pubkey, _addressProgram = _BitcoinAddressUtils.pubkeyToHash160(pubkey.toHex()); - LegacyAddress.fromRedeemScript({required Script script, super.network}) + LegacyAddress.fromRedeemScript({required Script script}) : _addressProgram = _BitcoinAddressUtils.scriptToHash160(script); - LegacyAddress.fromScriptSig({required Script script, super.network}) { + LegacyAddress.fromScriptSig({required Script script}) { switch (type) { case PubKeyAddressType.p2pk: _signature = script.findScriptParam(0); @@ -79,13 +78,7 @@ abstract class LegacyAddress extends BitcoinBaseAddress { } @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { throw BitcoinBasePluginException("network does not support ${type.value} address"); } @@ -104,11 +97,10 @@ abstract class LegacyAddress extends BitcoinBaseAddress { } class P2shAddress extends LegacyAddress { - static RegExp get regex => RegExp(r'[23M][a-km-zA-HJ-NP-Z1-9]{25,34}'); + static final regex = RegExp(r'[23M][a-km-zA-HJ-NP-Z1-9]{25,34}'); P2shAddress.fromRedeemScript({ required super.script, - super.network, this.type = P2shAddressType.p2pkInP2sh, }) : super.fromRedeemScript(); @@ -120,20 +112,48 @@ class P2shAddress extends LegacyAddress { P2shAddress.fromHash160({ required super.h160, - super.network, this.type = P2shAddressType.p2pkInP2sh, }) : super.fromHash160(type: type); + factory P2shAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + P2shAddressType type = P2shAddressType.p2pkInP2sh, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + final pubkey = ECPublic.fromBip32(bip32.derive(fullPath).publicKey); + + switch (type) { + case P2shAddressType.p2pkInP2sh: + return pubkey.toP2pkInP2sh(); + case P2shAddressType.p2pkhInP2sh: + return pubkey.toP2pkhInP2sh(); + case P2shAddressType.p2wshInP2sh: + return pubkey.toP2wshInP2sh(); + case P2shAddressType.p2wpkhInP2sh: + return pubkey.toP2wpkhInP2sh(); + default: + throw UnimplementedError(); + } + } + + factory P2shAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2wpkhInP2sh(); + } + factory P2shAddress.fromScriptPubkey({ required Script script, - BasedUtxoNetwork? network, type = P2shAddressType.p2pkInP2sh, }) { if (script.getAddressType() is! P2shAddressType) { throw ArgumentError("Invalid scriptPubKey"); } - return P2shAddress.fromHash160(h160: script.findScriptParam(1), network: network, type: type); + return P2shAddress.fromHash160(h160: script.findScriptParam(1), type: type); } @override @@ -155,18 +175,17 @@ class P2shAddress extends LegacyAddress { } class P2pkhAddress extends LegacyAddress { - static RegExp get regex => RegExp(r'[1mnL][a-km-zA-HJ-NP-Z1-9]{25,34}'); + static final regex = RegExp(r'[1mnL][a-km-zA-HJ-NP-Z1-9]{25,34}'); factory P2pkhAddress.fromScriptPubkey({ required Script script, - BasedUtxoNetwork? network, P2pkhAddressType type = P2pkhAddressType.p2pkh, }) { if (script.getAddressType() != P2pkhAddressType.p2pkh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2pkhAddress.fromHash160(h160: script.findScriptParam(2), network: network, type: type); + return P2pkhAddress.fromHash160(h160: script.findScriptParam(2), type: type); } P2pkhAddress.fromAddress({ @@ -175,17 +194,27 @@ class P2pkhAddress extends LegacyAddress { this.type = P2pkhAddressType.p2pkh, }) : super.fromAddress(); - P2pkhAddress.fromHash160({ - required super.h160, - super.network, - this.type = P2pkhAddressType.p2pkh, - }) : super.fromHash160(type: type); + P2pkhAddress.fromHash160({required super.h160, this.type = P2pkhAddressType.p2pkh}) + : super.fromHash160(type: type); - P2pkhAddress.fromScriptSig({ - required super.script, - super.network, - this.type = P2pkhAddressType.p2pkh, - }) : super.fromScriptSig(); + P2pkhAddress.fromScriptSig({required super.script, this.type = P2pkhAddressType.p2pkh}) + : super.fromScriptSig(); + + factory P2pkhAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2pkhAddress(); + } + + factory P2pkhAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2pkhAddress(); + } @override Script toScriptPubKey() { @@ -209,7 +238,7 @@ class P2pkhAddress extends LegacyAddress { class P2pkAddress extends LegacyAddress { static RegExp get regex => RegExp(r'1([A-Za-z0-9]{34})'); - P2pkAddress({required ECPublic publicKey, super.network}) + P2pkAddress({required ECPublic publicKey}) : _pubkeyHex = publicKey.toHex(), super.fromPubkey(pubkey: publicKey); @@ -233,13 +262,7 @@ class P2pkAddress extends LegacyAddress { } @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { return _BitcoinAddressUtils.legacyToAddress( network: network, addressProgram: _BitcoinAddressUtils.pubkeyToHash160(_pubkeyHex), diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index 8b8fd7f..bed4732 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -9,7 +9,7 @@ abstract class BitcoinNetworkAddress { /// Converts the address to a string representation for the specified network [T]. String toAddress([T? network]) { - return network == null ? address : baseAddress.toAddress(); + return network == null ? address : baseAddress.toAddress(network); } /// The type of the Bitcoin address. @@ -28,7 +28,7 @@ class BitcoinAddress extends BitcoinNetworkAddress { factory BitcoinAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -45,7 +45,7 @@ class DogeAddress extends BitcoinNetworkAddress { factory DogeAddress.fromBaseAddress(BitcoinBaseAddress address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DogeAddress._(baseAddress, baseAddress.toAddress()); + return DogeAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -63,7 +63,7 @@ class PepeAddress extends BitcoinNetworkAddress { factory PepeAddress.fromBaseAddress(BitcoinBaseAddress address, {PepeNetwork network = PepeNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return PepeAddress._(baseAddress, baseAddress.toAddress()); + return PepeAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -81,7 +81,7 @@ class LitecoinAddress extends BitcoinNetworkAddress { factory LitecoinAddress.fromBaseAddress(BitcoinBaseAddress address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return LitecoinAddress._(baseAddress, baseAddress.toAddress()); + return LitecoinAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -108,7 +108,7 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { factory BitcoinCashAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinCashAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinCashAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -133,7 +133,7 @@ class DashAddress extends BitcoinNetworkAddress { factory DashAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DashAddress._(baseAddress, baseAddress.toAddress()); + return DashAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -150,7 +150,7 @@ class BitcoinSVAddress extends BitcoinNetworkAddress { factory BitcoinSVAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinSVAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinSVAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index 740e1ea..b45f4b0 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -5,7 +5,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { required String address, required BasedUtxoNetwork network, required this.segwitVersion, - }) : super(network: network) { + }) : super() { addressProgram = _BitcoinAddressUtils.toSegwitProgramWithVersionAndNetwork( address: address, version: segwitVersion, @@ -16,7 +16,6 @@ abstract class SegwitAddress extends BitcoinBaseAddress { SegwitAddress.fromProgram({ required String program, required SegwitAddresType addressType, - super.network, required this.segwitVersion, this.pubkey, }) : addressProgram = _BitcoinAddressUtils.validateAddressProgram(program, addressType), @@ -24,7 +23,6 @@ abstract class SegwitAddress extends BitcoinBaseAddress { SegwitAddress.fromRedeemScript({ required Script script, - super.network, required this.segwitVersion, }) : addressProgram = _BitcoinAddressUtils.segwitScriptToSHA256(script); @@ -34,13 +32,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { ECPublic? pubkey; @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { throw BitcoinBasePluginException("network does not support ${type.value} address"); } @@ -59,26 +51,43 @@ abstract class SegwitAddress extends BitcoinBaseAddress { } class P2wpkhAddress extends SegwitAddress { - static RegExp get regex => RegExp(r'(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}'); + static final regex = RegExp(r'(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}'); P2wpkhAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); - P2wpkhAddress.fromProgram({required super.program, super.network}) + P2wpkhAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, addressType: SegwitAddresType.p2wpkh, ); - P2wpkhAddress.fromRedeemScript({required super.script, super.network}) + P2wpkhAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); - factory P2wpkhAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2wpkhAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2wpkhAddress(); + } + + factory P2wpkhAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2wpkhAddress(); + } + + factory P2wpkhAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2wpkh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2wpkhAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2wpkhAddress.fromProgram(program: script.findScriptParam(1)); } /// returns the scriptPubKey of a P2WPKH witness script @@ -93,27 +102,43 @@ class P2wpkhAddress extends SegwitAddress { } class P2trAddress extends SegwitAddress { - static RegExp get regex => + static final regex = RegExp(r'(bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'); P2trAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV1); - P2trAddress.fromProgram({required super.program, super.network, super.pubkey}) + P2trAddress.fromProgram({required super.program, super.pubkey}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV1, addressType: SegwitAddresType.p2tr, ); - P2trAddress.fromRedeemScript({required super.script, super.network}) + P2trAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV1); - factory P2trAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2trAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2trAddress(); + } + + factory P2trAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2trAddress(); + } + + factory P2trAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2tr) { throw ArgumentError("Invalid scriptPubKey"); } - return P2trAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2trAddress.fromProgram(program: script.findScriptParam(1)); } /// returns the scriptPubKey of a P2TR witness script @@ -128,26 +153,38 @@ class P2trAddress extends SegwitAddress { } class P2wshAddress extends SegwitAddress { - static RegExp get regex => RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{40,80}'); + static final regex = RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{40,80}'); P2wshAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); - P2wshAddress.fromProgram({required super.program, super.network}) + P2wshAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, addressType: SegwitAddresType.p2wsh, ); - P2wshAddress.fromRedeemScript({required super.script, super.network}) + P2wshAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); - factory P2wshAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2wshAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2wshAddress(); + } + + factory P2wshAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2wsh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2wshAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2wshAddress.fromProgram(program: script.findScriptParam(1)); } /// Returns the scriptPubKey of a P2WPKH witness script diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index 7ec257d..b878886 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; import 'package:bitcoin_base/src/utils/utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bitcoin_base/src/bitcoin/address/address.dart'; @@ -26,6 +27,28 @@ class BitcoinAddressUtils { return addressType.toScriptPubKey().toBytes(); } + static String addressFromOutputScript(Script script, BasedUtxoNetwork network) { + try { + switch (script.getAddressType()) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkhInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2tr: + return P2trAddress.fromScriptPubkey(script: script).toAddress(network); + default: + } + } catch (_) {} + + return ''; + } + static String scriptHash(String address, {required BasedUtxoNetwork network}) { final outputScript = addressToOutputScript(address: address, network: network); final parts = BytesUtils.toHexString(QuickCrypto.sha256Hash(outputScript)).split(''); @@ -41,4 +64,52 @@ class BitcoinAddressUtils { return res; } + + static BitcoinAddressType getScriptType(BitcoinBaseAddress type) { + if (type is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (type is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (type is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (type is P2trAddress) { + return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; + } else if (type is SilentPaymentsAddresType) { + return SilentPaymentsAddresType.p2sp; + } else { + return SegwitAddresType.p2wpkh; + } + } + + static int getAccountFromChange(bool isChange) { + return isChange ? 1 : 0; + } + + static BitcoinDerivationInfo getDerivationFromType( + BitcoinAddressType scriptType, { + bool? isElectrum = false, + }) { + switch (scriptType) { + case P2pkhAddressType.p2pkh: + return BitcoinDerivationInfos.BIP44; + case P2shAddressType.p2wpkhInP2sh: + return BitcoinDerivationInfos.BIP49; + case SegwitAddresType.p2wpkh: + if (isElectrum == true) { + return BitcoinDerivationInfos.ELECTRUM; + } else { + return BitcoinDerivationInfos.BIP84; + } + case SegwitAddresType.p2tr: + return BitcoinDerivationInfos.BIP86; + case SegwitAddresType.mweb: + return BitcoinDerivationInfos.BIP86; + case SegwitAddresType.p2wsh: + return BitcoinDerivationInfos.BIP84; + default: + throw Exception("Derivation not available for $scriptType"); + } + } } diff --git a/lib/src/bitcoin/amount/amount.dart b/lib/src/bitcoin/amount/amount.dart new file mode 100644 index 0000000..df7f858 --- /dev/null +++ b/lib/src/bitcoin/amount/amount.dart @@ -0,0 +1,5 @@ +library bitcoin_base.amount; + +import 'package:intl/intl.dart'; + +part 'utils.dart'; diff --git a/lib/src/bitcoin/amount/utils.dart b/lib/src/bitcoin/amount/utils.dart new file mode 100644 index 0000000..8b1f8bc --- /dev/null +++ b/lib/src/bitcoin/amount/utils.dart @@ -0,0 +1,33 @@ +part of 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; + +class BitcoinAmountUtils { + static const bitcoinAmountLength = 8; + static const bitcoinAmountDivider = 100000000; + static final bitcoinAmountFormat = NumberFormat() + ..maximumFractionDigits = bitcoinAmountLength + ..minimumFractionDigits = 1; + + static double cryptoAmountToDouble({required num amount, required num divider}) => + amount / divider; + + static String bitcoinAmountToString({required int amount}) => + bitcoinAmountFormat.format(cryptoAmountToDouble( + amount: amount, + divider: bitcoinAmountDivider, + )); + + static double bitcoinAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); + + static int stringDoubleToBitcoinAmount(String amount) { + int result = 0; + + try { + result = (double.parse(amount) * bitcoinAmountDivider).round(); + } catch (e) { + result = 0; + } + + return result; + } +} diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 3ea3079..4169ddc 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -148,16 +148,17 @@ class BtcTransaction { mwebBytes = rawtx.sublist(cursor, rawtx.length - 4); cursor = rawtx.length - 4; } - List lock = rawtx.sublist(cursor, cursor + 4); + List lock = rawtx.sublist(rawtx.length - 4); return BtcTransaction( - inputs: inputs, - outputs: outputs, - witnesses: witnesses, - hasSegwit: hasSegwit, - canReplaceByFee: canReplaceByFee, - mwebBytes: mwebBytes, - version: version, - lock: lock); + inputs: inputs, + outputs: outputs, + witnesses: witnesses, + hasSegwit: hasSegwit, + canReplaceByFee: canReplaceByFee, + mwebBytes: mwebBytes, + version: version, + lock: lock, + ); } /// returns the transaction input's digest that is to be signed according. @@ -242,7 +243,7 @@ class BtcTransaction { if (mwebBytes != null) { data.add(mwebBytes!); } - data.add(locktime); + data.add([0, 0, 0, 0]); return data.toBytes(); } diff --git a/lib/src/bitcoin/silent_payments/address.dart b/lib/src/bitcoin/silent_payments/address.dart index e622851..4c3afea 100644 --- a/lib/src/bitcoin/silent_payments/address.dart +++ b/lib/src/bitcoin/silent_payments/address.dart @@ -2,10 +2,6 @@ // ignore_for_file: non_constant_identifier_names part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; -const SCAN_PATH = "m/352'/1'/0'/1'/0"; - -const SPEND_PATH = "m/352'/1'/0'/0'/0"; - class SilentPaymentOwner extends SilentPaymentAddress { final ECPrivate b_scan; final ECPrivate b_spend; @@ -16,7 +12,6 @@ class SilentPaymentOwner extends SilentPaymentAddress { required super.B_spend, required this.b_scan, required this.b_spend, - super.network, }) : super(); factory SilentPaymentOwner.fromPrivateKeys({ @@ -30,33 +25,38 @@ class SilentPaymentOwner extends SilentPaymentAddress { b_spend: b_spend, B_scan: b_scan.getPublic(), B_spend: b_spend.getPublic(), - network: network, version: version ?? 0, ); } - factory SilentPaymentOwner.fromHd(Bip32Slip10Secp256k1 bip32, {String? hrp, int? version}) { - final scanDerivation = bip32.derivePath(SCAN_PATH); - final spendDerivation = bip32.derivePath(SPEND_PATH); + factory SilentPaymentOwner.fromBip32(Bip32Slip10Secp256k1 bip32, {int? version}) { + final scanDerivation = bip32.derive( + Bip32PathParser.parse(BitcoinDerivationPaths.SILENT_PAYMENTS_SCAN), + ); + final spendDerivation = bip32.derive( + Bip32PathParser.parse(BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND), + ); return SilentPaymentOwner( b_scan: ECPrivate(scanDerivation.privateKey), b_spend: ECPrivate(spendDerivation.privateKey), B_scan: ECPublic.fromBip32(scanDerivation.publicKey), B_spend: ECPublic.fromBip32(spendDerivation.publicKey), - network: hrp == "tsp" ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, version: version ?? 0, ); } - factory SilentPaymentOwner.fromMnemonic(String mnemonic, {String? hrp, int? version}) { - return SilentPaymentOwner.fromHd( - Bip32Slip10Secp256k1.fromSeed( - Bip39MnemonicDecoder().decode(mnemonic), - hrp == "tsp" ? Bip32Const.testNetKeyNetVersions : Bip32Const.mainNetKeyNetVersions, - ), - hrp: hrp, - version: version); + factory SilentPaymentOwner.fromMnemonic(String mnemonic, + {BasedUtxoNetwork? network, int? version}) { + return SilentPaymentOwner.fromBip32( + Bip32Slip10Secp256k1.fromSeed( + Bip39MnemonicDecoder().decode(mnemonic), + network == BitcoinNetwork.testnet + ? Bip32Const.testNetKeyNetVersions + : Bip32Const.mainNetKeyNetVersions, + ), + version: version, + ); } List generateLabel(int m) { @@ -70,10 +70,29 @@ class SilentPaymentOwner extends SilentPaymentAddress { b_spend: b_spend, B_scan: B_scan, B_spend: B_m, - network: network, version: version, ); } + + Map toJson() { + return { + 'version': version, + 'B_scan': B_scan.toHex(), + 'B_spend': B_spend.toHex(), + 'b_scan': b_scan.toHex(), + 'b_spend': b_spend.toHex(), + }; + } + + static SilentPaymentOwner fromJson(Map json) { + return SilentPaymentOwner( + version: json['version'] as int, + B_scan: ECPublic.fromHex(json['B_scan'] as String), + B_spend: ECPublic.fromHex(json['B_spend'] as String), + b_scan: ECPrivate.fromHex(json['b_scan'] as String), + b_spend: ECPrivate.fromHex(json['b_spend'] as String), + ); + } } class SilentPaymentDestination extends SilentPaymentAddress { @@ -81,7 +100,6 @@ class SilentPaymentDestination extends SilentPaymentAddress { required super.version, required ECPublic scanPubkey, required ECPublic spendPubkey, - super.network, required this.amount, }) : super(B_scan: scanPubkey, B_spend: spendPubkey); @@ -93,7 +111,6 @@ class SilentPaymentDestination extends SilentPaymentAddress { return SilentPaymentDestination( scanPubkey: receiver.B_scan, spendPubkey: receiver.B_spend, - network: receiver.network, version: receiver.version, amount: amount, ); @@ -106,16 +123,12 @@ class SilentPaymentAddress implements BitcoinBaseAddress { final int version; final ECPublic B_scan; final ECPublic B_spend; - @override - BasedUtxoNetwork? network; - final String hrp; SilentPaymentAddress({ required this.B_scan, required this.B_spend, - this.network = BitcoinNetwork.mainnet, this.version = 0, - }) : hrp = (network == BitcoinNetwork.testnet ? "tsp" : "sp") { + }) { if (version != 0) { throw Exception("Can't have other version than 0 for now"); } @@ -143,20 +156,19 @@ class SilentPaymentAddress implements BitcoinBaseAddress { return SilentPaymentAddress( B_scan: ECPublic.fromBytes(key.sublist(0, 33)), B_spend: ECPublic.fromBytes(key.sublist(33)), - network: prefix == 'tsp' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, version: version, ); } @override - String toAddress([BasedUtxoNetwork? network]) { + String toAddress(BasedUtxoNetwork network) { return toString(network: network); } @override String toString({BasedUtxoNetwork? network}) { return Bech32EncoderBase.encodeBech32( - hrp, + network == BitcoinNetwork.testnet ? 'tsp' : 'sp', [ version, ...Bech32BaseUtils.convertToBase32( diff --git a/lib/src/crypto/keypair/ec_private.dart b/lib/src/crypto/keypair/ec_private.dart index e8ace7a..c5ace75 100644 --- a/lib/src/crypto/keypair/ec_private.dart +++ b/lib/src/crypto/keypair/ec_private.dart @@ -4,7 +4,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:pointycastle/export.dart'; -import 'package:bip32/bip32.dart' as bip32; import 'package:bip32/src/utils/ecurve.dart' as ecc; /// Represents an ECDSA private key. @@ -33,6 +32,18 @@ class ECPrivate { return ECPrivate.fromBytes(decode.item1); } + factory ECPrivate.fromBip32({required Bip32Base bip32, int? account, int? index}) { + if (account != null) { + bip32 = bip32.childKey(Bip32KeyIndex(account)); + + if (index != null) { + bip32 = bip32.childKey(Bip32KeyIndex(index)); + } + } + + return ECPrivate(bip32.privateKey); + } + /// returns as WIFC (compressed) or WIF format (string) String toWif({bool compressed = true, BitcoinNetwork? network}) { List bytes = [...(network ?? BitcoinNetwork.mainnet).wifNetVer, ...toBytes()]; @@ -57,7 +68,6 @@ class ECPrivate { /// Returns a Bitcoin compact signature in hex String signMessage(List message, {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { - final messageHash = QuickCrypto.sha256Hash(BitcoinSignerUtils.magicMessage(message, messagePrefix)); diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index 18a5ec4..045f993 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -116,6 +116,10 @@ class ECPublic { return P2trAddress.fromProgram(program: pubKey, pubkey: ECPublic.fromHex(pubKey)); } + P2trAddress toP2trAddress({List>? scripts, bool tweak = true}) { + return toTaprootAddress(scripts: scripts, tweak: tweak); + } + /// toP2wpkhInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2WPKH (Pay-to-Witness-Public-Key-Hash) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart index 3f470df..81a09ac 100644 --- a/lib/src/provider/api_provider/api_provider.dart +++ b/lib/src/provider/api_provider/api_provider.dart @@ -7,10 +7,13 @@ 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.fromMempool( + BasedUtxoNetwork network, { + Map? header, + String? baseUrl, + }) { + final api = APIConfig.mempool(network, baseUrl); + return ApiProvider(api: api, header: header, service: BitcoinApiService()); } factory ApiProvider.fromBlocCypher(BasedUtxoNetwork network, ApiService service, {Map? header}) { @@ -80,7 +83,7 @@ class ApiProvider { } } - Future getNetworkFeeRate({String Function(String)? tokenize}) async { + Future getRecommendedFeeRate({String Function(String)? tokenize}) async { final apiUrl = api.getFeeApiUrl(); final url = tokenize?.call(apiUrl) ?? apiUrl; final response = await _getRequest>(url); diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index 4db27db..e9a348e 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -1,35 +1,96 @@ +import 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; -import 'package:blockchain_utils/exception/exceptions.dart'; +typedef ListenerCallback = StreamSubscription Function( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, +}); class ElectrumApiProvider { final BitcoinBaseElectrumRPCService rpc; - ElectrumApiProvider(this.rpc); + ElectrumApiProvider._(this.rpc); int _id = 0; + Timer? _aliveTimer; + + static Future connect(Future rpc) async { + final provider = ElectrumApiProvider._(await rpc); + provider.keepAlive(); + return provider; + } /// 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 { + 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)); + final result = await rpc.call(params, timeout); + return request.onResponse(result); + } + + // Preserving generic type T in subscribe method + ListenerCallback? subscribe(ElectrumRequest request) { + final id = ++_id; + final params = request.toRequest(id); + final subscription = rpc.subscribe(params); + + if (subscription == null) return null; + + try { + // Create a transformer that uses the request's response handler + final stream = subscription.subscription.map(request.onResponse); + + // Return a properly typed listener callback + return ( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + }; + } catch (_) { + return null; + } } - 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, - ); + Future> getFeeRates() async { + try { + final topDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 1)); + final middleDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 5)); + final bottomDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 10)); + final top = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) + .round(); + final middle = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) + .round(); + final bottom = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) + .round(); + + return [bottom, middle, top]; + } catch (_) { + return []; } + } + + void keepAlive() { + _aliveTimer?.cancel(); + _aliveTimer = Timer.periodic(const Duration(seconds: 6), (_) async => ping()); + } - return data["result"]; + void ping() async { + try { + return await request(ElectrumPing()); + } catch (_) {} } } 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..2488bb1 100644 --- a/lib/src/provider/electrum_methods/methods/add_peer.dart +++ b/lib/src/provider/electrum_methods/methods/add_peer.dart @@ -21,7 +21,7 @@ class ElectrumAddPeer extends ElectrumRequest { /// 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..2f24a6a 100644 --- a/lib/src/provider/electrum_methods/methods/block_headers.dart +++ b/lib/src/provider/electrum_methods/methods/block_headers.dart @@ -2,10 +2,8 @@ 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 - extends ElectrumRequest, Map> { - ElectrumBlockHeaders( - {required this.startHeight, required this.count, required this.cpHeight}); +class ElectrumBlockHeaders extends ElectrumRequest, Map> { + ElectrumBlockHeaders({required this.startHeight, required this.count, required this.cpHeight}); /// The height of the first header requested, a non-negative integer. final int startHeight; @@ -27,7 +25,7 @@ class ElectrumBlockHeaders /// 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..1dfdc1c 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -11,7 +11,7 @@ class ElectrumBroadCastTransaction extends ElectrumRequest { /// blockchain.transaction.broadcast @override - String get method => ElectrumRequestMethods.broadCast.method; + String get method => ElectrumRequestMethods.broadcast.method; @override List toJson() { @@ -20,7 +20,7 @@ class ElectrumBroadCastTransaction extends ElectrumRequest { /// 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..f66127f 100644 --- a/lib/src/provider/electrum_methods/methods/donate_address.dart +++ b/lib/src/provider/electrum_methods/methods/donate_address.dart @@ -13,7 +13,7 @@ class ElectrumDonationAddress extends ElectrumRequest { } @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..5fb0e63 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -22,7 +22,7 @@ class ElectrumVersion extends ElectrumRequest, List> { /// 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..5e59971 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -20,7 +20,7 @@ class ElectrumEstimateFee extends ElectrumRequest { /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResonse(result) { + BigInt onResponse(result) { return BtcUtils.toSatoshi(result.toString()).abs(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_balance.dart b/lib/src/provider/electrum_methods/methods/get_balance.dart index c9a49e0..a8a0ef9 100644 --- a/lib/src/provider/electrum_methods/methods/get_balance.dart +++ b/lib/src/provider/electrum_methods/methods/get_balance.dart @@ -22,7 +22,7 @@ class ElectrumGetScriptHashBalance /// 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; } } 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..512da7a 100644 --- a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart +++ b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart @@ -3,8 +3,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.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 ElectrumGetFeeHistogram extends ElectrumRequest>, List> { /// mempool.get_fee_histogram @override String get method => ElectrumRequestMethods.getFeeHistogram.method; @@ -19,7 +18,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) { + List> onResponse(result) { return result.map((e) => List.from(e)).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..ccf3ccd 100644 --- a/lib/src/provider/electrum_methods/methods/get_history.dart +++ b/lib/src/provider/electrum_methods/methods/get_history.dart @@ -23,7 +23,7 @@ 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(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_mempool.dart b/lib/src/provider/electrum_methods/methods/get_mempool.dart index fcd33a2..3fc91a7 100644 --- a/lib/src/provider/electrum_methods/methods/get_mempool.dart +++ b/lib/src/provider/electrum_methods/methods/get_mempool.dart @@ -21,7 +21,7 @@ class ElectrumScriptHashGetMempool /// 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..a4d196c 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -3,8 +3,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.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> { +class ElectrumGetMerkle extends ElectrumRequest, Map> { ElectrumGetMerkle({required this.transactionHash, required this.height}); /// The transaction hash as a hexadecimal string. @@ -23,7 +22,7 @@ class ElectrumGetMerkle } @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..2fbb74a 100644 --- a/lib/src/provider/electrum_methods/methods/get_transaction.dart +++ b/lib/src/provider/electrum_methods/methods/get_transaction.dart @@ -3,14 +3,38 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.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 ElectrumGetTransactionHex extends ElectrumRequest { + ElectrumGetTransactionHex({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 toJson() { + 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 ElectrumGetTransactionVerbose + extends ElectrumRequest, Map> { + ElectrumGetTransactionVerbose({required this.transactionHash}); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; /// blockchain.transaction.get @override @@ -18,7 +42,7 @@ class ElectrumGetTransaction extends ElectrumRequest { @override List toJson() { - return [transactionHash, verbose]; + return [transactionHash, true]; } /// If verbose is false: @@ -27,7 +51,7 @@ 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) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index 2a957d1..c9e3dd1 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -4,10 +4,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Return an ordered list of UTXOs sent to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashListUnspent - extends ElectrumRequest, List> { - ElectrumScriptHashListUnspent( - {required this.scriptHash, this.includeTokens = false}); +class ElectrumScriptHashListUnspent extends ElectrumRequest, List> { + ElectrumScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -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 List 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..8bfe134 100644 --- a/lib/src/provider/electrum_methods/methods/get_value_proof.dart +++ b/lib/src/provider/electrum_methods/methods/get_value_proof.dart @@ -25,7 +25,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..f961340 100644 --- a/lib/src/provider/electrum_methods/methods/header.dart +++ b/lib/src/provider/electrum_methods/methods/header.dart @@ -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..2d681b5 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -1,9 +1,24 @@ 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> { + extends ElectrumRequest> { /// blockchain.headers.subscribe @override String get method => ElectrumRequestMethods.headersSubscribe.method; @@ -15,7 +30,7 @@ class ElectrumHeaderSubscribe /// 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..cf7b2e8 100644 --- a/lib/src/provider/electrum_methods/methods/id_from_pos.dart +++ b/lib/src/provider/electrum_methods/methods/id_from_pos.dart @@ -27,7 +27,7 @@ class ElectrumIdFromPos extends ElectrumRequest { /// 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..64be82a 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart @@ -20,7 +20,7 @@ class ElectrumMasternodeAnnounceBroadcast extends ElectrumRequest { /// 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..316767d 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_list.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_list.dart @@ -2,8 +2,7 @@ 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> { +class ElectrumMasternodeList extends ElectrumRequest, List> { ElectrumMasternodeList({required this.payees}); /// An array of masternode payee addresses. @@ -20,7 +19,7 @@ class ElectrumMasternodeList /// 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..e9b1445 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart @@ -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..d29fdb8 100644 --- a/lib/src/provider/electrum_methods/methods/ping.dart +++ b/lib/src/provider/electrum_methods/methods/ping.dart @@ -12,7 +12,7 @@ class ElectrumPing extends ElectrumRequest { } @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..7004fae 100644 --- a/lib/src/provider/electrum_methods/methods/protx_diff.dart +++ b/lib/src/provider/electrum_methods/methods/protx_diff.dart @@ -22,7 +22,7 @@ class ElectrumProtXDiff extends ElectrumRequest, dynamic> { /// 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..1f78505 100644 --- a/lib/src/provider/electrum_methods/methods/protx_info.dart +++ b/lib/src/provider/electrum_methods/methods/protx_info.dart @@ -19,7 +19,7 @@ class ElectrumProtXInfo extends ElectrumRequest, dynamic> { /// 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..aea1478 100644 --- a/lib/src/provider/electrum_methods/methods/relay_fee.dart +++ b/lib/src/provider/electrum_methods/methods/relay_fee.dart @@ -15,7 +15,7 @@ class ElectrumRelayFee extends ElectrumRequest { /// 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..90cfcb8 100644 --- a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart +++ b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart @@ -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..4a2c0ac 100644 --- a/lib/src/provider/electrum_methods/methods/server_banner.dart +++ b/lib/src/provider/electrum_methods/methods/server_banner.dart @@ -12,7 +12,7 @@ class ElectrumServerBanner extends ElectrumRequest { } @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..725d6ef 100644 --- a/lib/src/provider/electrum_methods/methods/server_features.dart +++ b/lib/src/provider/electrum_methods/methods/server_features.dart @@ -15,7 +15,7 @@ class ElectrumServerFeatures extends ElectrumRequest { /// 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..04e8709 100644 --- a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart @@ -2,8 +2,7 @@ import 'package:bitcoin_base/src/provider/api_provider.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 ElectrumServerPeersSubscribe extends ElectrumRequest, List> { /// server.peers.subscribe @override String get method => ElectrumRequestMethods.serverPeersSubscribe.method; @@ -15,7 +14,7 @@ class ElectrumServerPeersSubscribe /// 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..2967676 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/status.dart @@ -2,8 +2,7 @@ import 'package:bitcoin_base/src/provider/api_provider.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashSubscribe - extends ElectrumRequest, dynamic> { +class ElectrumScriptHashSubscribe extends ElectrumRequest { ElectrumScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) @@ -20,7 +19,7 @@ class ElectrumScriptHashSubscribe /// The status of the script hash. @override - Map onResonse(result) { - return Map.from(result); + String onResponse(result) { + return 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..18cf2ee --- /dev/null +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -0,0 +1,98 @@ +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}); + + factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { + 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}); + + final int height; + final int count; + + @override + String get method => ElectrumRequestMethods.tweaksSubscribe.method; + + @override + List toJson() { + return [height, count]; + } + + /// The header of the current block chain tip. + @override + ElectrumTweaksSubscribeResponse onResponse(result) { + return ElectrumTweaksSubscribeResponse.fromJson(result); + } +} diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 9369398..444098e 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -71,8 +71,7 @@ class APIConfig { } 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/###", sendTransaction: "$baseUrl/txs/push", @@ -82,18 +81,19 @@ 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( diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart index 9fd9abc..9ec03d7 100644 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ b/lib/src/provider/models/electrum/electrum_utxo.dart @@ -30,11 +30,25 @@ 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, + ); + } + + Map toJson() { + return { + "height": height, + "tx_hash": txId, + "tx_pos": vout, + "value": value.toString(), + }; + } + + static List fromJsonList(List json) { + return json.map((e) => ElectrumUtxo.fromJson(e)).toList(); } } diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 09c6312..37aa345 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; + final BitcoinFee? economyFee; /// only mnenpool api - final BigInt? hourFee; + 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/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index c8b56be..8be4045 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -1,3 +1,7 @@ export 'methods.dart'; export 'params.dart'; export 'service.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..57e1cbd --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.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; + + onConnectionStatusChange = null; + } + + 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).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)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] as int?; + if (id == null) { + _tasks.forEach((key, value) { + if (value.request.method == response['method']) { + id = key; + } + }); + } + + try { + final result = _findResult(response, _tasks[id]!.request); + + if (result != null) { + _finish(id!, result); + } + } catch (_) {} + } + + void _onMessage(List event) { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } + + dynamic _findResult(dynamic data, ElectrumRequestDetails request) { + if (data["error"] != null) { + if (data["error"] is String) { + _errors[request.id] = RPCError( + data: data["error"], + errorCode: 0, + message: data["error"], + request: request.params, + ); + } else { + final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; + final message = data["error"]?['message'] ?? ""; + _errors[request.id] = RPCError( + errorCode: code, + message: message, + data: data["error"]?["data"], + request: data["request"] ?? request.params, + ); + } + + throw _errors[request.id]!; + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + task.completer!.complete(result); + } + + if (!task.isSubscription) { + _tasks.remove(id); + } 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; + } + } + + 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); + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} 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..ea965af --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.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; + + onConnectionStatusChange = null; + } + + 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)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] as int?; + if (id == null) { + _tasks.forEach((key, value) { + if (value.request.method == response['method']) { + id = key; + } + }); + } + + try { + final result = _findResult(response, _tasks[id]!.request); + + if (result != null) { + _finish(id!, result); + } + } catch (_) {} + } + + void _onMessage(List event) { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } + + dynamic _findResult(dynamic data, ElectrumRequestDetails request) { + if (data["error"] != null) { + if (data["error"] is String) { + _errors[request.id] = RPCError( + data: data["error"], + errorCode: 0, + message: data["error"], + request: request.params, + ); + } else { + final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; + final message = data["error"]?['message'] ?? ""; + _errors[request.id] = RPCError( + errorCode: code, + message: message, + data: data["error"]?["data"], + request: data["request"] ?? request.params, + ); + } + + throw _errors[request.id]!; + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + task.completer!.complete(result); + } + + if (!task.isSubscription) { + _tasks.remove(id); + } 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; + } + } + + 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); + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} 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..e5fe2df 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 = @@ -59,16 +60,16 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("blockchain.estimatefee"); /// 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"); @@ -83,8 +84,14 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods version = ElectrumRequestMethods._("server.version"); /// 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..d253ff7 100644 --- a/lib/src/provider/service/electrum/params.dart +++ b/lib/src/provider/service/electrum/params.dart @@ -34,7 +34,7 @@ class ElectrumRequestDetails { abstract class ElectrumRequest implements ElectrumRequestParams { String? get validate => null; - RESULT onResonse(RESPONSE result) { + RESULT onResponse(RESPONSE result) { return result as RESULT; } 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..972641d 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -1,12 +1,50 @@ +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.completer, + this.subject, + }); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; + final ElectrumRequestDetails 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); + + /// 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]); + + bool get isConnected; + void disconnect(); + void reconnect(); +} + +bool isJSONStringCorrect(String source) { + try { + json.decode(source); + return true; + } catch (_) { + return false; + } } diff --git a/lib/src/provider/service/http/http_service.dart b/lib/src/provider/service/http/http_service.dart index be0e94a..c0ddd78 100644 --- a/lib/src/provider/service/http/http_service.dart +++ b/lib/src/provider/service/http/http_service.dart @@ -1,3 +1,8 @@ +import 'dart:convert'; + +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:http/http.dart' as http; + /// The [ApiService] abstract class defines a contract for making HTTP requests. abstract class ApiService { /// Performs an HTTP GET request to the specified [url]. @@ -15,3 +20,68 @@ abstract class ApiService { Future post(String url, {Map headers = const {"Content-Type": "application/json"}, Object? body}); } + +class ApiProviderException implements Exception { + final String message; + final int? statusCode; + final Map? responseData; + const ApiProviderException(this.message, [this.statusCode, this.responseData]); + @override + String toString() { + return "status: $statusCode $message ${responseData ?? ""}"; + } +} + +class BitcoinApiService implements ApiService { + BitcoinApiService([http.Client? client]) : _client = client ?? http.Client(); + final http.Client _client; + @override + Future get(String url) async { + final response = await _client.get(Uri.parse(url)); + return _readResponse(response); + } + + @override + Future post(String url, + {Map headers = const {"Content-Type": "application/json"}, + Object? body}) async { + final response = await _client.post(Uri.parse(url), headers: headers, body: body); + return _readResponse(response); + } + + T _readResponse(http.Response response) { + final String toString = _readBody(response); + switch (T) { + case String: + return toString as T; + case List: + case Map: + return jsonDecode(toString) as T; + default: + try { + return jsonDecode(toString) as T; + } catch (e) { + throw const ApiProviderException("invalid request"); + } + } + } + + String _readBody(http.Response response) { + _readErr(response); + return StringUtils.decode(response.bodyBytes); + } + + void _readErr(http.Response response) { + if (response.statusCode == 200 || response.statusCode == 201) return; + String toString = StringUtils.decode(response.bodyBytes); + Map? errorResult; + try { + if (toString.isNotEmpty) { + errorResult = StringUtils.toJson(toString); + } + // ignore: empty_catches + } catch (e) {} + toString = toString.isEmpty ? "request_error" : toString; + throw ApiProviderException(toString, response.statusCode, errorResult); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a4d6530..059c368 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,9 +20,10 @@ dependencies: pointycastle: ^3.7.4 bip32: ^2.0.0 blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ + rxdart: ^0.28.0 + intl: ^0.18.0 + http: ^1.1.0 dev_dependencies: diff --git a/test/encode_decode_transaction_test.dart b/test/encode_decode_transaction_test.dart index b77dfdb..d3588a1 100644 --- a/test/encode_decode_transaction_test.dart +++ b/test/encode_decode_transaction_test.dart @@ -5,8 +5,7 @@ void main() { test("test1", () { final tx = BtcTransaction(inputs: [ TxInput( - txId: - "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 0, scriptSig: Script( script: [ @@ -16,8 +15,7 @@ void main() { ), ), TxInput( - txId: - "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 2, scriptSig: Script(script: [ "3044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd41", @@ -38,8 +36,7 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: - "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.from(2000), ), @@ -53,8 +50,7 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: - "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.parse("799999999999996000"), ), @@ -69,8 +65,9 @@ void main() { )), ]); const String raw = - "0200000002752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada000000006a47304402203795e2aa978afbbd676b36c0edd1a39478744d320c5e02dd6a39d154755a5a3e02205545a7765bae3db9b820b1db9be2f8955a8998fa92ac08b0d7416aa30a3cee42412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada020000006a473044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff03389c9900000000001976a914b8d913342894ec7b066420952a618ec2a8269bc288ace80300000000000048ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10fdd007aa201d12ffe8e85fdab36794cb09418982efdbd5c8cee5fbeb216ac43887ea4817b887e80300000000000044ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10ff60f04fecc22b1a0b76a914b8d913342894ec7b066420952a618ec2a8269bc288ac00000000"; + "010000000001072b50f5750fc84801618b558e79d8e3c37d2c513c59cdbb590d90d043d3a9aa83000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffbcf5ae0a0c5efd15b06b380d59e535cf4fec65150ad301906012ea796f3a4607000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4be773c0882999ba5ae2ee574951f926a72037637aeda4344fa37655769ed78d000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4b29cb407a116c0c26c73332a3a2dd5e878b3c66dd0bdc516c9b763344cb8119000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff0b76aceb79e3f2570b82fe4ec5709b5affd04ac9bf7898fdee4a5dac5a429c82000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff5469f86d1c2ceb0d7da957f6d82d98f3b0f3e4138800fb9f2fb23444476c4ed7000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffc1fb64b4a9153728ccdfb8f5a2f0d60d42644bb39f0be24408f181c369c9337f000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff028c43e32e00000000160014455980d0b130ff894536fa79a322db815ab9cde4d9efb0000000000017a914424f29a8a84fa867814ff9ded43379c9dc9a6814870248304502210096990ba2bb32d80dc35ee55548bb300237744e9fdf08fcaf918672c60698da7b02202fe20657730114cdcbe01eef860b537be0b34380505d3d891d741ef2344330ca012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022010699f80eb6495d01ce1b00c70dacc76ed8ea5b1652f9cb04b91456d94cfafe30220111ca2bbb5192ec35247cb6250d1a68f84bf93fe0151f330f20077d3b9b4b5af012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd6024730440220170abcd7371d5ac83afff93751e3ea028627d1b38f4bed7da2c897d73d61420402205af8871c7befa740f325deb83483c1450dbcf5388e2836582c8bdaab8bf6a35b012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100b0702718bc906cf8691b7cbc95baf64e0ab3312a2abfd139b1928715757c0dbc022054f45fbc4cd9517eca6da4771dd8c23e5b0d393e61f5b7dd488086fe24029f80012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100d8055802be7aabefc62c6f5da69ee556be064c9727c8eab4d6e26ced2f81472c0220294599ad7b030c908923cb3ae1227b2b3f91275e3c00bc4943cdfe1c5e0b6843012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022011a60277e781e891fce3dc920d0f2a0785b0561eb033c54a15a5c06e7c8bfa5a02207bec23c576fcafc1da59936dd6d35e526d3631f20e089f199245a1c4dcf96bb6012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100c58ee4955ec0d8d84e1b89e4dd4435ab02776dcf5b3b7cb45b837483c157be1002201733ad33460d48e69558bfac47fe045194b3840046c80e4005daff272401d030012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd600000000"; final tr = BtcTransaction.fromRaw(raw); + print(tr.txId()); expect(tx.toHex(), raw); expect(tr.toHex(), raw); From 49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:15:34 -0300 Subject: [PATCH 2/7] chore: deps --- pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 059c368..20e5037 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,9 @@ dependencies: pointycastle: ^3.7.4 bip32: ^2.0.0 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 rxdart: ^0.28.0 intl: ^0.18.0 http: ^1.1.0 From 0fab5761517a6da7b87791fe8065b9312d8fe4c2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 6 Nov 2024 12:06:12 -0300 Subject: [PATCH 3/7] feat: new method strings --- lib/src/provider/service/electrum/methods.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/provider/service/electrum/methods.dart b/lib/src/provider/service/electrum/methods.dart index e5fe2df..bd4121a 100644 --- a/lib/src/provider/service/electrum/methods.dart +++ b/lib/src/provider/service/electrum/methods.dart @@ -56,8 +56,8 @@ 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 String getHistoryMethod = "blockchain.scripthash.get_history"; @@ -81,7 +81,8 @@ 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"; From 1cf5abb68e17284c21936572d685972edaee9866 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 12:22:36 -0300 Subject: [PATCH 4/7] feat: ssl, historical mode --- .../api_provider/electrum_api_provider.dart | 26 +++---------------- .../methods/tweaks_subscribe.dart | 15 ++++++++--- .../provider/service/electrum/electrum.dart | 2 +- .../electrum/electrum_ssl_service.dart | 6 ++++- .../provider/service/electrum/service.dart | 9 +++++++ 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index e9a348e..c9d59cf 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -2,6 +2,8 @@ import 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + typedef ListenerCallback = StreamSubscription Function( void Function(T)? onData, { Function? onError, @@ -32,34 +34,14 @@ class ElectrumApiProvider { } // Preserving generic type T in subscribe method - ListenerCallback? subscribe(ElectrumRequest request) { + BehaviorSubject? subscribe(ElectrumRequest request) { final id = ++_id; final params = request.toRequest(id); final subscription = rpc.subscribe(params); if (subscription == null) return null; - try { - // Create a transformer that uses the request's response handler - final stream = subscription.subscription.map(request.onResponse); - - // Return a properly typed listener callback - return ( - void Function(T)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - }; - } catch (_) { - return null; - } + return subscription.subscription; } Future> getFeeRates() async { diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart index 18cf2ee..61941c4 100644 --- a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -24,7 +24,11 @@ class ElectrumTweaksSubscribeResponse { final int block; final Map blockTweaks; - ElectrumTweaksSubscribeResponse({required this.block, required this.blockTweaks, this.message}); + ElectrumTweaksSubscribeResponse({ + required this.block, + required this.blockTweaks, + this.message, + }); factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { late int block; @@ -77,17 +81,22 @@ class ElectrumTweaksSubscribeResponse { class ElectrumTweaksSubscribe extends ElectrumRequest> { /// blockchain.tweaks.subscribe - ElectrumTweaksSubscribe({required this.height, required this.count}); + 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 toJson() { - return [height, count]; + return [height, count, historicalMode]; } /// The header of the current block chain tip. diff --git a/lib/src/provider/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index 8be4045..bf6e9cb 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -2,6 +2,6 @@ export 'methods.dart'; export 'params.dart'; export 'service.dart'; export 'request_completer.dart'; -// export 'electrum_ssl_service.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 index 57e1cbd..880e4d8 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -85,7 +85,11 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { final Duration connectionTimeOut = const Duration(seconds: 30), void Function(ConnectionStatus)? onConnectionStatusChange, }) async { - final channel = await SecureSocket.connect(uri.host, uri.port).timeout(connectionTimeOut); + final channel = await SecureSocket.connect( + uri.host, + uri.port, + onBadCertificate: (_) => true, + ).timeout(connectionTimeOut); return ElectrumSSLService._( uri.toString(), diff --git a/lib/src/provider/service/electrum/service.dart b/lib/src/provider/service/electrum/service.dart index 972641d..96090d7 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -38,6 +38,15 @@ abstract class BitcoinBaseElectrumRPCService { 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) { From 7b202b8e39f9a15f72f128cd9aa8530a0ceb21c0 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 22 Nov 2024 18:40:20 -0300 Subject: [PATCH 5/7] feat: fix disconnect & find unsupported methods --- .../methods/tweaks_subscribe.dart | 19 +++++++++-- .../electrum/electrum_ssl_service.dart | 32 +++++++++++++++---- .../electrum/electrum_tcp_service.dart | 32 +++++++++++++++---- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart index 61941c4..b2f6bea 100644 --- a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -30,7 +30,20 @@ class ElectrumTweaksSubscribeResponse { this.message, }); - factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { + 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 = {}; @@ -79,7 +92,7 @@ class ElectrumTweaksSubscribeResponse { /// 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> { + extends ElectrumRequest> { /// blockchain.tweaks.subscribe ElectrumTweaksSubscribe({ required this.height, @@ -101,7 +114,7 @@ class ElectrumTweaksSubscribe /// The header of the current block chain tip. @override - ElectrumTweaksSubscribeResponse onResponse(result) { + ElectrumTweaksSubscribeResponse? onResponse(result) { return ElectrumTweaksSubscribeResponse.fromJson(result); } } diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart index 880e4d8..2e3c8ab 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -66,7 +66,7 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { _subscription?.cancel().catchError((e) {}); _subscription = null; - onConnectionStatusChange = null; + _setConnectionStatus(ConnectionStatus.disconnected); } void _onDone() { @@ -138,10 +138,26 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { } void _handleResponse(Map response) { - var id = response['id'] as int?; + 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 == response['method']) { + if (value.request.method == method) { id = key; } }); @@ -149,10 +165,7 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { try { final result = _findResult(response, _tasks[id]!.request); - - if (result != null) { - _finish(id!, result); - } + _finish(id!, result); } catch (_) {} } @@ -186,6 +199,11 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { data: data["error"]?["data"], request: data["request"] ?? request.params, ); + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } } throw _errors[request.id]!; diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart index ea965af..bddecb1 100644 --- a/lib/src/provider/service/electrum/electrum_tcp_service.dart +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -66,7 +66,7 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { _subscription?.cancel().catchError((e) {}); _subscription = null; - onConnectionStatusChange = null; + _setConnectionStatus(ConnectionStatus.disconnected); } void _onDone() { @@ -134,10 +134,26 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { } void _handleResponse(Map response) { - var id = response['id'] as int?; + 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 == response['method']) { + if (value.request.method == method) { id = key; } }); @@ -145,10 +161,7 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { try { final result = _findResult(response, _tasks[id]!.request); - - if (result != null) { - _finish(id!, result); - } + _finish(id!, result); } catch (_) {} } @@ -182,6 +195,11 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { data: data["error"]?["data"], request: data["request"] ?? request.params, ); + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } } throw _errors[request.id]!; From bff40edbeb609fa51fa23e572eb7b6f3e99e65af Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 24 Nov 2024 19:05:44 -0300 Subject: [PATCH 6/7] fix: electrum protocol version --- .../provider/electrum_methods/methods/electrum_version.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index 5fb0e63..d080b7d 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -8,8 +8,7 @@ class ElectrumVersion extends ElectrumRequest, List> { /// 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 From 82306ae21ea247ba400b9dc3823631d69ae45699 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 25 Nov 2024 12:45:22 -0300 Subject: [PATCH 7/7] chore: intl dep --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 20e5037..d14b166 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 rxdart: ^0.28.0 - intl: ^0.18.0 + intl: ^0.19.0 http: ^1.1.0