From bedd81e8f5e457465a8581e112489861bac3bc43 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:10:01 -0300 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 From 5282dee74545e39a04d87326a83073c2a0e0771f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 20 Dec 2024 20:17:51 -0300 Subject: [PATCH 08/22] feat: api config, misc --- .../electrum/electrum_ssl_service.dart | 136 ------------------ .../electrum/electrum_tcp_service.dart | 135 ----------------- .../electrum/electrum_websocket_service.dart | 86 ----------- .../provider/api_provider/api_provider.dart | 9 ++ .../api_provider/electrum_api_provider.dart | 1 + .../electrum_methods/methods/broad_cast.dart | 4 +- lib/src/provider/models/config.dart | 67 +++++---- .../electrum/electrum_ssl_service.dart | 43 +++--- 8 files changed, 76 insertions(+), 405 deletions(-) delete mode 100644 example/lib/services_examples/electrum/electrum_ssl_service.dart delete mode 100644 example/lib/services_examples/electrum/electrum_tcp_service.dart delete mode 100644 example/lib/services_examples/electrum/electrum_websocket_service.dart diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart deleted file mode 100644 index 1313aa7..0000000 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ /dev/null @@ -1,136 +0,0 @@ -/// Simple example how to send request to electurm with tcp - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:rxdart/rxdart.dart'; - -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(_onMessage, onError: _onClose, onDone: _onDone); - } - SecureSocket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - final Map _tasks = {}; - - bool _isDisconnected = false; - @override - bool get isConnected => !_isDisconnected; - - @override - final String url; - - void add(List params) { - if (_isDisconnected) { - throw StateError("socket has been disconnected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDisconnected = true; - - _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 parts = url.split(":"); - final channel = - await SecureSocket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); - - return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); - } - - 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 = _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 completer = AsyncRequestCompleter(params.params); - - try { - _registerTask(params.id, completer.completer); - add(params.toTCPParams()); - final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - _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 deleted file mode 100644 index efc2506..0000000 --- a/example/lib/services_examples/electrum/electrum_tcp_service.dart +++ /dev/null @@ -1,135 +0,0 @@ -/// Simple example how to send request to electurm with tcp - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:rxdart/rxdart.dart'; - -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(_onMessage, onError: _onClose, onDone: _onDone); - } - Socket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - final Map _tasks = {}; - - bool _isDisconnected = false; - @override - bool get isConnected => !_isDisconnected; - - @override - final String url; - - void add(List params) { - if (_isDisconnected) { - throw StateError("socket has been disconnected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDisconnected = true; - - _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 parts = url.split(":"); - final channel = await Socket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); - - return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); - } - - 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 = _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 completer = AsyncRequestCompleter(params.params); - - try { - _registerTask(params.id, completer.completer); - add(params.toTCPParams()); - final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - _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 deleted file mode 100644 index 2ec9bb7..0000000 --- a/example/lib/services_examples/electrum/electrum_websocket_service.dart +++ /dev/null @@ -1,86 +0,0 @@ -/// Simple example how to send request to electurm with websocket -import 'dart:async'; -import 'dart:convert'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/cross_platform_websocket/core.dart'; - -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/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart index 81a09ac..aeaed79 100644 --- a/lib/src/provider/api_provider/api_provider.dart +++ b/lib/src/provider/api_provider/api_provider.dart @@ -133,6 +133,15 @@ class ApiProvider { } } + Future> getBlock( + String blockHash, { + String Function(String)? tokenize, + }) async { + final apiUrl = api.getBlockUrl(blockHash); + final url = tokenize?.call(apiUrl) ?? apiUrl; + return await _getRequest>(url); + } + Future getBlockHeight(int height) async { final url = api.getBlockHeight(height); 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 c9d59cf..f3ff49d 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -15,6 +15,7 @@ class ElectrumApiProvider { final BitcoinBaseElectrumRPCService rpc; ElectrumApiProvider._(this.rpc); int _id = 0; + int get id => _id; Timer? _aliveTimer; static Future connect(Future rpc) async { diff --git a/lib/src/provider/electrum_methods/methods/broad_cast.dart b/lib/src/provider/electrum_methods/methods/broad_cast.dart index 1dfdc1c..11ae936 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -3,7 +3,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Broadcast a transaction to the network. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBroadCastTransaction extends ElectrumRequest { +class ElectrumBroadCastTransaction extends ElectrumRequest { ElectrumBroadCastTransaction({required this.transactionRaw}); /// The raw transaction as a hexadecimal string. @@ -20,7 +20,7 @@ class ElectrumBroadCastTransaction extends ElectrumRequest { /// The transaction hash as a hexadecimal string. @override - String onResponse(result) { + String? onResponse(result) { return result; } } diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 444098e..2920d06 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -5,6 +5,18 @@ import 'package:bitcoin_base/src/provider/constant/constant.dart'; enum APIType { mempool, blockCypher } class APIConfig { + APIConfig({ + required this.url, + required this.feeRate, + required this.transaction, + required this.transactions, + required this.sendTransaction, + required this.apiType, + required this.network, + required this.blockHeight, + this.block, + }); + final String url; final String feeRate; final String transaction; @@ -13,6 +25,7 @@ class APIConfig { final String blockHeight; final APIType apiType; final BasedUtxoNetwork network; + final String? block; factory APIConfig.selectApi(APIType apiType, BasedUtxoNetwork network) { switch (apiType) { @@ -37,6 +50,15 @@ class APIConfig { return baseUrl.replaceAll("###", transactionId); } + String getBlockUrl(String transactionId) { + if (block == null) { + throw const BitcoinBasePluginException("block url is not available"); + } + + String baseUrl = block!; + return baseUrl.replaceAll("###", transactionId); + } + String getTransactionsUrl(String address) { String baseUrl = transactions; return baseUrl.replaceAll("###", address); @@ -71,14 +93,15 @@ class APIConfig { } return APIConfig( - url: "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", - feeRate: baseUrl, - transaction: "$baseUrl/txs/###", - sendTransaction: "$baseUrl/txs/push", - apiType: APIType.blockCypher, - transactions: "$baseUrl/addrs/###/full?limit=200", - network: network, - blockHeight: "$baseUrl/blocks/###"); + url: "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", + feeRate: baseUrl, + transaction: "$baseUrl/txs/###", + sendTransaction: "$baseUrl/txs/push", + apiType: APIType.blockCypher, + transactions: "$baseUrl/addrs/###/full?limit=200", + network: network, + blockHeight: "$baseUrl/blocks/###", + ); } factory APIConfig.mempool(BasedUtxoNetwork network, [String? baseUrl]) { @@ -97,23 +120,15 @@ class APIConfig { } return APIConfig( - url: "$baseUrl/address/###/utxo", - feeRate: "$baseUrl/v1/fees/recommended", - transaction: "$baseUrl/tx/###", - sendTransaction: "$baseUrl/tx", - apiType: APIType.mempool, - transactions: "$baseUrl/address/###/txs", - network: network, - blockHeight: "$baseUrl/block-height/###"); + url: "$baseUrl/address/###/utxo", + feeRate: "$baseUrl/fees/recommended", + transaction: "$baseUrl/tx/###", + sendTransaction: "$baseUrl/tx", + apiType: APIType.mempool, + transactions: "$baseUrl/address/###/txs", + network: network, + blockHeight: "$baseUrl/block-height/###", + block: "$baseUrl/block/###", + ); } - - APIConfig( - {required this.url, - required this.feeRate, - required this.transaction, - required this.transactions, - required this.sendTransaction, - required this.apiType, - required this.network, - required this.blockHeight}); } diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart index 2e3c8ab..0d89497 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -145,10 +145,13 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { if (method == null) { final error = response["error"]; - if (response["error"] != null) { + + if (error != null) { final message = error["message"]; + if (message != null) { final isFulcrum = message.toLowerCase().contains("unsupported request"); + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) .firstMatch(message); method = match?.group(1) ?? ''; @@ -156,17 +159,17 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { } } - _tasks.forEach((key, value) { - if (value.request.method == method) { - id = key; - } - }); + if (id == null && method != null) { + _tasks.forEach((key, value) { + if (value.request.method == method) { + id = key; + } + }); + } } - try { - final result = _findResult(response, _tasks[id]!.request); - _finish(id!, result); - } catch (_) {} + final result = _findResult(response, _tasks[id]!.request); + _finish(id!, result); } void _onMessage(List event) { @@ -182,21 +185,23 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { } dynamic _findResult(dynamic data, ElectrumRequestDetails request) { - if (data["error"] != null) { - if (data["error"] is String) { + final error = data["error"]; + + if (error != null) { + if (error is String) { _errors[request.id] = RPCError( - data: data["error"], + data: error, errorCode: 0, - message: data["error"], + message: error, request: request.params, ); } else { - final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; - final message = data["error"]?['message'] ?? ""; + final code = int.tryParse(((error['code']?.toString()) ?? "0")) ?? 0; + final message = error['message'] ?? ""; _errors[request.id] = RPCError( errorCode: code, message: message, - data: data["error"]?["data"], + data: error["data"], request: data["request"] ?? request.params, ); @@ -205,8 +210,6 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { return {}; } } - - throw _errors[request.id]!; } return data["result"] ?? data["params"]?[0]; @@ -276,5 +279,5 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { } } - String getErrorMessage(int id) => _errors[id]?.data ?? ''; + RPCError? getError(int id) => _errors[id]; } From 5edcb1d726ed3591c6050ce16c04ec02eb2f6125 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 26 Dec 2024 18:52:37 -0300 Subject: [PATCH 09/22] feat: review comment, fee estimation --- lib/src/bitcoin/address/legacy_address.dart | 2 +- lib/src/bitcoin/address/util.dart | 37 ++++++++ lib/src/bitcoin/script/transaction.dart | 2 +- .../provider/api_provider/api_provider.dart | 9 ++ lib/src/provider/models/config.dart | 16 +++- .../transaction_builder.dart | 92 ++++++++++++++++++- 6 files changed, 152 insertions(+), 6 deletions(-) diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index f998c15..caa348d 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -120,7 +120,7 @@ class P2shAddress extends LegacyAddress { required BitcoinDerivationInfo derivationInfo, required bool isChange, required int index, - P2shAddressType type = P2shAddressType.p2pkInP2sh, + P2shAddressType type = P2shAddressType.p2wpkhInP2sh, }) { final fullPath = derivationInfo.derivationPath .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index b878886..b1b1ed0 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -1,4 +1,5 @@ import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.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'; @@ -49,6 +50,42 @@ class BitcoinAddressUtils { return ''; } + static BitcoinAddressType addressTypeFromStr(String address, BasedUtxoNetwork network) { + try { + return P2pkhAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return P2shAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return P2wpkhAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return P2shAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return P2wshAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return P2trAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return MwebAddress.fromAddress(address: address, network: network).type; + } catch (_) {} + + try { + return SilentPaymentAddress.fromAddress(address).type; + } catch (_) {} + + throw Exception('Invalid address'); + } + static String scriptHash(String address, {required BasedUtxoNetwork network}) { final outputScript = addressToOutputScript(address: address, network: network); final parts = BytesUtils.toHexString(QuickCrypto.sha256Hash(outputScript)).split(''); diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index cea48f3..0fa48d5 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -244,7 +244,7 @@ class BtcTransaction { if (mwebBytes != null) { data.add(mwebBytes!); } - data.add([0, 0, 0, 0]); + data.add(locktime); return data.toBytes(); } diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart index aeaed79..a31580b 100644 --- a/lib/src/provider/api_provider/api_provider.dart +++ b/lib/src/provider/api_provider/api_provider.dart @@ -142,6 +142,15 @@ class ApiProvider { return await _getRequest>(url); } + Future> getBlockTimestamp( + DateTime date, { + String Function(String)? tokenize, + }) async { + final apiUrl = api.getBlockTimestampUrl((date.millisecondsSinceEpoch / 1000).round()); + final url = tokenize?.call(apiUrl) ?? apiUrl; + return await _getRequest>(url); + } + Future getBlockHeight(int height) async { final url = api.getBlockHeight(height); final response = await _getRequest(url); diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 2920d06..3421194 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -15,6 +15,7 @@ class APIConfig { required this.network, required this.blockHeight, this.block, + this.blockTimestamp, }); final String url; @@ -26,6 +27,7 @@ class APIConfig { final APIType apiType; final BasedUtxoNetwork network; final String? block; + final String? blockTimestamp; factory APIConfig.selectApi(APIType apiType, BasedUtxoNetwork network) { switch (apiType) { @@ -50,13 +52,22 @@ class APIConfig { return baseUrl.replaceAll("###", transactionId); } - String getBlockUrl(String transactionId) { + String getBlockUrl(String blockHash) { if (block == null) { throw const BitcoinBasePluginException("block url is not available"); } String baseUrl = block!; - return baseUrl.replaceAll("###", transactionId); + return baseUrl.replaceAll("###", blockHash); + } + + String getBlockTimestampUrl(int timestamp) { + if (blockTimestamp == null) { + throw const BitcoinBasePluginException("block timestamp url is not available"); + } + + String baseUrl = blockTimestamp!; + return baseUrl.replaceAll("###", timestamp.toString()); } String getTransactionsUrl(String address) { @@ -129,6 +140,7 @@ class APIConfig { network: network, blockHeight: "$baseUrl/block-height/###", block: "$baseUrl/block/###", + blockTimestamp: "$baseUrl/mining/blocks/timestamp/###", ); } } diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/provider/transaction_builder/transaction_builder.dart index aace073..c6326fa 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/provider/transaction_builder/transaction_builder.dart @@ -124,7 +124,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { ); /// 64 byte schnorr signature length - const String fakeSchnorSignaturBytes = + const String fakeSchnorSignatureBytes = "01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; /// 71 bytes (64 byte signature, 6-7 byte Der encoding length) @@ -134,7 +134,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final transaction = transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { if (utxo.utxo.isP2tr()) { - return fakeSchnorSignaturBytes; + return fakeSchnorSignatureBytes; } else { return fakeECDSASignatureBytes; } @@ -147,6 +147,94 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return size; } + static int estimateTransactionSizeFromTypes({ + required List inputTypes, + required List outputTypes, + required BasedUtxoNetwork network, + String? memo, + bool enableRBF = false, + }) { + final fakePublicKey = ECPrivate.random().getPublic(); + final fakeUtxos = []; + + for (final inputType in inputTypes) { + late BitcoinBaseAddress address; + switch (inputType) { + case P2pkhAddressType.p2pkh: + address = fakePublicKey.toP2pkhAddress(); + break; + case P2shAddressType.p2pkInP2sh: + address = fakePublicKey.toP2wpkhInP2sh(); + break; + case SegwitAddresType.p2wpkh: + address = fakePublicKey.toP2wpkhAddress(); + break; + case P2shAddressType.p2pkhInP2sh: + address = fakePublicKey.toP2pkhInP2sh(); + break; + case SegwitAddresType.p2wsh: + address = fakePublicKey.toP2wshAddress(); + break; + case SegwitAddresType.p2tr: + address = fakePublicKey.toTaprootAddress(); + break; + default: + throw const BitcoinBasePluginException("invalid bitcoin address type"); + } + + final utxo = UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: "0" * 64, + vout: 0, + value: BigInt.from(0), + scriptType: inputType, + ), + ownerDetails: UtxoAddressDetails(publicKey: fakePublicKey.toHex(), address: address), + ); + + fakeUtxos.add(utxo); + } + + final fakeOutputs = []; + + for (final outputType in outputTypes) { + late BitcoinBaseAddress address; + switch (outputType) { + case P2pkhAddressType.p2pkh: + address = fakePublicKey.toP2pkhAddress(); + break; + case P2shAddressType.p2pkInP2sh: + address = fakePublicKey.toP2pkhInP2sh(); + break; + case SegwitAddresType.p2wpkh: + address = fakePublicKey.toP2wpkhAddress(); + break; + case P2shAddressType.p2pkhInP2sh: + address = fakePublicKey.toP2pkhInP2sh(); + break; + case SegwitAddresType.p2wsh: + address = fakePublicKey.toP2wshAddress(); + break; + case SegwitAddresType.p2tr: + case SilentPaymentsAddresType.p2sp: + address = fakePublicKey.toTaprootAddress(); + break; + default: + throw const BitcoinBasePluginException("invalid bitcoin address type"); + } + + fakeOutputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + } + + return estimateTransactionSize( + utxos: fakeUtxos, + outputs: fakeOutputs, + network: network, + memo: memo, + enableRBF: enableRBF, + ); + } + /// HasSegwit checks whether any of the unspent transaction outputs (UTXOs) in the BitcoinTransactionBuilder's /// Utxos list are Segregated Witness (SegWit) UTXOs. It iterates through the Utxos list and returns true if it /// finds any UTXO with a SegWit script type; otherwise, it returns false. From ebee34cd5b81e166ec4c3714dfd2ca39f8cf8bd1 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 27 Dec 2024 10:17:53 -0300 Subject: [PATCH 10/22] refactor: reviews --- example/pubspec.yaml | 5 +- lib/src/bitcoin/address/util.dart | 8 +- lib/src/bitcoin/script/transaction.dart | 1 - .../transaction_builder.dart | 126 ++++++++---------- test/encode_decode_transaction_test.dart | 15 ++- 5 files changed, 76 insertions(+), 79 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 11a339d..bb07685 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,7 +38,10 @@ dependencies: bitcoin_base: path: ../ blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v2 + http: ^1.2.0 dev_dependencies: diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index b1b1ed0..bae1e78 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -34,11 +34,12 @@ class BitcoinAddressUtils { case P2pkhAddressType.p2pkh: return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); case P2shAddressType.p2pkInP2sh: + case P2shAddressType.p2pkhInP2sh: + case P2shAddressType.p2wpkhInP2sh: + case P2shAddressType.p2wshInP2sh: 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: @@ -131,7 +132,10 @@ class BitcoinAddressUtils { switch (scriptType) { case P2pkhAddressType.p2pkh: return BitcoinDerivationInfos.BIP44; + case P2shAddressType.p2pkInP2sh: + case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2wpkhInP2sh: + case P2shAddressType.p2wshInP2sh: return BitcoinDerivationInfos.BIP49; case SegwitAddresType.p2wpkh: if (isElectrum == true) { diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 0fa48d5..4b96fdb 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -147,7 +147,6 @@ class BtcTransaction { if (hasMweb) { mwebBytes = rawtx.sublist(cursor, rawtx.length - 4); } - cursor = rawtx.length - 4; List lock = rawtx.sublist(cursor); return BtcTransaction( diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/provider/transaction_builder/transaction_builder.dart index c6326fa..8ec7400 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/provider/transaction_builder/transaction_builder.dart @@ -155,76 +155,34 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { bool enableRBF = false, }) { final fakePublicKey = ECPrivate.random().getPublic(); - final fakeUtxos = []; - for (final inputType in inputTypes) { - late BitcoinBaseAddress address; - switch (inputType) { - case P2pkhAddressType.p2pkh: - address = fakePublicKey.toP2pkhAddress(); - break; - case P2shAddressType.p2pkInP2sh: - address = fakePublicKey.toP2wpkhInP2sh(); - break; - case SegwitAddresType.p2wpkh: - address = fakePublicKey.toP2wpkhAddress(); - break; - case P2shAddressType.p2pkhInP2sh: - address = fakePublicKey.toP2pkhInP2sh(); - break; - case SegwitAddresType.p2wsh: - address = fakePublicKey.toP2wshAddress(); - break; - case SegwitAddresType.p2tr: - address = fakePublicKey.toTaprootAddress(); - break; - default: - throw const BitcoinBasePluginException("invalid bitcoin address type"); - } - - final utxo = UtxoWithAddress( - utxo: BitcoinUtxo( - txHash: "0" * 64, - vout: 0, - value: BigInt.from(0), - scriptType: inputType, - ), - ownerDetails: UtxoAddressDetails(publicKey: fakePublicKey.toHex(), address: address), - ); - - fakeUtxos.add(utxo); - } - - final fakeOutputs = []; - - for (final outputType in outputTypes) { - late BitcoinBaseAddress address; - switch (outputType) { - case P2pkhAddressType.p2pkh: - address = fakePublicKey.toP2pkhAddress(); - break; - case P2shAddressType.p2pkInP2sh: - address = fakePublicKey.toP2pkhInP2sh(); - break; - case SegwitAddresType.p2wpkh: - address = fakePublicKey.toP2wpkhAddress(); - break; - case P2shAddressType.p2pkhInP2sh: - address = fakePublicKey.toP2pkhInP2sh(); - break; - case SegwitAddresType.p2wsh: - address = fakePublicKey.toP2wshAddress(); - break; - case SegwitAddresType.p2tr: - case SilentPaymentsAddresType.p2sp: - address = fakePublicKey.toTaprootAddress(); - break; - default: - throw const BitcoinBasePluginException("invalid bitcoin address type"); - } - - fakeOutputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); - } + final fakeInputAddress = _fakeAddressesFromTypes(inputTypes); + final fakeUtxos = fakeInputAddress + .map( + (e) => UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: "0" * 64, + vout: 0, + value: BigInt.from(0), + scriptType: e.type, + ), + ownerDetails: UtxoAddressDetails( + publicKey: fakePublicKey.toHex(), + address: e, + ), + ), + ) + .toList(); + + final fakeOutputAddress = _fakeAddressesFromTypes(outputTypes); + final fakeOutputs = fakeOutputAddress + .map( + (e) => BitcoinOutput( + address: e, + value: BigInt.from(0), + ), + ) + .toList(); return estimateTransactionSize( utxos: fakeUtxos, @@ -924,3 +882,33 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } } } + +List _fakeAddressesFromTypes(List types) { + final fakePub = ECPrivate.random().getPublic(); + + return types.map((e) { + switch (e) { + case PubKeyAddressType.p2pk: + return fakePub.toP2pkAddress(); + case P2pkhAddressType.p2pkh: + return fakePub.toP2pkhAddress(); + case P2shAddressType.p2pkInP2sh: + return fakePub.toP2pkInP2sh(); + case P2shAddressType.p2pkhInP2sh: + return fakePub.toP2pkhInP2sh(); + case P2shAddressType.p2wpkhInP2sh: + return fakePub.toP2wpkhInP2sh(); + case P2shAddressType.p2wshInP2sh: + return fakePub.toP2wshInP2sh(); + case SegwitAddresType.p2wpkh: + return fakePub.toP2wpkhAddress(); + case SegwitAddresType.p2wsh: + return fakePub.toP2wshAddress(); + case SegwitAddresType.p2tr: + case SilentPaymentsAddresType.p2sp: + return fakePub.toTaprootAddress(); + default: + throw const BitcoinBasePluginException("invalid bitcoin address type"); + } + }).toList(); +} diff --git a/test/encode_decode_transaction_test.dart b/test/encode_decode_transaction_test.dart index d3588a1..b77dfdb 100644 --- a/test/encode_decode_transaction_test.dart +++ b/test/encode_decode_transaction_test.dart @@ -5,7 +5,8 @@ void main() { test("test1", () { final tx = BtcTransaction(inputs: [ TxInput( - txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: + "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 0, scriptSig: Script( script: [ @@ -15,7 +16,8 @@ void main() { ), ), TxInput( - txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: + "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 2, scriptSig: Script(script: [ "3044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd41", @@ -36,7 +38,8 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: + "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.from(2000), ), @@ -50,7 +53,8 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: + "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.parse("799999999999996000"), ), @@ -65,9 +69,8 @@ void main() { )), ]); const String raw = - "010000000001072b50f5750fc84801618b558e79d8e3c37d2c513c59cdbb590d90d043d3a9aa83000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffbcf5ae0a0c5efd15b06b380d59e535cf4fec65150ad301906012ea796f3a4607000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4be773c0882999ba5ae2ee574951f926a72037637aeda4344fa37655769ed78d000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4b29cb407a116c0c26c73332a3a2dd5e878b3c66dd0bdc516c9b763344cb8119000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff0b76aceb79e3f2570b82fe4ec5709b5affd04ac9bf7898fdee4a5dac5a429c82000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff5469f86d1c2ceb0d7da957f6d82d98f3b0f3e4138800fb9f2fb23444476c4ed7000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffc1fb64b4a9153728ccdfb8f5a2f0d60d42644bb39f0be24408f181c369c9337f000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff028c43e32e00000000160014455980d0b130ff894536fa79a322db815ab9cde4d9efb0000000000017a914424f29a8a84fa867814ff9ded43379c9dc9a6814870248304502210096990ba2bb32d80dc35ee55548bb300237744e9fdf08fcaf918672c60698da7b02202fe20657730114cdcbe01eef860b537be0b34380505d3d891d741ef2344330ca012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022010699f80eb6495d01ce1b00c70dacc76ed8ea5b1652f9cb04b91456d94cfafe30220111ca2bbb5192ec35247cb6250d1a68f84bf93fe0151f330f20077d3b9b4b5af012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd6024730440220170abcd7371d5ac83afff93751e3ea028627d1b38f4bed7da2c897d73d61420402205af8871c7befa740f325deb83483c1450dbcf5388e2836582c8bdaab8bf6a35b012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100b0702718bc906cf8691b7cbc95baf64e0ab3312a2abfd139b1928715757c0dbc022054f45fbc4cd9517eca6da4771dd8c23e5b0d393e61f5b7dd488086fe24029f80012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100d8055802be7aabefc62c6f5da69ee556be064c9727c8eab4d6e26ced2f81472c0220294599ad7b030c908923cb3ae1227b2b3f91275e3c00bc4943cdfe1c5e0b6843012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022011a60277e781e891fce3dc920d0f2a0785b0561eb033c54a15a5c06e7c8bfa5a02207bec23c576fcafc1da59936dd6d35e526d3631f20e089f199245a1c4dcf96bb6012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100c58ee4955ec0d8d84e1b89e4dd4435ab02776dcf5b3b7cb45b837483c157be1002201733ad33460d48e69558bfac47fe045194b3840046c80e4005daff272401d030012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd600000000"; + "0200000002752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada000000006a47304402203795e2aa978afbbd676b36c0edd1a39478744d320c5e02dd6a39d154755a5a3e02205545a7765bae3db9b820b1db9be2f8955a8998fa92ac08b0d7416aa30a3cee42412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada020000006a473044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff03389c9900000000001976a914b8d913342894ec7b066420952a618ec2a8269bc288ace80300000000000048ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10fdd007aa201d12ffe8e85fdab36794cb09418982efdbd5c8cee5fbeb216ac43887ea4817b887e80300000000000044ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10ff60f04fecc22b1a0b76a914b8d913342894ec7b066420952a618ec2a8269bc288ac00000000"; final tr = BtcTransaction.fromRaw(raw); - print(tr.txId()); expect(tx.toHex(), raw); expect(tr.toHex(), raw); From f7ba439a68991eed311f9eedb2d036b3d4981e4d Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 27 Dec 2024 19:10:30 -0300 Subject: [PATCH 11/22] chore: misc --- example/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bb07685..8f9a3cf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,8 +38,8 @@ dependencies: bitcoin_base: path: ../ blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils + git: + url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 http: ^1.2.0 From 98825f14f2f13e225292dd874bb92eddd6c42cba Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 8 Jan 2025 17:21:37 -0300 Subject: [PATCH 12/22] refactor: misc, typos, and fix electrum getFees --- lib/src/bitcoin/address/segwit_address.dart | 2 +- lib/src/bitcoin/address/util.dart | 4 +- .../api_provider/electrum_api_provider.dart | 14 +- .../provider/models/fee_rate/fee_rate.dart | 4 +- .../forked_transaction_builder.dart | 201 ++++++++++-------- lib/src/utils/regex.dart | 2 +- 6 files changed, 126 insertions(+), 101 deletions(-) diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index b45f4b0..994381b 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -201,7 +201,7 @@ class P2wshAddress extends SegwitAddress { class MwebAddress extends SegwitAddress { static RegExp get regex => RegExp(r'(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}'); - factory MwebAddress.fromAddress({required String address, required BasedUtxoNetwork network}) { + factory MwebAddress.fromAddress({required String address}) { final decoded = Bech32DecoderBase.decodeBech32( address, Bech32Const.separator, diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index bae1e78..88746ab 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -21,7 +21,7 @@ class BitcoinAddressUtils { if (addressType.type == SegwitAddresType.mweb) { return BytesUtils.fromHexString( - MwebAddress.fromAddress(address: address, network: network).addressProgram, + MwebAddress.fromAddress(address: address).addressProgram, ); } @@ -77,7 +77,7 @@ class BitcoinAddressUtils { } catch (_) {} try { - return MwebAddress.fromAddress(address: address, network: network).type; + return MwebAddress.fromAddress(address: address).type; } catch (_) {} try { diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index f3ff49d..2df2205 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -1,4 +1,3 @@ -import 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; @@ -50,15 +49,10 @@ class ElectrumApiProvider { 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(); + + final top = (topDoubleString.toInt() / 1000).round(); + final middle = (middleDoubleString.toInt() / 1000).round(); + final bottom = (bottomDoubleString.toInt() / 1000).round(); return [bottom, middle, top]; } catch (_) { diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 37aa345..910a825 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -34,10 +34,10 @@ class BitcoinFeeRate { /// low fee rate in satoshis per kilobyte final BitcoinFee low; - /// only mnenpool api + /// only mempool api final BitcoinFee? economyFee; - /// only mnenpool api + /// only mempool api final BitcoinFee? minimumFee; BitcoinFee _feeRate(BitcoinFeeRateType feeRateType) { diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/provider/transaction_builder/forked_transaction_builder.dart index 3403ffb..7436315 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/provider/transaction_builder/forked_transaction_builder.dart @@ -103,8 +103,8 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { const String fakeECDSASignatureBytes = "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; - final transaction = transactionBuilder - .buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { + final transaction = + transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { return fakeECDSASignatureBytes; }); @@ -114,6 +114,52 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { return size; } + static int estimateTransactionSizeFromTypes({ + required List inputTypes, + required List outputTypes, + required BitcoinCashNetwork network, + String? memo, + bool enableRBF = false, + }) { + final fakePublicKey = ECPrivate.random().getPublic(); + + final fakeInputAddress = _fakeAddressesFromTypes(inputTypes); + final fakeUtxos = fakeInputAddress + .map( + (e) => UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: "0" * 64, + vout: 0, + value: BigInt.from(0), + scriptType: e.type, + ), + ownerDetails: UtxoAddressDetails( + publicKey: fakePublicKey.toHex(), + address: e, + ), + ), + ) + .toList(); + + final fakeOutputAddress = _fakeAddressesFromTypes(outputTypes); + final fakeOutputs = fakeOutputAddress + .map( + (e) => BitcoinOutput( + address: e, + value: BigInt.from(0), + ), + ) + .toList(); + + return estimateTransactionSize( + utxos: fakeUtxos, + outputs: fakeOutputs, + network: network, + memo: memo, + enableRBF: enableRBF, + ); + } + /// It is used to make the appropriate scriptSig Script _buildInputScriptPubKeys(UtxoWithAddress utxo) { if (utxo.isMultiSig()) { @@ -177,8 +223,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { script: scriptPubKeys, amount: utox.utxo.value, token: utox.utxo.token, - sighash: - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); + sighash: BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); } /// buildP2wshOrP2shScriptSig constructs and returns a script signature (represented as a List of strings) @@ -191,8 +236,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { // /// Returns: /// - List: A List of strings representing the script signature for the P2WSH or P2SH input. - List _buildMiltisigUnlockingScript( - List signedDigest, UtxoWithAddress utx) { + List _buildMiltisigUnlockingScript(List signedDigest, UtxoWithAddress utx) { /// The constructed script signature consists of the signed digest elements followed by /// the script details of the multi-signature address. return ['', ...signedDigest, utx.multiSigAddress.multiSigScript.toHex()]; @@ -249,11 +293,10 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } List inputs = sortedUtxos.map((e) => e.utxo.toInput()).toList(); if (enableRBF && inputs.isNotEmpty) { - inputs[0] = inputs[0] - .copyWith(sequence: BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); + inputs[0] = inputs[0].copyWith(sequence: BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); } - return Tuple(List.unmodifiable(inputs), - List.unmodifiable(sortedUtxos)); + return Tuple( + List.unmodifiable(inputs), List.unmodifiable(sortedUtxos)); } List _buildOutputs() { @@ -263,8 +306,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi .toList(); if (memo != null) { - builtOutputs - .add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); + builtOutputs.add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); } if (outputOrdering == BitcoinOrdering.shuffle) { @@ -275,8 +317,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi (a, b) { final valueComparison = a.amount.compareTo(b.amount); if (valueComparison == 0) { - return BytesUtils.compareBytes( - a.scriptPubKey.toBytes(), b.scriptPubKey.toBytes()); + return BytesUtils.compareBytes(a.scriptPubKey.toBytes(), b.scriptPubKey.toBytes()); } return valueComparison; }, @@ -329,11 +370,7 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumOutputAmounts}) { if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { throw BitcoinBasePluginException('Sum value of utxo not spending', - details: { - "inputAmount": sumUtxoAmount, - "fee": fee, - "outputAmount": sumOutputAmounts - }); + details: {"inputAmount": sumUtxoAmount, "fee": fee, "outputAmount": sumOutputAmounts}); } if (!isFakeTransaction) { /// sum of token amounts @@ -347,19 +384,13 @@ be retrieved by anyone who examines the blockchain's history. amount += outputs .whereType() .where((element) => element.categoryID == i.key) - .fold( - BigInt.zero, - (previousValue, element) => - previousValue + (element.value ?? BigInt.zero)); + .fold(BigInt.zero, + (previousValue, element) => previousValue + (element.value ?? BigInt.zero)); if (amount != i.value) { throw BitcoinBasePluginException( 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', - details: { - "token": i.key, - "inputValue": i.value, - "outputValue": amount - }); + details: {"token": i.key, "inputValue": i.value, "outputValue": amount}); } } } @@ -368,16 +399,11 @@ be retrieved by anyone who examines the blockchain's history. final token = i.utxo.token!; if (token.hasAmount) continue; if (!token.hasNFT) continue; - final hasOneoutput = outputs.whereType().any( - (element) => - element.utxoHash == i.utxo.txHash && - element.token.category == token.category); + final hasOneoutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.token.category == token.category); if (hasOneoutput) continue; - final hasBurnableOutput = outputs - .whereType() - .any((element) => - element.utxoHash == i.utxo.txHash && - element.categoryID == token.category); + final hasBurnableOutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.categoryID == token.category); if (hasBurnableOutput) continue; throw BitcoinBasePluginException( 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', @@ -401,12 +427,8 @@ be retrieved by anyone who examines the blockchain's history. final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -420,8 +442,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } continue; } @@ -460,11 +481,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -474,24 +493,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -503,12 +517,10 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } @@ -521,8 +533,7 @@ be retrieved by anyone who examines the blockchain's history. } @override - Future buildTransactionAsync( - BitcoinSignerCallBackAsync sign) async { + Future buildTransactionAsync(BitcoinSignerCallBackAsync sign) async { /// build inputs final sortedInputs = _buildInputs(); @@ -550,11 +561,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -564,24 +573,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = await sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + await sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -593,18 +597,15 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } /// now we need sign the transaction digest - final sig = - await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); + final sig = await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } @@ -624,3 +625,33 @@ be retrieved by anyone who examines the blockchain's history. input.scriptSig = Script(script: scriptSig); } } + +List _fakeAddressesFromTypes(List types) { + final fakePub = ECPrivate.random().getPublic(); + + return types.map((e) { + switch (e) { + case PubKeyAddressType.p2pk: + return fakePub.toP2pkAddress(); + case P2pkhAddressType.p2pkh: + return fakePub.toP2pkhAddress(); + case P2shAddressType.p2pkInP2sh: + return fakePub.toP2pkInP2sh(); + case P2shAddressType.p2pkhInP2sh: + return fakePub.toP2pkhInP2sh(); + case P2shAddressType.p2wpkhInP2sh: + return fakePub.toP2wpkhInP2sh(); + case P2shAddressType.p2wshInP2sh: + return fakePub.toP2wshInP2sh(); + case SegwitAddresType.p2wpkh: + return fakePub.toP2wpkhAddress(); + case SegwitAddresType.p2wsh: + return fakePub.toP2wshAddress(); + case SegwitAddresType.p2tr: + case SilentPaymentsAddresType.p2sp: + return fakePub.toTaprootAddress(); + default: + throw const BitcoinBasePluginException("invalid bitcoin address type"); + } + }).toList(); +} diff --git a/lib/src/utils/regex.dart b/lib/src/utils/regex.dart index 9508d66..b58ed71 100644 --- a/lib/src/utils/regex.dart +++ b/lib/src/utils/regex.dart @@ -32,7 +32,7 @@ class RegexUtils { } else if (stringIsAddress(address, SilentPaymentAddress.regex)) { return SilentPaymentAddress.fromAddress(address); } else if (stringIsAddress(address, MwebAddress.regex)) { - return MwebAddress.fromAddress(address: address, network: network); + return MwebAddress.fromAddress(address: address); } else { return P2wpkhAddress.fromAddress(address: address, network: network); } From 7bd166a16860b82385129d4ed847455c826e4b6e Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 9 Jan 2025 18:38:59 -0300 Subject: [PATCH 13/22] merge from mrtnetwork/bitcoin_base --- CHANGELOG.md | 30 + README.md | 18 +- analysis_options.yaml | 52 + .../lib/bitcoin_cash/burn_token_example.dart | 9 +- .../create_cash_token_example.dart | 9 +- .../lib/bitcoin_cash/create_nft_example.dart | 9 +- .../lib/bitcoin_cash/make_vout0_example.dart | 9 +- .../lib/bitcoin_cash/minting_nft_example.dart | 9 +- .../bitcoin_cash/p2sh32_spend_example.dart | 10 +- .../bitcoin_cash/send_ft_token_example.dart | 9 +- .../bitcoin_cash/transfer_bch_example.dart | 9 +- example/lib/global/bch_example.dart | 9 +- ...amples_multisig_taproot_legacy_uncomp.dart | 138 ++ .../global/old_examples/bitcoin_example.dart | 8 +- .../multi_sig_transactions.dart | 2 +- .../transaction_builder_example.dart | 2 +- ...er_from_7_account_to_6_accout_example.dart | 68 +- .../global/transfer_to_8_account_example.dart | 75 +- example/pubspec.lock | 67 +- example/pubspec.yaml | 2 +- example/test/test_test.dart | 1 + example/test/widget_test.dart | 1 - lib/bitcoin_base.dart | 36 +- lib/src/bitcoin/address/address.dart | 2 +- lib/src/bitcoin/address/core.dart | 81 +- lib/src/bitcoin/address/derivations.dart | 26 +- lib/src/bitcoin/address/legacy_address.dart | 14 +- lib/src/bitcoin/address/network_address.dart | 122 +- lib/src/bitcoin/address/segwit_address.dart | 45 +- lib/src/bitcoin/address/util.dart | 24 +- .../bitcoin/address/utils/address_utils.dart | 55 +- lib/src/bitcoin/script/control_block.dart | 6 +- lib/src/bitcoin/script/input.dart | 107 +- lib/src/bitcoin/script/op_code/constant.dart | 19 +- .../bitcoin/script/op_code/constant_lib.dart | 2 +- lib/src/bitcoin/script/op_code/tools.dart | 38 +- lib/src/bitcoin/script/output.dart | 62 +- lib/src/bitcoin/script/script.dart | 101 +- lib/src/bitcoin/script/sequence.dart | 14 +- lib/src/bitcoin/script/transaction.dart | 246 ++-- lib/src/bitcoin/script/witness.dart | 13 +- lib/src/bitcoin/silent_payments/address.dart | 1 - lib/src/bitcoin/silent_payments/utils.dart | 6 +- lib/src/bitcoin_cash/bcmr.dart | 8 +- lib/src/bitcoin_cash/bcmr_registery.dart | 46 +- lib/src/cash_token/cash_token.dart | 93 +- lib/src/crypto/crypto.dart | 8 +- lib/src/crypto/keypair/ec_private.dart | 31 +- lib/src/crypto/keypair/ec_public.dart | 78 +- lib/src/exception/exception.dart | 8 +- lib/src/models/network.dart | 125 +- lib/src/provider/api_provider.dart | 5 +- .../api_provider/electrum_api_provider.dart | 17 +- lib/src/provider/api_provider/providers.dart | 2 - lib/src/provider/constant/constant.dart | 18 +- .../electrum_methods/methods/add_peer.dart | 6 +- .../methods/block_headers.dart | 7 +- .../electrum_methods/methods/broad_cast.dart | 7 +- .../methods/donate_address.dart | 5 +- .../methods/electrum_version.dart | 7 +- .../methods/estimate_fee.dart | 10 +- .../electrum_methods/methods/get_balance.dart | 8 +- .../methods/get_fee_histogram.dart | 10 +- .../electrum_methods/methods/get_history.dart | 8 +- .../electrum_methods/methods/get_mempool.dart | 8 +- .../electrum_methods/methods/get_merkle.dart | 8 +- .../methods/get_transaction.dart | 7 +- .../electrum_methods/methods/get_unspet.dart | 13 +- .../methods/get_value_proof.dart | 6 +- .../electrum_methods/methods/header.dart | 6 +- .../methods/headers_subscribe.dart | 2 +- .../electrum_methods/methods/id_from_pos.dart | 8 +- .../masternode_announce_broadcast.dart | 6 +- .../methods/masternode_list.dart | 5 +- .../methods/masternode_subscribe.dart | 6 +- .../electrum_methods/methods/ping.dart | 3 +- .../electrum_methods/methods/protx_diff.dart | 6 +- .../electrum_methods/methods/protx_info.dart | 6 +- .../electrum_methods/methods/relay_fee.dart | 3 +- .../methods/scripthash_unsubscribe.dart | 7 +- .../methods/server_banner.dart | 3 +- .../methods/server_features.dart | 3 +- .../methods/server_peer_subscribe.dart | 5 +- .../electrum_methods/methods/status.dart | 8 +- .../block_cypher/block_cypher_models.dart | 2 +- lib/src/provider/models/config.dart | 52 +- .../models/electrum/electrum_utxo.dart | 12 +- .../provider/models/fee_rate/fee_rate.dart | 4 +- .../models/mempool/mempol_models.dart | 2 +- lib/src/provider/models/multisig_script.dart | 52 +- lib/src/provider/models/utxo_details.dart | 114 +- .../explorer.dart} | 41 +- .../electrum/electrum_ssl_service.dart | 2 +- .../electrum/electrum_tcp_service.dart | 4 +- lib/src/provider/service/services.dart | 1 - .../explorer.dart} | 2 +- .../transaction_builder/builder.dart | 0 .../transaction_builder/core.dart | 0 .../forked_transaction_builder.dart | 241 ++-- .../transaction_builder.dart | 240 ++-- lib/src/utils/btc_utils.dart | 11 +- lib/src/utils/enumerate.dart | 2 +- pubspec.yaml | 10 +- test/bcmr/art_collection.dart | 136 +- test/bcmr/bcmr_test.dart | 2 +- test/bcmr/decentralized_application.dart | 116 +- test/bcmr/fungible_token.dart | 52 +- test/bcmr/payouts_or_dividends.dart | 136 +- test/bitcoin_cash_address_test.dart | 336 ++--- test/bitcoin_cash_token_encoding_test.dart | 1119 ++++++++--------- test/deserialize_test.dart | 21 + test/encode_decode_transaction_test.dart | 56 +- test/keys_test.dart | 176 ++- test/p2kh_tr_test.dart | 38 +- test/p2sh_test.dart | 11 +- test/p2tr_test.dart | 136 +- test/p2wpkh_test.dart | 84 +- test/p2wsh_test.dart | 32 +- test/silent_payments.dart | 15 +- 119 files changed, 2925 insertions(+), 2525 deletions(-) create mode 100644 example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart create mode 100644 example/test/test_test.dart delete mode 100644 example/test/widget_test.dart delete mode 100644 lib/src/provider/api_provider/providers.dart rename lib/src/provider/{api_provider/api_provider.dart => providers/explorer.dart} (81%) rename lib/src/provider/{service/http/http_service.dart => services/explorer.dart} (96%) rename lib/src/{provider => }/transaction_builder/builder.dart (100%) rename lib/src/{provider => }/transaction_builder/core.dart (100%) rename lib/src/{provider => }/transaction_builder/forked_transaction_builder.dart (69%) rename lib/src/{provider => }/transaction_builder/transaction_builder.dart (81%) create mode 100644 test/deserialize_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c89343..20e636c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## 5.0.0 +* Update dependencies +* Minimum required Dart SDK version updated to 3.3. + +## 4.9.4 + +* Improved serialization process for large transaction scripts. +* Added support for the Electra network. +* Create and spent from uncompressed public key format. +* Important Notice: This is the final version supporting Dart v2. The next release will require Dart v3.3 or higher. + +## 4.9.2 + +* Update dependencies +* Resolved issue with transaction deserialization (unsigned tx) + +## 4.9.1 + +* Resolved issue with transaction deserialization (Issue #9) + +## 4.9.0 + +* Correct Bitcoin address network configuration. +* Resolve issue with Electrum fee estimation results. + + +## 4.8.0 + +* Update dependencies + ## 4.7.0 * Update dependencies diff --git a/README.md b/README.md index 16ba6b9..6b82768 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li await ElectrumWebSocketService.connect("184...."); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// spender details final privateKey = ECPrivate.fromHex( @@ -310,7 +310,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li for (final i in spenders) { /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account final elctrumUtxos = await provider - .request(ElectrumScriptHashListUnspent(scriptHash: i.pubKeyHash())); + .request(ElectrumRequestScriptHashListUnspent(scriptHash: i.pubKeyHash())); /// Converts all UTXOs to a list of UtxoWithAddress, containing UTXO information along with address details. /// read spender utxos @@ -427,7 +427,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li final raw = transaction.serialize(); /// send to network - await provider.request(ElectrumBroadCastTransaction(transactionRaw: raw)); + await provider.request(ElectrumRequestBroadCastTransaction(transactionRaw: raw)); /// Once completed, we verify the status by checking the mempool or using another explorer to review the transaction details. /// https://mempool.space/testnet/tx/70cf664bba4b5ac9edc6133e9c6891ffaf8a55eaea9d2ac99aceead1c3db8899 @@ -446,7 +446,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -470,7 +470,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: true, )); @@ -566,7 +566,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// send transaction to network await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + .request(ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/97030c1236a024de7cad7ceadf8571833029c508e016bcc8173146317e367ae6 @@ -704,7 +704,7 @@ I haven't implemented any specific HTTP service or socket service within this pl await ElectrumSSLService.connect("testnet.aranguren.org:51002"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); final address = P2trAddress.fromAddress(address: ".....", network: network); @@ -714,7 +714,7 @@ I haven't implemented any specific HTTP service or socket service within this pl /// Return an ordered list of UTXOs sent to a script hash. final accountUnspend = await provider - .request(ElectrumScriptHashListUnspent(scriptHash: address.pubKeyHash())); + .request(ElectrumRequestScriptHashListUnspent(scriptHash: address.pubKeyHash())); /// Return the confirmed and unconfirmed history of a script hash. final accountHistory = await provider @@ -722,7 +722,7 @@ I haven't implemented any specific HTTP service or socket service within this pl /// Broadcast a transaction to the network. final broadcastTransaction = await provider - .request(ElectrumBroadCastTransaction(transactionRaw: "txDigest")); + .request(ElectrumRequestBroadCastTransaction(transactionRaw: "txDigest")); /// .... ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index d429a46..c20153f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,55 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +# Uncomment the following section to specify additional rules. +linter: + rules: + # - always_declare_return_types + # - annotate_overrides + # - avoid_init_to_null + # - avoid_null_checks_in_equality_operators + # - avoid_relative_lib_imports + # - avoid_return_types_on_setters + # - avoid_shadowing_type_parameters + # - avoid_single_cascade_in_expression_statements + # - avoid_types_as_parameter_names + # - await_only_futures + # - camel_case_extensions + # - curly_braces_in_flow_control_structures + # - empty_catches + # - empty_constructor_bodies + # - library_names + # - library_prefixes + # - no_duplicate_case_values + # - null_closures + # - omit_local_variable_types + # - prefer_adjacent_string_concatenation + # - prefer_collection_literals + # - prefer_conditional_assignment + # - prefer_contains + # - prefer_equal_for_default_values + # - prefer_final_fields + # - prefer_for_elements_to_map_fromIterable + # - prefer_generic_function_type_aliases + # - prefer_if_null_operators + # - prefer_inlined_adds + # - prefer_is_empty + # - prefer_is_not_empty + # - prefer_iterable_whereType + # - prefer_single_quotes + # - prefer_spread_collections + # - recursive_getters + # - slash_for_doc_comments + # - sort_child_properties_last + # - type_init_formals + # - unawaited_futures + # - unnecessary_brace_in_string_interps + # - unnecessary_const + # - unnecessary_getters_setters + # - unnecessary_new + # - unnecessary_null_in_if_null_operators + # - unnecessary_this + # - unrelated_type_equality_checks + # - use_function_type_syntax_for_parameters + # - use_rethrow_when_possible + # - valid_regexps \ No newline at end of file diff --git a/example/lib/bitcoin_cash/burn_token_example.dart b/example/lib/bitcoin_cash/burn_token_example.dart index 4acdb9b..a6e854b 100644 --- a/example/lib/bitcoin_cash/burn_token_example.dart +++ b/example/lib/bitcoin_cash/burn_token_example.dart @@ -10,7 +10,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -34,7 +34,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: true, )); @@ -141,8 +142,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/d85da44ba0c12ab8b0f4c636ca5451ae2c3a90b0f6d9e47fe381d0f5c6966ff3 diff --git a/example/lib/bitcoin_cash/create_cash_token_example.dart b/example/lib/bitcoin_cash/create_cash_token_example.dart index c0b8754..b709d66 100644 --- a/example/lib/bitcoin_cash/create_cash_token_example.dart +++ b/example/lib/bitcoin_cash/create_cash_token_example.dart @@ -11,7 +11,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -29,7 +29,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.pubKeyHash(), includeTokens: false, )); @@ -135,8 +136,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/fe0f9f84bd8782b8037160c09a515d39a9cc5bbeda6dcca6fb8a89e952bc9dea diff --git a/example/lib/bitcoin_cash/create_nft_example.dart b/example/lib/bitcoin_cash/create_nft_example.dart index 4db99f2..a1607cc 100644 --- a/example/lib/bitcoin_cash/create_nft_example.dart +++ b/example/lib/bitcoin_cash/create_nft_example.dart @@ -11,7 +11,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -30,7 +30,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: false, )); @@ -121,8 +122,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/4e153029c75963f39920184233756f8f55d5a8f86e01cbdaf0340320c814e25e diff --git a/example/lib/bitcoin_cash/make_vout0_example.dart b/example/lib/bitcoin_cash/make_vout0_example.dart index 6415852..8f8ce76 100644 --- a/example/lib/bitcoin_cash/make_vout0_example.dart +++ b/example/lib/bitcoin_cash/make_vout0_example.dart @@ -12,7 +12,7 @@ void main() async { await ElectrumSSLService.connect("chipnet.imaginary.cash:50002"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -30,7 +30,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.pubKeyHash(), includeTokens: false, )); @@ -77,8 +78,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/b20d4c13fe67adc2f73aee0161eb51c7e813643ddc8eb655c6bd9ae72b7562cb diff --git a/example/lib/bitcoin_cash/minting_nft_example.dart b/example/lib/bitcoin_cash/minting_nft_example.dart index 6c2e1fa..f42865c 100644 --- a/example/lib/bitcoin_cash/minting_nft_example.dart +++ b/example/lib/bitcoin_cash/minting_nft_example.dart @@ -11,7 +11,7 @@ void main() async { await ElectrumWebSocketService.connect("wss://tbch4.loping.net:62004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -30,7 +30,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: true, )); @@ -177,8 +178,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://tbch4.loping.net/tx/caa91b0fea2843a99c3cd7375ac4d3102b6b74a25e52cd866ad7ecc486204f0d diff --git a/example/lib/bitcoin_cash/p2sh32_spend_example.dart b/example/lib/bitcoin_cash/p2sh32_spend_example.dart index c7af331..09b0000 100644 --- a/example/lib/bitcoin_cash/p2sh32_spend_example.dart +++ b/example/lib/bitcoin_cash/p2sh32_spend_example.dart @@ -12,7 +12,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -45,12 +45,12 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. final example1ElectrumUtxos = - await provider.request(ElectrumScriptHashListUnspent( + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2sh32Example1.baseAddress.pubKeyHash(), includeTokens: false, )); final example2ElectrumUtxos = - await provider.request(ElectrumScriptHashListUnspent( + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2sh32Example2.baseAddress.pubKeyHash(), includeTokens: false, )); @@ -111,8 +111,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/b76b851ce0374504591db414d7469aadb68649079defb26e44c62e970afda729 diff --git a/example/lib/bitcoin_cash/send_ft_token_example.dart b/example/lib/bitcoin_cash/send_ft_token_example.dart index 2e9b82d..c220983 100644 --- a/example/lib/bitcoin_cash/send_ft_token_example.dart +++ b/example/lib/bitcoin_cash/send_ft_token_example.dart @@ -11,7 +11,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -35,7 +35,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: true, )); @@ -136,8 +137,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/97030c1236a024de7cad7ceadf8571833029c508e016bcc8173146317e367ae6 diff --git a/example/lib/bitcoin_cash/transfer_bch_example.dart b/example/lib/bitcoin_cash/transfer_bch_example.dart index 12fdbd2..d91aff3 100644 --- a/example/lib/bitcoin_cash/transfer_bch_example.dart +++ b/example/lib/bitcoin_cash/transfer_bch_example.dart @@ -12,7 +12,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -39,7 +39,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: false, )); @@ -96,8 +97,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/9e534f8a64f76b1af5ccf2522392697f2242fd215206a458cfe286bca4a3ec0a diff --git a/example/lib/global/bch_example.dart b/example/lib/global/bch_example.dart index 3f5bdd4..6f0a6d0 100644 --- a/example/lib/global/bch_example.dart +++ b/example/lib/global/bch_example.dart @@ -9,7 +9,7 @@ void main() async { "wss://chipnet.imaginary.cash:50004"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// initialize private key final privateKey = ECPrivate.fromBytes(BytesUtils.fromHexString( @@ -36,7 +36,8 @@ void main() async { /// Reads all UTXOs (Unspent Transaction outputs) associated with the account. /// We does not need tokens utxo and we set to false. - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + final elctrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), includeTokens: false, )); @@ -93,8 +94,8 @@ void main() async { final transactionRaw = transaaction.toHex(); /// send transaction to network - await provider - .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); /// done! check the transaction in block explorer /// https://chipnet.imaginary.cash/tx/9e534f8a64f76b1af5ccf2522392697f2242fd215206a458cfe286bca4a3ec0a diff --git a/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart new file mode 100644 index 0000000..617fa11 --- /dev/null +++ b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart @@ -0,0 +1,138 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:example/services_examples/electrum/electrum_ssl_service.dart'; + +void main() async { + final one = ECPrivate.fromBytes(List.filled(32, 12)); + final two = ECPrivate.fromBytes(List.filled(32, 13)); + final three = ECPrivate.fromBytes(List.filled(32, 14)); + final four = ECPrivate.fromBytes(List.filled(32, 15)); + final five = ECPrivate.fromBytes(List.filled(32, 16)); + final six = ECPrivate.fromBytes(List.filled(32, 17)); + final seven = ECPrivate.fromBytes(List.filled(32, 18)); + final eight = ECPrivate.fromBytes(List.filled(32, 19)); + final Map keys = { + for (final i in [one, two, three, four, five, six, seven, eight]) + i.getPublic().toHex(): i + }; + final account = MultiSignatureAddress(threshold: 8, signers: [ + MultiSignatureSigner( + publicKey: one.getPublic().toHex(mode: PublicKeyType.uncompressed), + weight: 1), + MultiSignatureSigner(publicKey: two.getPublic().toHex(), weight: 1), + MultiSignatureSigner(publicKey: three.getPublic().toHex(), weight: 1), + MultiSignatureSigner( + publicKey: four.getPublic().toHex(mode: PublicKeyType.uncompressed), + weight: 1), + MultiSignatureSigner(publicKey: five.getPublic().toHex(), weight: 1), + MultiSignatureSigner(publicKey: six.getPublic().toHex(), weight: 1), + MultiSignatureSigner( + publicKey: seven.getPublic().toHex(mode: PublicKeyType.uncompressed), + weight: 1), + MultiSignatureSigner(publicKey: eight.getPublic().toHex(), weight: 1), + ]); + + /// connect to electrum service with websocket + /// please see `services_examples` folder for how to create electrum websocket service + final service = await ElectrumSSLService.connect( + "testnet4-electrumx.wakiyamap.dev:51002"); + + /// create provider with service + final provider = ElectrumProvider(service); + + final addrOne = + one.getPublic().toP2pkAddress(mode: PublicKeyType.uncompressed); + + final addrTwo = two.getPublic().toAddress(mode: PublicKeyType.uncompressed); + + final addrThree = + three.getPublic().toP2pkInP2sh(mode: PublicKeyType.uncompressed); + final addrFour = + four.getPublic().toP2pkhInP2sh(mode: PublicKeyType.uncompressed); + final addrFive = four.getPublic().toSegwitAddress(); + final addrSix = account.toP2shAddress(); + final addr7 = eight.getPublic().toTaprootAddress(); + final addr8 = eight.getPublic().toP2wshAddress(); + final addr9 = eight.getPublic().toP2wshInP2sh(); + final List pubkys = [ + one.getPublic().toHex(mode: PublicKeyType.uncompressed), + two.getPublic().toHex(mode: PublicKeyType.uncompressed), + three.getPublic().toHex(mode: PublicKeyType.uncompressed), + four.getPublic().toHex(mode: PublicKeyType.uncompressed), + four.getPublic().toHex(), + four.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + ]; + final addresses = [ + one.getPublic().toP2pkAddress(mode: PublicKeyType.uncompressed), + two.getPublic().toAddress(mode: PublicKeyType.uncompressed), + three.getPublic().toP2pkInP2sh(mode: PublicKeyType.uncompressed), + four.getPublic().toP2pkhInP2sh(mode: PublicKeyType.uncompressed), + four.getPublic().toSegwitAddress(), + four.getPublic().toP2wshInP2sh(), + addrSix, + addr7, + addr8, + addr9 + ]; + List utxos = []; + for (int i = 0; i < addresses.length; i++) { + final address = addresses[i]; + final elctrumUtxos = await provider.request( + ElectrumRequestScriptHashListUnspent( + scriptHash: address.pubKeyHash(), includeTokens: false)); + if (elctrumUtxos.isEmpty) continue; + if (i == 6) { + utxos.addAll(elctrumUtxos.map((e) => UtxoWithAddress( + utxo: e.toUtxo(address.type), + ownerDetails: UtxoAddressDetails.multiSigAddress( + multiSigAddress: account, address: address)))); + continue; + } + utxos.addAll(elctrumUtxos + .map((e) => UtxoWithAddress( + utxo: e.toUtxo(address.type), + ownerDetails: + UtxoAddressDetails(publicKey: pubkys[i], address: address))) + .toList()); + } + + final sumOfUtxo = utxos.sumOfUtxosValue(); + + if (sumOfUtxo == BigInt.zero) { + return; + } + final change = + sumOfUtxo - (BigInt.from(1000) * BigInt.from(11) + BigInt.from(4295)); + final bchTransaction = BitcoinTransactionBuilder(outPuts: [ + /// change input (sumofutxos - spend) + BitcoinOutput(address: addrOne, value: change), + BitcoinOutput(address: addrOne, value: BigInt.from(1000)), + BitcoinOutput(address: addrTwo, value: BigInt.from(1000)), + BitcoinOutput(address: addrThree, value: BigInt.from(1000)), + BitcoinOutput(address: addrFour, value: BigInt.from(1000)), + BitcoinOutput(address: addrFour, value: BigInt.from(1000)), + BitcoinOutput(address: addrFive, value: BigInt.from(1000)), + BitcoinOutput(address: addrSix, value: BigInt.from(1000)), + BitcoinOutput(address: addrSix, value: BigInt.from(1000)), + BitcoinOutput(address: addr7, value: BigInt.from(1000)), + BitcoinOutput(address: addr8, value: BigInt.from(1000)), + BitcoinOutput(address: addr9, value: BigInt.from(1000)), + ], fee: BigInt.from(4295), network: BitcoinNetwork.testnet, utxos: utxos); + final transaaction = + bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { + final pk = ECPublic.fromHex(publicKey); + if (utxo.utxo.isP2tr) { + return keys[pk.toHex()]!.signTapRoot(trDigest, sighash: sighash); + } + return keys[pk.toHex()]!.signInput(trDigest, sigHash: sighash); + }); + + final transactionRaw = transaaction.toHex(); + await provider.request( + ElectrumRequestBroadCastTransaction(transactionRaw: transactionRaw)); +} + +/// https://mempool.space/testnet4/tx/a7f08f07739de45a6a4f8871f8e6ad79e0aefbc940086df76571354ba22263fa diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index a24aaf2..d684cd2 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -158,7 +158,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// and sign the transaction digest to construct the unlocking script. if (publicKey == examplePublicKey2.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return examplePrivateKey.signTapRoot(trDigest); } return examplePrivateKey.signInput(trDigest, sigHash: sighash); @@ -376,19 +376,19 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return childKey1PrivateKey.signTapRoot(trDigest, sighash: sighash); } return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); } if (publicKey == examplePublicKey.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return childKey2PrivateKey.signTapRoot(trDigest, sighash: sighash); } return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return examplePrivateKey.signTapRoot(trDigest, sighash: sighash); } return examplePrivateKey.signInput(trDigest, sigHash: sighash); 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 98d0e9c..cdc9d6c 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 @@ -252,7 +252,7 @@ void main() async { // Ok, now we have the private key, we need to check which method to use for signing // We check whether the UTX corresponds to the P2TR address or not. - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. 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 701f72b..20afaed 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 @@ -200,7 +200,7 @@ void main() async { // Ok, now we have the private key, we need to check which method to use for signing // We check whether the UTX corresponds to the P2TR address or not. - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. diff --git a/example/lib/global/transfer_from_7_account_to_6_accout_example.dart b/example/lib/global/transfer_from_7_account_to_6_accout_example.dart index 37c6e1d..a57a270 100644 --- a/example/lib/global/transfer_from_7_account_to_6_accout_example.dart +++ b/example/lib/global/transfer_from_7_account_to_6_accout_example.dart @@ -15,15 +15,14 @@ import 'package:example/services_examples/electrum/electrum_ssl_service.dart'; void main() async { /// connect to electrum service with websocket /// please see `services_examples` folder for how to create electrum websocket service - final service = - await ElectrumSSLService.connect("testnet.aranguren.org:51002"); + final service = await ElectrumSSLService.connect("testnet.aranguren.org:51002"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// spender details - final privateKey = ECPrivate.fromHex( - "76257aafc9b954351c7f6445b2d07277f681a5e83d515a1f32ebf54989c2af4f"); + final privateKey = + ECPrivate.fromHex("76257aafc9b954351c7f6445b2d07277f681a5e83d515a1f32ebf54989c2af4f"); final examplePublicKey = privateKey.getPublic(); final spender1 = examplePublicKey.toP2pkhAddress(); final spender2 = examplePublicKey.toP2wpkhAddress(); @@ -47,17 +46,16 @@ void main() async { /// loop each spenders address and get utxos and add to accountsUtxos for (final i in spenders) { - /// Reads all UTXOs (Unspent Transaction outputs) associated with the account - final elctrumUtxos = await provider - .request(ElectrumScriptHashListUnspent(scriptHash: i.pubKeyHash())); + /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account + final electrumUtxos = + await provider.request(ElectrumRequestScriptHashListUnspent(scriptHash: i.pubKeyHash())); /// Converts all UTXOs to a list of UtxoWithAddress, containing UTXO information along with address details. /// read spender utxos - final List utxos = elctrumUtxos + final List utxos = electrumUtxos .map((e) => UtxoWithAddress( utxo: e.toUtxo(i.type), - ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), address: i))) + ownerDetails: UtxoAddressDetails(publicKey: examplePublicKey.toHex(), address: i))) .toList(); accountsUtxos.addAll(utxos); } @@ -68,8 +66,8 @@ void main() async { return; } - final examplePublicKey2 = ECPublic.fromHex( - "02d82c9860e36f15d7b72aa59e29347f951277c21cd4d34822acdeeadbcff8a546"); + final examplePublicKey2 = + ECPublic.fromHex("02d82c9860e36f15d7b72aa59e29347f951277c21cd4d34822acdeeadbcff8a546"); /// When creating outputs with an address, I utilize the public key. Alternatively, an address class, such as /// P2pkhAddress.fromAddress(address: ".....", network: network); @@ -77,28 +75,21 @@ void main() async { /// .... final List outputs = [ BitcoinOutput( - address: examplePublicKey2.toP2wpkhAddress(), - value: BtcUtils.toSatoshi("0.00001")), + address: examplePublicKey2.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( - address: examplePublicKey2.toTaprootAddress(), - value: BtcUtils.toSatoshi("0.00001")), + address: examplePublicKey2.toTaprootAddress(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey2.toP2pkhInP2sh(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey2.toP2pkInP2sh(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( - address: examplePublicKey2.toP2pkhInP2sh(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey2.toP2pkInP2sh(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey2.toP2wshAddress(), - value: BtcUtils.toSatoshi("0.00001")), + address: examplePublicKey2.toP2wshAddress(), value: BtcUtils.toSatoshi("0.00001")), ]; /// OP_RETURN const String memo = "https://github.com/mrtnetwork"; /// SUM OF OUTOUT AMOUNTS - final sumOfOutputs = outputs.fold( - BigInt.zero, (previousValue, element) => previousValue + element.value); + final sumOfOutputs = + outputs.fold(BigInt.zero, (previousValue, element) => previousValue + element.value); /// Estimate transaction size int transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( @@ -107,8 +98,7 @@ void main() async { ...outputs, /// I add more output for change value to get correct transaction size - BitcoinOutput( - address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) + BitcoinOutput(address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -121,11 +111,15 @@ void main() async { enableRBF: true); /// get network fee esmtimate (fee per kilobyte) - final networkEstimate = await provider.request(ElectrumEstimateFee()); + final networkEstimate = await provider.request(ElectrumRequestEstimateFee()); + + /// the daemon does not have enough information to make an estimate + if (networkEstimate == null) { + return; + } /// Convert kilobytes to bytes, multiply by the transaction size, and the result yields the transaction fees. - final fee = - BigInt.from(transactionSize) * (networkEstimate ~/ BigInt.from(1000)); + final fee = BigInt.from(transactionSize) * (networkEstimate ~/ BigInt.from(1000)); /// change value final changeValue = sumOfUtxo - (sumOfOutputs + fee); @@ -135,8 +129,7 @@ void main() async { } //// if we have change value we back amount to account if (changeValue > BigInt.zero) { - outputs.add(BitcoinOutput( - address: examplePublicKey2.toP2pkhAddress(), value: changeValue)); + outputs.add(BitcoinOutput(address: examplePublicKey2.toP2pkhAddress(), value: changeValue)); } /// create transaction builder @@ -151,9 +144,8 @@ void main() async { enableRBF: true); /// create transaction and sign it - final transaction = - builder.buildTransaction((trDigest, utxo, publicKey, sighash) { - if (utxo.utxo.isP2tr()) { + final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { + if (utxo.utxo.isP2tr) { return privateKey.signTapRoot(trDigest, sighash: sighash); } return privateKey.signInput(trDigest, sigHash: sighash); @@ -166,7 +158,7 @@ void main() async { final raw = transaction.serialize(); /// send to network - await provider.request(ElectrumBroadCastTransaction(transactionRaw: raw)); + await provider.request(ElectrumRequestBroadCastTransaction(transactionRaw: raw)); /// Once completed, we verify the status by checking the mempool or using another explorer to review the transaction details. /// https://mempool.space/testnet/tx/70cf664bba4b5ac9edc6133e9c6891ffaf8a55eaea9d2ac99aceead1c3db8899 diff --git a/example/lib/global/transfer_to_8_account_example.dart b/example/lib/global/transfer_to_8_account_example.dart index a8f6f6b..822dcb1 100644 --- a/example/lib/global/transfer_to_8_account_example.dart +++ b/example/lib/global/transfer_to_8_account_example.dart @@ -5,11 +5,10 @@ import 'package:example/services_examples/electrum/electrum_ssl_service.dart'; void main() async { /// connect to electrum service with websocket /// please see `services_examples` folder for how to create electrum websocket service - final service = - await ElectrumSSLService.connect("testnet.aranguren.org:51002"); + final service = await ElectrumSSLService.connect("testnet.aranguren.org:51002"); /// create provider with service - final provider = ElectrumApiProvider(service); + final provider = ElectrumProvider(service); /// spender details /// Define another private key from wif @@ -20,22 +19,22 @@ void main() async { final p2pkhAddress = examplePublicKey2.toP2pkhAddress(); /// receiver addresses i use public key for generate address - final examplePublicKey = ECPublic.fromHex( - "032a4f8be9ebffb46e2c6a1c240702553b9c9c8ad9638650833d07d5d22f618621"); + final examplePublicKey = + ECPublic.fromHex("032a4f8be9ebffb46e2c6a1c240702553b9c9c8ad9638650833d07d5d22f618621"); const network = BitcoinNetwork.testnet; - /// Reads all UTXOs (Unspent Transaction outputs) associated with the account - final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( - scriptHash: examplePublicKey2.toP2pkhAddress().pubKeyHash())); + /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account + final electrumUtxos = await provider.request( + ElectrumRequestScriptHashListUnspent(scriptHash: examplePublicKey2.toAddress().pubKeyHash())); /// Converts all UTXOs to a list of UtxoWithAddress, containing UTXO information along with address details. /// read spender utxos - final List utxos = elctrumUtxos + final List utxos = electrumUtxos .map((e) => UtxoWithAddress( utxo: e.toUtxo(p2pkhAddress.type), - ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey2.toHex(), address: p2pkhAddress))) + ownerDetails: + UtxoAddressDetails(publicKey: examplePublicKey2.toHex(), address: p2pkhAddress))) .toList(); /// get sum of values @@ -49,35 +48,23 @@ void main() async { /// P2trAddress.fromAddress(address: "....", network: network) /// .... final List outputs = [ + BitcoinOutput(address: examplePublicKey.toP2pkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( - address: examplePublicKey.toP2pkhAddress(), - value: BtcUtils.toSatoshi("0.00001")), + address: examplePublicKey.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( - address: examplePublicKey.toP2wpkhAddress(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey.toTaprootAddress(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey.toP2pkhInP2sh(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey.toP2pkInP2sh(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey.toP2wshAddress(), - value: BtcUtils.toSatoshi("0.00001")), - BitcoinOutput( - address: examplePublicKey.toP2wpkhInP2sh(), - value: BtcUtils.toSatoshi("0.00001")), + address: examplePublicKey.toTaprootAddress(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey.toP2pkhInP2sh(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey.toP2pkInP2sh(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey.toP2wshAddress(), value: BtcUtils.toSatoshi("0.00001")), + BitcoinOutput(address: examplePublicKey.toP2wpkhInP2sh(), value: BtcUtils.toSatoshi("0.00001")), ]; /// OP_RETURN const String memo = "https://github.com/mrtnetwork"; /// SUM OF OUTOUT AMOUNTS - final sumOfOutputs = outputs.fold( - BigInt.zero, (previousValue, element) => previousValue + element.value); + final sumOfOutputs = + outputs.fold(BigInt.zero, (previousValue, element) => previousValue + element.value); /// ESTIMATE TRANSACTION SIZE int estimateSize = BitcoinTransactionBuilder.estimateTransactionSize( @@ -86,8 +73,7 @@ void main() async { ...outputs, /// I add more output for change value to get correct transaction size - BitcoinOutput( - address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) + BitcoinOutput(address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -100,11 +86,15 @@ void main() async { enableRBF: true); /// get network fee esmtimate (kb/s) - final networkEstimate = await provider.request(ElectrumEstimateFee()); + final networkEstimate = await provider.request(ElectrumRequestEstimateFee()); + + /// the daemon does not have enough information to make an estimate + if (networkEstimate == null) { + return; + } /// kb to bytes and mul with transaction size and now we have fee - final fee = - BigInt.from(estimateSize) * (networkEstimate ~/ BigInt.from(1000)); + final fee = BigInt.from(estimateSize) * (networkEstimate ~/ BigInt.from(1000)); /// change value final changeValue = sumOfUtxo - (sumOfOutputs + fee); @@ -112,8 +102,8 @@ void main() async { if (changeValue.isNegative) return; //// if we have change value we back amount to account if (changeValue > BigInt.zero) { - final changeOutput = BitcoinOutput( - address: examplePublicKey2.toP2pkhAddress(), value: changeValue); + final changeOutput = + BitcoinOutput(address: examplePublicKey2.toP2pkhAddress(), value: changeValue); outputs.add(changeOutput); } @@ -129,9 +119,8 @@ void main() async { enableRBF: true); /// create transaction and sign it - final transaction = - builder.buildTransaction((trDigest, utxo, publicKey, sighash) { - if (utxo.utxo.isP2tr()) { + final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { + if (utxo.utxo.isP2tr) { return examplePrivateKey2.signTapRoot(trDigest, sighash: sighash); } return examplePrivateKey2.signInput(trDigest, sigHash: sighash); @@ -144,7 +133,7 @@ void main() async { final raw = transaction.serialize(); /// send to network - await provider.request(ElectrumBroadCastTransaction(transactionRaw: raw)); + await provider.request(ElectrumRequestBroadCastTransaction(transactionRaw: raw)); /// Once completed, we verify the status by checking the mempool or using another explorer to review the transaction details. /// https://mempool.space/testnet/tx/abab018f3d2b92bf30c63b4aca419cf6d6571692b3620f06311c7e5a21a88b56 diff --git a/example/pubspec.lock b/example/pubspec.lock index 0aa5320..2ade285 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -23,14 +23,16 @@ packages: path: ".." relative: true source: path - version: "4.7.0" + version: "5.0.0" blockchain_utils: dependency: "direct main" description: - path: "/home/rafael/Working/blockchain_utils/" - relative: false - source: path - version: "3.3.0" + path: "." + ref: cake-update-v4 + resolved-ref: "8a39e4342c15a4476c6ee57b5efba46452130868" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "4.0.0" boolean_selector: dependency: transitive description: @@ -75,26 +77,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -133,10 +135,10 @@ packages: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http_parser: dependency: transitive description: @@ -149,10 +151,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" js: dependency: transitive description: @@ -165,26 +167,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -205,18 +207,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" path: dependency: transitive description: @@ -290,10 +292,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" typed_data: dependency: transitive description: @@ -314,17 +316,18 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" web: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "1.1.0" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8f9a3cf..a192cc3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + ref: cake-update-v4 http: ^1.2.0 diff --git a/example/test/test_test.dart b/example/test/test_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/example/test/test_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index f76cc54..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1 +0,0 @@ -void main() async {} diff --git a/lib/bitcoin_base.dart b/lib/bitcoin_base.dart index a2c93a8..4a5869b 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -4,30 +4,38 @@ /// including spending transactions, Bitcoin address management, /// Bitcoin Schnorr signatures, BIP-39 mnemonic phrase generation, /// hierarchical deterministic (HD) wallet derivation, and Web3 Secret Storage Definition. -library bitcoin_base; +library; -export 'package:bitcoin_base/src/bitcoin/address/address.dart'; +export 'src/bitcoin/address/address.dart'; -export 'package:bitcoin_base/src/bitcoin/address/util.dart'; +export 'src/bitcoin/address/util.dart'; -export 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +export 'src/bitcoin/script/scripts.dart'; -export 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; +export 'src/bitcoin/amount/amount.dart'; -export 'package:bitcoin_base/src/crypto/crypto.dart'; +export 'src/crypto/crypto.dart'; -export 'package:bitcoin_base/src/models/network.dart'; +export 'src/bitcoin/script/scripts.dart'; -export 'package:bitcoin_base/src/provider/api_provider.dart'; +export 'src/crypto/crypto.dart'; -export 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; +export 'src/models/network.dart'; -export 'package:bitcoin_base/src/utils/utils.dart'; +export 'src/provider/api_provider.dart'; -export 'package:bitcoin_base/src/cash_token/cash_token.dart'; +export 'src/provider/models/electrum/electrum_utxo.dart'; -export 'package:bitcoin_base/src/bitcoin_cash/bitcoin_cash.dart'; +export 'src/utils/utils.dart'; -export 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; +export 'src/utils/btc_utils.dart'; -export 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; +export 'src/cash_token/cash_token.dart'; + +export 'src/bitcoin_cash/bitcoin_cash.dart'; + +export 'src/transaction_builder/builder.dart'; + +export 'src/bitcoin/silent_payments/silent_payments.dart'; + +export 'src/bitcoin/script/op_code/constant.dart'; diff --git a/lib/src/bitcoin/address/address.dart b/lib/src/bitcoin/address/address.dart index c644cf8..cc4760e 100644 --- a/lib/src/bitcoin/address/address.dart +++ b/lib/src/bitcoin/address/address.dart @@ -6,7 +6,7 @@ // - Utility functions for address manipulation. // - encode/decode Segregated Witness (SegWit) address implementation. // - Enhanced functionality for improved handling of addresses across diverse networks. -library bitcoin_base.address; +library; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index 28bfdb6..8756f86 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -9,7 +9,7 @@ abstract class BitcoinAddressType implements Enumerate { /// Factory method to create a BitcoinAddressType enum value from a name or value. static BitcoinAddressType fromValue(String value) { return values.firstWhere((element) => element.value == value, - orElse: () => throw BitcoinBasePluginException('Invalid BitcoinAddressType: $value')); + orElse: () => throw DartBitcoinPluginException('Invalid BitcoinAddressType: $value')); } static BitcoinAddressType fromAddress(BitcoinBaseAddress address) { @@ -18,16 +18,16 @@ abstract class BitcoinAddressType implements Enumerate { } else if (address is P2shAddress) { return P2shAddressType.p2wpkhInP2sh; } else if (address is P2wshAddress) { - return SegwitAddresType.p2wsh; + return SegwitAddressType.p2wsh; } else if (address is P2trAddress) { - return SegwitAddresType.p2tr; + return SegwitAddressType.p2tr; } else if (address is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else if (address is P2wpkhAddress) { - return SegwitAddresType.p2wpkh; + return SegwitAddressType.p2wpkh; } - throw BitcoinBasePluginException('Invalid BitcoinAddressType: $address'); + throw DartBitcoinPluginException('Invalid BitcoinAddressType: $address'); } /// Check if the address type is Pay-to-Script-Hash (P2SH). @@ -38,10 +38,10 @@ abstract class BitcoinAddressType implements Enumerate { // Enum values as a list for iteration static const List values = [ P2pkhAddressType.p2pkh, - SegwitAddresType.p2wpkh, - SegwitAddresType.p2tr, - SegwitAddresType.p2wsh, - SegwitAddresType.mweb, + SegwitAddressType.p2wpkh, + SegwitAddressType.p2tr, + SegwitAddressType.p2wsh, + SegwitAddressType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -55,6 +55,14 @@ abstract class BitcoinAddressType implements Enumerate { P2pkhAddressType.p2pkhwt, SilentPaymentsAddresType.p2sp ]; + T cast() { + if (this is! T) { + throw DartBitcoinPluginException('BitcoinAddressType casting failed.', + details: {'excepted': '$T', 'type': value}); + } + return this as T; + } + @override String toString() => value; } @@ -95,46 +103,42 @@ abstract class BitcoinBaseAddress { return P2wpkhAddress.fromAddress(address: address, network: network); } - throw BitcoinBasePluginException('Invalid BitcoinBaseAddress: $address'); + throw DartBitcoinPluginException('Invalid BitcoinBaseAddress: $address'); } } -class PubKeyAddressType implements BitcoinAddressType { - const PubKeyAddressType._(this.value); - static const PubKeyAddressType p2pk = PubKeyAddressType._("P2PK"); +class PubKeyAddressType extends BitcoinAddressType { + const PubKeyAddressType._(super.value) : super._(); + static const PubKeyAddressType p2pk = PubKeyAddressType._('P2PK'); @override bool get isP2sh => false; @override bool get isSegwit => false; - @override - final String value; + @override int get hashLength => 20; @override String toString() => value; } -class P2pkhAddressType implements BitcoinAddressType { - const P2pkhAddressType._(this.value); - static const P2pkhAddressType p2pkh = P2pkhAddressType._("P2PKH"); - static const P2pkhAddressType p2pkhwt = P2pkhAddressType._("P2PKHWT"); +class P2pkhAddressType extends BitcoinAddressType { + const P2pkhAddressType._(super.value) : super._(); + static const P2pkhAddressType p2pkh = P2pkhAddressType._('P2PKH'); + static const P2pkhAddressType p2pkhwt = P2pkhAddressType._('P2PKHWT'); @override bool get isP2sh => false; @override bool get isSegwit => false; - @override - final String value; - @override int get hashLength => 20; @override String toString() => value; } -class P2shAddressType implements BitcoinAddressType { - const P2shAddressType._(this.value, this.hashLength, this.withToken); +class P2shAddressType extends BitcoinAddressType { + const P2shAddressType._(super.value, this.hashLength, this.withToken) : super._(); static const P2shAddressType p2wshInP2sh = P2shAddressType._("P2SH/P2WSH", _BitcoinAddressUtils.hash160DigestLength, false); static const P2shAddressType p2wpkhInP2sh = @@ -176,33 +180,27 @@ class P2shAddressType implements BitcoinAddressType { static const P2shAddressType p2pkInP2shwt = P2shAddressType._("P2SHWT/P2PK", _BitcoinAddressUtils.hash160DigestLength, true); - @override - final String value; - @override String toString() => value; } -class SegwitAddresType implements BitcoinAddressType { - const SegwitAddresType._(this.value); - static const SegwitAddresType p2wpkh = SegwitAddresType._("P2WPKH"); - static const SegwitAddresType p2tr = SegwitAddresType._("P2TR"); - static const SegwitAddresType p2wsh = SegwitAddresType._("P2WSH"); - static const SegwitAddresType mweb = SegwitAddresType._("MWEB"); +class SegwitAddressType extends BitcoinAddressType { + const SegwitAddressType._(super.value) : super._(); + static const SegwitAddressType p2wpkh = SegwitAddressType._("P2WPKH"); + static const SegwitAddressType p2tr = SegwitAddressType._("P2TR"); + static const SegwitAddressType p2wsh = SegwitAddressType._("P2WSH"); + static const SegwitAddressType mweb = SegwitAddressType._("MWEB"); @override bool get isP2sh => false; @override bool get isSegwit => true; - @override - final String value; - @override int get hashLength { switch (this) { - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return 20; - case SegwitAddresType.mweb: + case SegwitAddressType.mweb: return 66; default: return 32; @@ -213,17 +211,14 @@ class SegwitAddresType implements BitcoinAddressType { String toString() => value; } -class SilentPaymentsAddresType implements BitcoinAddressType { - const SilentPaymentsAddresType._(this.value); +class SilentPaymentsAddresType extends BitcoinAddressType { + const SilentPaymentsAddresType._(super.value) : super._(); static const SilentPaymentsAddresType p2sp = SilentPaymentsAddresType._("P2SP"); @override bool get isP2sh => false; @override bool get isSegwit => true; - @override - final String value; - @override int get hashLength { return 32; diff --git a/lib/src/bitcoin/address/derivations.dart b/lib/src/bitcoin/address/derivations.dart index 303a889..3b3510f 100644 --- a/lib/src/bitcoin/address/derivations.dart +++ b/lib/src/bitcoin/address/derivations.dart @@ -64,7 +64,7 @@ abstract class BitcoinDerivationInfos { derivationType: BitcoinDerivationType.electrum, derivationPath: BitcoinDerivationPaths.ELECTRUM, description: "Electrum", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ); static final BitcoinDerivationInfo BIP44 = BitcoinDerivationInfo( @@ -83,20 +83,20 @@ abstract class BitcoinDerivationInfos { derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP84, description: "Standard BIP84 native segwit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ); static final BitcoinDerivationInfo BIP86 = BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP86, description: "Standard BIP86 Taproot", - scriptType: SegwitAddresType.p2tr, + scriptType: SegwitAddressType.p2tr, ); static final BitcoinDerivationInfo LITECOIN = BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.LITECOIN, description: "Default Litecoin", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ); static final BitcoinDerivationInfo SILENT_PAYMENTS_SCAN = BitcoinDerivationInfo( @@ -133,13 +133,13 @@ final Map> BITCOIN_DERIVATION derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP84, description: "Standard BIP84 native segwit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP86, description: "Standard BIP86 Taproot", - scriptType: SegwitAddresType.p2tr, + scriptType: SegwitAddressType.p2tr, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, @@ -157,37 +157,37 @@ final Map> BITCOIN_DERIVATION derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.NON_STANDARD, description: "Non-standard native segwit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP44, description: "Samourai Deposit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.BIP49, description: "Samourai Deposit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.SAMOURAI_BAD_BANK, description: "Samourai Bad Bank (toxic change)", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_PREMIX, description: "Samourai Whirlpool Pre Mix", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_POSTMIX, description: "Samourai Whirlpool Post Mix", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfo( derivationType: BitcoinDerivationType.bip39, @@ -205,7 +205,7 @@ final Map> BITCOIN_DERIVATION derivationType: BitcoinDerivationType.bip39, derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_NATIVE_SEGWIT, description: "Samourai Ricochet native segwit", - scriptType: SegwitAddresType.p2wpkh, + scriptType: SegwitAddressType.p2wpkh, ), BitcoinDerivationInfos.LITECOIN, BitcoinDerivationInfos.SILENT_PAYMENTS_SCAN, diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index caa348d..4e68cbe 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -21,7 +21,7 @@ abstract class LegacyAddress extends BitcoinBaseAddress { ); if (decode == null) { - throw BitcoinBasePluginException("Invalid ${network.conf.coinName} address"); + throw DartBitcoinPluginException('Invalid ${network.conf.coinName} address'); } _addressProgram = decode; @@ -40,12 +40,12 @@ abstract class LegacyAddress extends BitcoinBaseAddress { _signature = script.findScriptParam(0); break; case P2pkhAddressType.p2pkh: - if (script.script.length != 2) throw ArgumentError('Input is invalid'); + if (script.script.length != 2) throw DartBitcoinPluginException('Input is invalid'); _signature = script.findScriptParam(0); if (!isCanonicalScriptSignature(BytesUtils.fromHexString(_signature!))) { - throw ArgumentError('Input has invalid signature'); + throw DartBitcoinPluginException('Input has invalid signature'); } _pubkey = ECPublic.fromHex(script.findScriptParam(1)); @@ -80,7 +80,7 @@ abstract class LegacyAddress extends BitcoinBaseAddress { @override String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException("network does not support ${type.value} address"); + throw DartBitcoinPluginException("network does not support ${type.value} address"); } return _BitcoinAddressUtils.legacyToAddress( @@ -150,7 +150,7 @@ class P2shAddress extends LegacyAddress { type = P2shAddressType.p2pkInP2sh, }) { if (script.getAddressType() is! P2shAddressType) { - throw ArgumentError("Invalid scriptPubKey"); + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2shAddress.fromHash160(h160: script.findScriptParam(1), type: type); @@ -182,7 +182,7 @@ class P2pkhAddress extends LegacyAddress { P2pkhAddressType type = P2pkhAddressType.p2pkh, }) { if (script.getAddressType() != P2pkhAddressType.p2pkh) { - throw ArgumentError("Invalid scriptPubKey"); + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2pkhAddress.fromHash160(h160: script.findScriptParam(2), type: type); @@ -248,7 +248,7 @@ class P2pkAddress extends LegacyAddress { factory P2pkAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() is! PubKeyAddressType) { - throw ArgumentError("Invalid scriptPubKey"); + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2pkAddress.fromPubkey(pubkey: ECPublic.fromHex(script.script[0])); diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index bed4732..2a8e1bb 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -2,96 +2,88 @@ part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; /// An abstract class representing a forked address for a specific network. abstract class BitcoinNetworkAddress { - const BitcoinNetworkAddress(); + const BitcoinNetworkAddress._( + {required this.address, required this.network, required this.baseAddress}); /// The underlying Bitcoin base address. - abstract final BitcoinBaseAddress baseAddress; + final BitcoinBaseAddress baseAddress; /// Converts the address to a string representation for the specified network [T]. - String toAddress([T? network]) { - return network == null ? address : baseAddress.toAddress(network); + String toAddress([T? updateNetwork]) { + return updateNetwork == null ? address : baseAddress.toAddress(updateNetwork); } /// The type of the Bitcoin address. BitcoinAddressType get type => baseAddress.type; /// The string representation of the address. - abstract final String address; + final String address; + + final T network; } /// A concrete implementation of [BitcoinNetworkAddress] for Bitcoin network. class BitcoinAddress extends BitcoinNetworkAddress { - const BitcoinAddress._(this.baseAddress, this.address); + const BitcoinAddress._(BitcoinBaseAddress baseAddress, String address, BitcoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinAddress(String address, {BitcoinNetwork network = BitcoinNetwork.mainnet}) { - return BitcoinAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return BitcoinAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory BitcoinAddress.fromBaseAddress(BitcoinBaseAddress address, - {DashNetwork network = DashNetwork.mainnet}) { + {BitcoinNetwork network = BitcoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Doge network. class DogeAddress extends BitcoinNetworkAddress { - const DogeAddress._(this.baseAddress, this.address); + const DogeAddress._(BitcoinBaseAddress baseAddress, String address, DogecoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory DogeAddress(String address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { - return DogeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return DogeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory DogeAddress.fromBaseAddress(BitcoinBaseAddress address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DogeAddress._(baseAddress, baseAddress.toAddress(network)); + return DogeAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Pepecoin network. class PepeAddress extends BitcoinNetworkAddress { - const PepeAddress._(this.baseAddress, this.address); + const PepeAddress._(BitcoinBaseAddress baseAddress, String address, PepeNetwork network) + : super._(address: address, network: network, baseAddress: baseAddress); factory PepeAddress(String address, {PepeNetwork network = PepeNetwork.mainnet}) { - return PepeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return PepeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory PepeAddress.fromBaseAddress(BitcoinBaseAddress address, {PepeNetwork network = PepeNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return PepeAddress._(baseAddress, baseAddress.toAddress(network)); + return PepeAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Litecoin network. class LitecoinAddress extends BitcoinNetworkAddress { - LitecoinAddress._(this.baseAddress, this.address); + const LitecoinAddress._(BitcoinBaseAddress baseAddress, String address, LitecoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory LitecoinAddress(String address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { - return LitecoinAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return LitecoinAddress._( + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory LitecoinAddress.fromBaseAddress(BitcoinBaseAddress address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return LitecoinAddress._(baseAddress, baseAddress.toAddress(network)); + return LitecoinAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Bitcoin cash network. class BitcoinCashAddress extends BitcoinNetworkAddress { - const BitcoinCashAddress._(this.baseAddress, this.address); + const BitcoinCashAddress._( + BitcoinBaseAddress baseAddress, String address, BitcoinCashNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinCashAddress(String address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet, bool validateNetworkPrefix = false}) { @@ -101,59 +93,67 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { validateNetworkHRP: validateNetworkPrefix, ); if (decodeAddress == null) { - throw BitcoinBasePluginException("Invalid ${network.value} address."); + throw DartBitcoinPluginException('Invalid ${network.value} address.'); } - return BitcoinCashAddress._(decodeAddress, address); + return BitcoinCashAddress._(decodeAddress, address, network); } factory BitcoinCashAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinCashAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinCashAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; @override - String toAddress([BitcoinCashNetwork? network, String? prefix]) { + String toAddress([BitcoinCashNetwork? updateNetwork, String? prefix]) { if (prefix != null) { return BchAddrConverter.convert(address, prefix, null); } - return super.toAddress(network); + return super.toAddress(updateNetwork); } } /// A concrete implementation of [BitcoinNetworkAddress] for Dash network. class DashAddress extends BitcoinNetworkAddress { - const DashAddress._(this.baseAddress, this.address); + const DashAddress._(BitcoinBaseAddress baseAddress, String address, DashNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory DashAddress(String address, {DashNetwork network = DashNetwork.mainnet}) { - return DashAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return DashAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory DashAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DashAddress._(baseAddress, baseAddress.toAddress(network)); + return DashAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for bitcoinSV network. -class BitcoinSVAddress extends BitcoinNetworkAddress { - const BitcoinSVAddress._(this.baseAddress, this.address); +class BitcoinSVAddress extends BitcoinNetworkAddress { + const BitcoinSVAddress._(BitcoinBaseAddress baseAddress, String address, BitcoinSVNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinSVAddress(String address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { - return BitcoinSVAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); + return BitcoinSVAddress._( + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory BitcoinSVAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinSVAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinSVAddress._(baseAddress, baseAddress.toAddress(network), network); + } +} + +/// A concrete implementation of [BitcoinNetworkAddress] for Electra protocol network. +class ElectraProtocolAddress extends BitcoinNetworkAddress { + const ElectraProtocolAddress._( + BitcoinBaseAddress baseAddress, String address, ElectraProtocolNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); + factory ElectraProtocolAddress(String address, + {ElectraProtocolNetwork network = ElectraProtocolNetwork.mainnet}) { + return ElectraProtocolAddress._( + _BitcoinAddressUtils.decodeAddress(address, network), address, network); + } + factory ElectraProtocolAddress.fromBaseAddress(BitcoinBaseAddress address, + {ElectraProtocolNetwork network = ElectraProtocolNetwork.mainnet}) { + final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); + return ElectraProtocolAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index 994381b..f07590e 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -15,7 +15,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { SegwitAddress.fromProgram({ required String program, - required SegwitAddresType addressType, + required SegwitAddressType addressType, required this.segwitVersion, this.pubkey, }) : addressProgram = _BitcoinAddressUtils.validateAddressProgram(program, addressType), @@ -34,7 +34,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { @override String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException("network does not support ${type.value} address"); + throw DartBitcoinPluginException("network does not support ${type.value} address"); } return _BitcoinAddressUtils.segwitToAddress( @@ -59,7 +59,7 @@ class P2wpkhAddress extends SegwitAddress { P2wpkhAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, - addressType: SegwitAddresType.p2wpkh, + addressType: SegwitAddressType.p2wpkh, ); P2wpkhAddress.fromRedeemScript({required super.script}) @@ -83,8 +83,8 @@ class P2wpkhAddress extends SegwitAddress { } factory P2wpkhAddress.fromScriptPubkey({required Script script}) { - if (script.getAddressType() != SegwitAddresType.p2wpkh) { - throw ArgumentError("Invalid scriptPubKey"); + if (script.getAddressType() != SegwitAddressType.p2wpkh) { + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2wpkhAddress.fromProgram(program: script.findScriptParam(1)); @@ -98,7 +98,7 @@ class P2wpkhAddress extends SegwitAddress { /// returns the type of address @override - SegwitAddresType get type => SegwitAddresType.p2wpkh; + SegwitAddressType get type => SegwitAddressType.p2wpkh; } class P2trAddress extends SegwitAddress { @@ -111,7 +111,7 @@ class P2trAddress extends SegwitAddress { P2trAddress.fromProgram({required super.program, super.pubkey}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV1, - addressType: SegwitAddresType.p2tr, + addressType: SegwitAddressType.p2tr, ); P2trAddress.fromRedeemScript({required super.script}) @@ -134,8 +134,8 @@ class P2trAddress extends SegwitAddress { } factory P2trAddress.fromScriptPubkey({required Script script}) { - if (script.getAddressType() != SegwitAddresType.p2tr) { - throw ArgumentError("Invalid scriptPubKey"); + if (script.getAddressType() != SegwitAddressType.p2tr) { + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2trAddress.fromProgram(program: script.findScriptParam(1)); @@ -149,7 +149,7 @@ class P2trAddress extends SegwitAddress { /// returns the type of address @override - SegwitAddresType get type => SegwitAddresType.p2tr; + SegwitAddressType get type => SegwitAddressType.p2tr; } class P2wshAddress extends SegwitAddress { @@ -161,7 +161,7 @@ class P2wshAddress extends SegwitAddress { P2wshAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, - addressType: SegwitAddresType.p2wsh, + addressType: SegwitAddressType.p2wsh, ); P2wshAddress.fromRedeemScript({required super.script}) @@ -180,8 +180,8 @@ class P2wshAddress extends SegwitAddress { } factory P2wshAddress.fromScriptPubkey({required Script script}) { - if (script.getAddressType() != SegwitAddresType.p2wsh) { - throw ArgumentError("Invalid scriptPubKey"); + if (script.getAddressType() != SegwitAddressType.p2wsh) { + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return P2wshAddress.fromProgram(program: script.findScriptParam(1)); @@ -195,7 +195,7 @@ class P2wshAddress extends SegwitAddress { /// Returns the type of address @override - SegwitAddresType get type => SegwitAddresType.p2wsh; + SegwitAddressType get type => SegwitAddressType.p2wsh; } class MwebAddress extends SegwitAddress { @@ -210,14 +210,15 @@ class MwebAddress extends SegwitAddress { final hrp = decoded.item1; final data = decoded.item2; if (hrp != 'ltcmweb') { - throw ArgumentException('Invalid format (HRP not valid, expected ltcmweb, got $hrp)'); + throw DartBitcoinPluginException( + 'Invalid format (HRP not valid, expected ltcmweb, got $hrp)'); } if (data[0] != _BitcoinAddressUtils.segwitV0) { - throw const ArgumentException("Invalid segwit version"); + throw DartBitcoinPluginException("Invalid segwit version"); } final convData = Bech32BaseUtils.convertFromBase32(data.sublist(1)); if (convData.length != 66) { - throw ArgumentException( + throw DartBitcoinPluginException( 'Invalid format (witness program length not valid: ${convData.length})'); } @@ -227,14 +228,14 @@ class MwebAddress extends SegwitAddress { MwebAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, - addressType: SegwitAddresType.mweb, + addressType: SegwitAddressType.mweb, ); MwebAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); - factory MwebAddress.fromScriptPubkey({required Script script, type = SegwitAddresType.mweb}) { - if (script.getAddressType() != SegwitAddresType.mweb) { - throw ArgumentError("Invalid scriptPubKey"); + factory MwebAddress.fromScriptPubkey({required Script script, type = SegwitAddressType.mweb}) { + if (script.getAddressType() != SegwitAddressType.mweb) { + throw DartBitcoinPluginException("Invalid scriptPubKey"); } return MwebAddress.fromProgram(program: BytesUtils.toHexString(script.script as List)); } @@ -247,5 +248,5 @@ class MwebAddress extends SegwitAddress { /// returns the type of address @override - SegwitAddresType get type => SegwitAddresType.mweb; + SegwitAddressType get type => SegwitAddressType.mweb; } diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index 88746ab..1208b82 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -19,7 +19,7 @@ class BitcoinAddressUtils { {required String address, required BasedUtxoNetwork network}) { final addressType = RegexUtils.addressTypeFromStr(address, network); - if (addressType.type == SegwitAddresType.mweb) { + if (addressType.type == SegwitAddressType.mweb) { return BytesUtils.fromHexString( MwebAddress.fromAddress(address: address).addressProgram, ); @@ -38,11 +38,11 @@ class BitcoinAddressUtils { case P2shAddressType.p2wpkhInP2sh: case P2shAddressType.p2wshInP2sh: return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return P2trAddress.fromScriptPubkey(script: script).toAddress(network); default: } @@ -109,15 +109,15 @@ class BitcoinAddressUtils { } else if (type is P2shAddress) { return P2shAddressType.p2wpkhInP2sh; } else if (type is P2wshAddress) { - return SegwitAddresType.p2wsh; + return SegwitAddressType.p2wsh; } else if (type is P2trAddress) { - return SegwitAddresType.p2tr; + return SegwitAddressType.p2tr; } else if (type is MwebAddress) { - return SegwitAddresType.mweb; + return SegwitAddressType.mweb; } else if (type is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else { - return SegwitAddresType.p2wpkh; + return SegwitAddressType.p2wpkh; } } @@ -137,17 +137,17 @@ class BitcoinAddressUtils { case P2shAddressType.p2wpkhInP2sh: case P2shAddressType.p2wshInP2sh: return BitcoinDerivationInfos.BIP49; - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: if (isElectrum == true) { return BitcoinDerivationInfos.ELECTRUM; } else { return BitcoinDerivationInfos.BIP84; } - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return BitcoinDerivationInfos.BIP86; - case SegwitAddresType.mweb: + case SegwitAddressType.mweb: return BitcoinDerivationInfos.BIP86; - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return BitcoinDerivationInfos.BIP84; default: throw Exception("Derivation not available for $scriptType"); diff --git a/lib/src/bitcoin/address/utils/address_utils.dart b/lib/src/bitcoin/address/utils/address_utils.dart index 9a27306..cfac5fd 100644 --- a/lib/src/bitcoin/address/utils/address_utils.dart +++ b/lib/src/bitcoin/address/utils/address_utils.dart @@ -24,7 +24,7 @@ class _BitcoinAddressUtils { final decode = List.unmodifiable(Base58Decoder.decode(address)); /// Extract script bytes excluding version and checksum. - final List scriptBytes = decode.sublist(1, decode.length - Base58Const.checksumByteLen); + final scriptBytes = decode.sublist(1, decode.length - Base58Const.checksumByteLen); /// Ensure the script bytes have the expected length. if (scriptBytes.length != hash160DigestLength) { @@ -32,12 +32,12 @@ class _BitcoinAddressUtils { } /// Extract version, data, and checksum. - final List version = [decode[0]]; - List data = decode.sublist(0, decode.length - Base58Const.checksumByteLen); - List checksum = decode.sublist(decode.length - Base58Const.checksumByteLen); + final version = [decode[0]]; + final data = decode.sublist(0, decode.length - Base58Const.checksumByteLen); + final checksum = decode.sublist(decode.length - Base58Const.checksumByteLen); /// Verify the checksum. - List hash = QuickCrypto.sha256DoubleHash(data).sublist(0, Base58Const.checksumByteLen); + final hash = QuickCrypto.sha256DoubleHash(data).sublist(0, Base58Const.checksumByteLen); if (!BytesUtils.bytesEqual(checksum, hash)) { return null; } @@ -82,7 +82,7 @@ class _BitcoinAddressUtils { final convert = SegwitBech32Decoder.decode(network.p2wpkhHrp, address); final witnessVersion = convert.item1; if (witnessVersion != version) { - throw const BitcoinBasePluginException("Invalid segwit version"); + throw const DartBitcoinPluginException('Invalid segwit version'); } return BytesUtils.toHexString(convert.item2); } @@ -95,6 +95,10 @@ class _BitcoinAddressUtils { /// /// Returns a SegwitAddress instance representing the converted SegWit address, /// or null if the conversion is not successful. + static SegwitAddress? toSegwitAddress(String address, BasedUtxoNetwork network) { + return toP2wpkhAddress(address, network); + } + static SegwitAddress? toP2wpkhAddress(String address, BasedUtxoNetwork network) { try { final convert = SegwitBech32Decoder.decode(network.p2wpkhHrp, address); @@ -128,8 +132,8 @@ class _BitcoinAddressUtils { if (network.supportedAddress.contains(address.type)) { return address; } - throw BitcoinBasePluginException( - "${network.value} does not support ${address.type.value} address"); + throw DartBitcoinPluginException( + '${network.value} does not support ${address.type.value} address'); } /// Decodes a Bitcoin address and returns a corresponding BitcoinBaseAddress instance @@ -143,17 +147,12 @@ class _BitcoinAddressUtils { /// Throws a [MessageException] if the address is invalid or not supported by the network. static BitcoinBaseAddress decodeAddress(String address, BasedUtxoNetwork network) { BitcoinBaseAddress? baseAddress; - if (network.supportedAddress.contains(SegwitAddresType.p2wpkh)) { - baseAddress = toP2wpkhAddress(address, network); + if (network.supportedAddress.contains(SegwitAddressType.p2wpkh)) { + baseAddress = toSegwitAddress(address, network); } baseAddress ??= toLegacy(address, network); if (baseAddress == null) { - try { - throw const BitcoinBasePluginException("test2 Bitcoin address"); - } catch (e, s) { - print(s); - } - throw const BitcoinBasePluginException("test Bitcoin address"); + throw const DartBitcoinPluginException('Invalid Bitcoin address'); } return validateAddress(baseAddress, network); } @@ -174,8 +173,8 @@ class _BitcoinAddressUtils { } // ignore: empty_catches } catch (e) {} - throw const BitcoinBasePluginException( - "Invalid Bitcoin address program length (program length should be 32 or 20 bytes)"); + throw const DartBitcoinPluginException( + 'Invalid Bitcoin address program length (program length should be 32 or 20 bytes)'); } /// Decodes a Bitcoin Cash (BCH) address and returns a corresponding LegacyAddress instance @@ -190,8 +189,8 @@ class _BitcoinAddressUtils { static LegacyAddress? decodeBchAddress(String address, BitcoinCashNetwork network, {bool validateNetworkHRP = false}) { try { - final String hrp = - validateNetworkHRP ? network.networkHRP : address.substring(0, address.indexOf(":")); + final hrp = + validateNetworkHRP ? network.networkHRP : address.substring(0, address.indexOf(':')); final decode = BchBech32Decoder.decode(hrp, address); final scriptBytes = decode.item2; final version = decode.item1; @@ -260,8 +259,8 @@ class _BitcoinAddressUtils { required BitcoinAddressType type, required BasedUtxoNetwork network}) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( - "${network.value} does not support ${type.value} address type"); + throw DartBitcoinPluginException( + '${network.value} does not support ${type.value} address type'); } if (network is BitcoinCashNetwork) { final decode = _BitcoinAddressUtils.decodeBchAddress(address, network); @@ -276,7 +275,7 @@ class _BitcoinAddressUtils { if (decode == null) return null; final version = decode.item2; final addrBytes = decode.item1; - final String scriptHex = BytesUtils.toHexString(addrBytes); + final scriptHex = BytesUtils.toHexString(addrBytes); switch (type) { case P2pkhAddressType.p2pkh: @@ -334,8 +333,8 @@ class _BitcoinAddressUtils { {required BitcoinCashNetwork network, required String addressProgram, required BitcoinAddressType type}) { - List programBytes = BytesUtils.fromHexString(addressProgram); - final List netVersion = + final programBytes = BytesUtils.fromHexString(addressProgram); + final netVersion = _getBchNetVersion(network: network, type: type, secriptLength: programBytes.length); return BchBech32Encoder.encode(network.networkHRP, netVersion, programBytes); @@ -352,7 +351,7 @@ class _BitcoinAddressUtils { {required BitcoinCashNetwork network, required BitcoinAddressType type, int secriptLength = hash160DigestLength}) { - bool isToken = type.value.contains("WT"); + final isToken = type.value.contains('WT'); if (!type.isP2sh) { if (!isToken) return network.p2pkhNetVer; return network.p2pkhWtNetVer; @@ -387,7 +386,7 @@ class _BitcoinAddressUtils { if (network is BitcoinCashNetwork) { return legacyToBchAddress(addressProgram: addressProgram, network: network, type: type); } - List programBytes = BytesUtils.fromHexString(addressProgram); + var programBytes = BytesUtils.fromHexString(addressProgram); switch (type) { case P2shAddressType.p2wpkhInP2sh: case P2shAddressType.p2wshInP2sh: @@ -412,7 +411,7 @@ class _BitcoinAddressUtils { /// Returns the RIPEMD-160 hash of the public key as a hexadecimal string. static String pubkeyToHash160(String publicKey) { final bytes = BytesUtils.fromHexString(publicKey); - List ripemd160Hash = QuickCrypto.hash160(bytes); + final ripemd160Hash = QuickCrypto.hash160(bytes); return BytesUtils.toHexString(ripemd160Hash); } diff --git a/lib/src/bitcoin/script/control_block.dart b/lib/src/bitcoin/script/control_block.dart index d6a07b2..209e4eb 100644 --- a/lib/src/bitcoin/script/control_block.dart +++ b/lib/src/bitcoin/script/control_block.dart @@ -12,10 +12,10 @@ class ControlBlock { final List? scripts; List toBytes() { - final List version = [BitcoinOpCodeConst.LEAF_VERSION_TAPSCRIPT]; + final version = [BitcoinOpCodeConst.LEAF_VERSION_TAPSCRIPT]; - final List pubKey = BytesUtils.fromHexString(public.toXOnlyHex()); - final List marklePath = scripts ?? []; + final pubKey = BytesUtils.fromHexString(public.toXOnlyHex()); + final marklePath = scripts ?? []; return [...version, ...pubKey, ...marklePath]; } diff --git a/lib/src/bitcoin/script/input.dart b/lib/src/bitcoin/script/input.dart index 149d545..be7c0af 100644 --- a/lib/src/bitcoin/script/input.dart +++ b/lib/src/bitcoin/script/input.dart @@ -1,5 +1,6 @@ +import 'dart:typed_data'; + import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; -import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/utils/utils.dart'; import 'script.dart'; @@ -10,16 +11,21 @@ import 'script.dart'; /// [scriptSig] the script that satisfies the locking conditions /// [sequence] the input sequence (for timelocks, RBF, etc.) class TxInput { - TxInput( - {required this.txId, - required this.txIndex, - Script? scriptSig, - List? sequence}) - : sequence = List.unmodifiable( - sequence ?? BitcoinOpCodeConst.DEFAULT_TX_SEQUENCE), + TxInput({ + required this.txId, + required this.txIndex, + Script? scriptSig, + List? sequence, + }) : sequence = List.unmodifiable( + sequence ?? BitcoinOpCodeConst.DEFAULT_TX_SEQUENCE, + ), scriptSig = scriptSig ?? Script(script: []); - TxInput copyWith( - {String? txId, int? txIndex, Script? scriptSig, List? sequence}) { + TxInput copyWith({ + String? txId, + int? txIndex, + Script? scriptSig, + List? sequence, + }) { return TxInput( txId: txId ?? this.txId, txIndex: txIndex ?? this.txIndex, @@ -34,60 +40,73 @@ class TxInput { /// creates a copy of the object TxInput copy() { - return TxInput( - txId: txId, txIndex: txIndex, scriptSig: scriptSig, sequence: sequence); + return TxInput(txId: txId, txIndex: txIndex, scriptSig: scriptSig, sequence: sequence); } /// serializes TxInput to bytes List toBytes() { final txidBytes = BytesUtils.fromHexString(txId).reversed.toList(); - final txoutBytes = List.filled(4, 0); - writeUint32LE(txIndex, txoutBytes); + final txoutBytes = IntUtils.toBytes(txIndex, length: 4, byteOrder: Endian.little); + // writeUint32LE(txIndex, txoutBytes); final scriptSigBytes = scriptSig.toBytes(); - final scriptSigLengthVarint = IntUtils.encodeVarint(scriptSigBytes.length); - final data = List.from([ - ...txidBytes, - ...txoutBytes, - ...scriptSigLengthVarint, - ...scriptSigBytes, - ...sequence, - ]); + final data = List.from( + [...txidBytes, ...txoutBytes, ...scriptSigLengthVarint, ...scriptSigBytes, ...sequence]); return data; } - static Tuple fromRaw( - {required String raw, int cursor = 0, bool hasSegwit = false}) { - final txInputRaw = BytesUtils.fromHexString(raw); - List inpHash = - txInputRaw.sublist(cursor, cursor + 32).reversed.toList(); - if (inpHash.isEmpty) { - throw const BitcoinBasePluginException( - "Input transaction hash not found. Probably malformed raw transaction"); - } - List outputN = - txInputRaw.sublist(cursor + 32, cursor + 36).reversed.toList(); - cursor += 36; - final vi = IntUtils.decodeVarint(txInputRaw.sublist(cursor, cursor + 9)); + static Tuple deserialize({ + required List bytes, + String? raw, + int cursor = 0, + bool hasSegwit = false, + }) { + return fromRaw(bytes: bytes, cursor: cursor, hasSegwit: hasSegwit); + } + + static Tuple fromRaw({ + List? bytes, + String? raw, + int cursor = 0, + bool hasSegwit = false, + }) { + final txInputRaw = bytes ?? BytesUtils.fromHexString(raw!); + final inpHash = txInputRaw.sublist(cursor, cursor + 32).reversed.toList(); + cursor += 32; + final outputN = + IntUtils.fromBytes(txInputRaw.sublist(cursor, cursor + 4), byteOrder: Endian.little); + cursor += 4; + final vi = IntUtils.decodeVarint(txInputRaw.sublist(cursor)); cursor += vi.item2; - List unlockingScript = txInputRaw.sublist(cursor, cursor + vi.item1); + final unlockingScript = txInputRaw.sublist(cursor, cursor + vi.item1); cursor += vi.item1; - List sequenceNumberData = txInputRaw.sublist(cursor, cursor + 4); + final sequenceNumberData = txInputRaw.sublist(cursor, cursor + 4); cursor += 4; return Tuple( TxInput( - txId: BytesUtils.toHexString(inpHash), - txIndex: int.parse(BytesUtils.toHexString(outputN), radix: 16), - scriptSig: Script.fromRaw( - hexData: BytesUtils.toHexString(unlockingScript), - hasSegwit: hasSegwit), - sequence: sequenceNumberData), + txId: BytesUtils.toHexString(inpHash), + txIndex: outputN, + scriptSig: Script.deserialize( + bytes: unlockingScript, + hasSegwit: hasSegwit, + ), + sequence: sequenceNumberData, + ), cursor); } + Map toJson() { + return { + 'txid': txId, + 'txIndex': txIndex, + 'scriptSig': scriptSig.script, + 'sequance': BytesUtils.toHexString(sequence), + }; + } + @override String toString() { - return "TxInput{txId: $txId, txIndex: $txIndex, scriptSig: $scriptSig, sequence: ${BytesUtils.toHexString(sequence)}}"; + return 'TxInput{txId: $txId, txIndex: $txIndex, scriptSig: $scriptSig, sequence: ${BytesUtils.toHexString(sequence)}}'; } } diff --git a/lib/src/bitcoin/script/op_code/constant.dart b/lib/src/bitcoin/script/op_code/constant.dart index bd62271..a677331 100644 --- a/lib/src/bitcoin/script/op_code/constant.dart +++ b/lib/src/bitcoin/script/op_code/constant.dart @@ -3,6 +3,15 @@ // ignore_for_file: constant_identifier_names, non_constant_identifier_names, equal_keys_in_map, camel_case_types class BitcoinOpCodeConst { + static const int opPushData1 = 0x4c; + static const int opPushData2 = 0x4d; + static const int opPushData4 = 0x4e; + static bool isOpPushData(int byte) { + return byte == BitcoinOpCodeConst.opPushData1 || + byte == BitcoinOpCodeConst.opPushData2 || + byte == BitcoinOpCodeConst.opPushData4; + } + static const OP_0 = "OP_0"; static const OP_FALSE = "OP_FALSE"; static const OP_PUSHDATA1 = "OP_PUSHDATA1"; @@ -371,9 +380,9 @@ class BitcoinOpCodeConst { static const int NEGATIVE_SATOSHI = -1; /// Bitcoin address types - static const String P2PKH_ADDRESS = "p2pkh"; - static const String P2SH_ADDRESS = "p2sh"; - static const String P2WPKH_ADDRESS_V0 = "p2wpkhv0"; - static const String P2WSH_ADDRESS_V0 = "p2wshv0"; - static const String P2TR_ADDRESS_V1 = "p2trv1"; + static const String P2PKH_ADDRESS = 'p2pkh'; + static const String P2SH_ADDRESS = 'p2sh'; + static const String P2WPKH_ADDRESS_V0 = 'p2wpkhv0'; + static const String P2WSH_ADDRESS_V0 = 'p2wshv0'; + static const String P2TR_ADDRESS_V1 = 'p2trv1'; } diff --git a/lib/src/bitcoin/script/op_code/constant_lib.dart b/lib/src/bitcoin/script/op_code/constant_lib.dart index 4a7c224..e6c4f56 100644 --- a/lib/src/bitcoin/script/op_code/constant_lib.dart +++ b/lib/src/bitcoin/script/op_code/constant_lib.dart @@ -1,4 +1,4 @@ -library bitcoin_constants; +library; export 'constant.dart'; export 'tools.dart'; diff --git a/lib/src/bitcoin/script/op_code/tools.dart b/lib/src/bitcoin/script/op_code/tools.dart index 9bb44e5..1dafe7b 100644 --- a/lib/src/bitcoin/script/op_code/tools.dart +++ b/lib/src/bitcoin/script/op_code/tools.dart @@ -1,41 +1,41 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/utils/utils.dart'; List opPushData(String hexData) { - final List dataBytes = BytesUtils.fromHexString(hexData); - if (dataBytes.length < 0x4c) { - return List.from([dataBytes.length]) + dataBytes; + final dataBytes = BytesUtils.fromHexString(hexData); + if (dataBytes.length < BitcoinOpCodeConst.opPushData1) { + return [dataBytes.length, ...dataBytes]; } else if (dataBytes.length < mask8) { - return List.from([0x4c]) + - List.from([dataBytes.length]) + - dataBytes; + return [BitcoinOpCodeConst.opPushData1, dataBytes.length, ...dataBytes]; } else if (dataBytes.length < mask16) { - var lengthBytes = List.filled(2, 0); - - writeUint16LE(dataBytes.length, lengthBytes); - return List.from([0x4d, ...lengthBytes, ...dataBytes]); + final lengthBytes = + IntUtils.toBytes(dataBytes.length, length: 2, byteOrder: Endian.little); + return [BitcoinOpCodeConst.opPushData2, ...lengthBytes, ...dataBytes]; } else if (dataBytes.length < mask32) { - var lengthBytes = List.filled(4, 0); - writeUint32LE(lengthBytes.length, lengthBytes); - return List.from([0x4e, ...lengthBytes, ...dataBytes]); + final lengthBytes = + IntUtils.toBytes(dataBytes.length, length: 4, byteOrder: Endian.little); + return [BitcoinOpCodeConst.opPushData4, ...lengthBytes, ...dataBytes]; } else { - throw const BitcoinBasePluginException( - "Data too large. Cannot push into script"); + throw const DartBitcoinPluginException( + 'Data too large. Cannot push into script'); } } List pushInteger(int integer) { if (integer < 0) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'Integer is currently required to be positive.'); } /// Calculate the number of bytes required to represent the integer - int numberOfBytes = (integer.bitLength + 7) ~/ 8; + final numberOfBytes = (integer.bitLength + 7) ~/ 8; /// Convert to little-endian bytes - List integerBytes = List.filled(numberOfBytes, 0); - for (int i = 0; i < numberOfBytes; i++) { + var integerBytes = List.filled(numberOfBytes, 0); + for (var i = 0; i < numberOfBytes; i++) { integerBytes[i] = (integer >> (i * 8)) & mask8; } diff --git a/lib/src/bitcoin/script/output.dart b/lib/src/bitcoin/script/output.dart index 09b11e2..45750f4 100644 --- a/lib/src/bitcoin/script/output.dart +++ b/lib/src/bitcoin/script/output.dart @@ -21,6 +21,14 @@ class TxOutput { final bool isSilentPayment; final bool isChange; + Map toJson() { + return { + 'cashToken': cashToken?.toJson(), + 'amount': amount.toString(), + 'scriptPubKey': scriptPubKey.script + }; + } + /// creates a copy of the object TxOutput copy() { return TxOutput( @@ -33,31 +41,57 @@ class TxOutput { } List toBytes() { - final amountBytes = BigintUtils.toBytes(amount, length: 8, order: Endian.little); - List scriptBytes = [...cashToken?.toBytes() ?? [], ...scriptPubKey.toBytes()]; - final data = [...amountBytes, ...IntUtils.encodeVarint(scriptBytes.length), ...scriptBytes]; + final amountBytes = BigintUtils.toBytes( + amount, + length: 8, + order: Endian.little, + ); + final scriptBytes = [ + ...cashToken?.toBytes() ?? [], + ...scriptPubKey.toBytes(), + ]; + final data = [ + ...amountBytes, + ...IntUtils.encodeVarint(scriptBytes.length), + ...scriptBytes, + ]; return data; } - static Tuple fromRaw( - {required String raw, required int cursor, bool hasSegwit = false}) { - final txBytes = BytesUtils.fromHexString(raw); - final value = - BigintUtils.fromBytes(txBytes.sublist(cursor, cursor + 8), byteOrder: Endian.little) - .toSigned(64); + static Tuple deserialize({ + required int cursor, + List? bytes, + String? raw, + bool hasSegwit = false, + }) { + return fromRaw(bytes: bytes, cursor: cursor, hasSegwit: hasSegwit); + } + + static Tuple fromRaw({ + required int cursor, + List? bytes, + String? raw, + bool hasSegwit = false, + }) { + final txBytes = bytes ?? BytesUtils.fromHexString(raw!); + final value = BigintUtils.fromBytes( + txBytes.sublist(cursor, cursor + 8), + byteOrder: Endian.little, + ).toSigned(64); cursor += 8; - final vi = IntUtils.decodeVarint(txBytes.sublist(cursor, cursor + 9)); + final vi = IntUtils.decodeVarint(txBytes.sublist(cursor)); cursor += vi.item2; final token = CashToken.fromRaw(txBytes.sublist(cursor)); - List lockScript = txBytes.sublist(cursor + token.item2, cursor + vi.item1); + + final lockScript = txBytes.sublist(cursor + token.item2, cursor + vi.item1); cursor += vi.item1; return Tuple( TxOutput( amount: value, cashToken: token.item1, - scriptPubKey: Script.fromRaw( - hexData: BytesUtils.toHexString(lockScript), + scriptPubKey: Script.deserialize( + bytes: lockScript, hasSegwit: hasSegwit, )), cursor); @@ -65,6 +99,6 @@ class TxOutput { @override String toString() { - return "TxOutput{cashToken: ${cashToken?.toString()}}, amount: $amount, script: ${scriptPubKey.toString()}}"; + return 'TxOutput{cashToken: ${cashToken?.toString()}}, amount: $amount, script: ${scriptPubKey.toString()}}'; } } diff --git a/lib/src/bitcoin/script/script.dart b/lib/src/bitcoin/script/script.dart index 47dc269..58af80b 100644 --- a/lib/src/bitcoin/script/script.dart +++ b/lib/src/bitcoin/script/script.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:bitcoin_base/src/models/network.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -14,43 +15,64 @@ class Script { } return true; }(), - "A valid script is a composition of opcodes, hexadecimal strings, and integers arranged in a structured list."), + 'A valid script is a composition of opcodes, hexadecimal strings, and integers arranged in a structured list.'), script = List.unmodifiable(script); final List script; - static Script fromRaw({List? byteData, String? hexData, bool hasSegwit = false}) { - List commands = []; - int index = 0; - final scriptBytes = byteData ?? (hexData != null ? BytesUtils.fromHexString(hexData) : null); + static Script deserialize({ + List? bytes, + String? hexData, + bool hasSegwit = false, + }) { + return fromRaw(bytes: bytes, hexData: hexData, hasSegwit: hasSegwit); + } + + static Script fromRaw({ + List? bytes, + String? hexData, + bool hasSegwit = false, + }) { + final commands = []; + var index = 0; + final scriptBytes = bytes ?? (hexData != null ? BytesUtils.fromHexString(hexData) : null); if (scriptBytes == null) { - throw ArgumentError("Invalid script"); + throw DartBitcoinPluginException("Invalid script"); } while (index < scriptBytes.length) { - int byte = scriptBytes[index]; + final byte = scriptBytes[index]; if (BitcoinOpCodeConst.CODE_OPS.containsKey(byte)) { - commands.add(BitcoinOpCodeConst.CODE_OPS[byte]!); - index = index + 1; - } else if (!hasSegwit && byte == 0x4c) { - int bytesToRead = scriptBytes[index + 1]; + if (!BitcoinOpCodeConst.isOpPushData(byte)) { + commands.add(BitcoinOpCodeConst.CODE_OPS[byte]!); + } + + /// skip op index = index + 1; - commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; - } else if (!hasSegwit && byte == 0x4d) { - int bytesToRead = readUint16LE(scriptBytes, index + 1); - - index = index + 3; - commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; - } else if (!hasSegwit && byte == 0x4e) { - int bytesToRead = readUint32LE(scriptBytes, index + 1); - - index = index + 5; - commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; + if (byte == BitcoinOpCodeConst.opPushData1) { + // get len + final bytesToRead = scriptBytes[index]; + // skip len + index = index + 1; + commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); + + /// add length + index = index + bytesToRead; + } else if (byte == BitcoinOpCodeConst.opPushData2) { + /// get len + final bytesToRead = readUint16LE(scriptBytes, index); + index = index + 2; + commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); + index = index + bytesToRead; + } else if (byte == BitcoinOpCodeConst.opPushData4) { + final bytesToRead = readUint32LE(scriptBytes, index); + + index = index + 4; + commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); + index = index + bytesToRead; + } } else { - final viAndSize = IntUtils.decodeVarint(scriptBytes.sublist(index, index + 9)); - int dataSize = viAndSize.item1; - int size = viAndSize.item2; + final viAndSize = IntUtils.decodeVarint(scriptBytes.sublist(index)); + final dataSize = viAndSize.item1; + final size = viAndSize.item2; final lastIndex = (index + size + dataSize) > scriptBytes.length ? scriptBytes.length : (index + size + dataSize); @@ -75,7 +97,7 @@ class Script { script.length == 66 && (script[0] == 2 || script[0] == 3) && (script[33] == 2 || script[33] == 3)) { - return SegwitAddresType.mweb; + return SegwitAddressType.mweb; } final first = findScriptParam(0); @@ -88,15 +110,15 @@ class Script { final lockingScriptBytes = opPushData(sec); if (lockingScriptBytes.length == 21) { - return SegwitAddresType.p2wpkh; + return SegwitAddressType.p2wpkh; } else if (lockingScriptBytes.length == 33) { - return SegwitAddresType.p2wsh; + return SegwitAddressType.p2wsh; } } else if (first == "OP_1") { final lockingScriptBytes = opPushData(sec); if (lockingScriptBytes.length == 33) { - return SegwitAddresType.p2tr; + return SegwitAddressType.p2tr; } } @@ -124,7 +146,7 @@ class Script { String toAddress() { final addressType = getAddressType(); if (addressType == null) { - throw ArgumentError("Invalid script"); + throw DartBitcoinPluginException("Invalid script"); } switch (addressType) { @@ -132,23 +154,22 @@ class Script { return P2pkhAddress.fromScriptPubkey(script: this).toAddress(BitcoinNetwork.mainnet); case P2shAddressType.p2pkhInP2sh: return P2shAddress.fromScriptPubkey(script: this).toAddress(BitcoinNetwork.mainnet); - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return P2wpkhAddress.fromScriptPubkey(script: this).toAddress(BitcoinNetwork.mainnet); - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return P2wshAddress.fromScriptPubkey(script: this).toAddress(BitcoinNetwork.mainnet); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return P2trAddress.fromScriptPubkey(script: this).toAddress(BitcoinNetwork.mainnet); } - throw ArgumentError("Invalid script"); + throw DartBitcoinPluginException("Invalid script"); } /// returns a serialized byte version of the script List toBytes() { if (script.isEmpty) return []; - if (script.every((x) => x is int)) return script.cast(); - DynamicByteTracker scriptBytes = DynamicByteTracker(); - for (var token in script) { + final scriptBytes = DynamicByteTracker(); + for (final token in script) { if (BitcoinOpCodeConst.OP_CODES.containsKey(token)) { scriptBytes.add(BitcoinOpCodeConst.OP_CODES[token]!); } else if (token is int && token >= 0 && token <= 16) { diff --git a/lib/src/bitcoin/script/sequence.dart b/lib/src/bitcoin/script/sequence.dart index f202eff..e4cfd0f 100644 --- a/lib/src/bitcoin/script/sequence.dart +++ b/lib/src/bitcoin/script/sequence.dart @@ -13,7 +13,7 @@ class Sequence { {required this.seqType, required this.value, this.isTypeBlock = true}) { if (seqType == BitcoinOpCodeConst.TYPE_RELATIVE_TIMELOCK && (value < 1 || value > mask16)) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'Sequence should be between 1 and 65535'); } } @@ -31,7 +31,7 @@ class Sequence { return List.from(BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); } if (seqType == BitcoinOpCodeConst.TYPE_RELATIVE_TIMELOCK) { - int seq = 0; + var seq = 0; if (!isTypeBlock) { seq |= 1 << 22; } @@ -39,16 +39,16 @@ class Sequence { return IntUtils.toBytes(seq, length: 4, byteOrder: Endian.little); } - throw const BitcoinBasePluginException("Invalid seqType"); + throw const DartBitcoinPluginException('Invalid seqType'); } /// Returns the appropriate integer for a script; e.g. for relative timelocks int forScript() { if (seqType == BitcoinOpCodeConst.TYPE_REPLACE_BY_FEE) { - throw const BitcoinBasePluginException( - "RBF is not to be included in a script."); + throw const DartBitcoinPluginException( + 'RBF is not to be included in a script.'); } - int scriptIntiger = value; + var scriptIntiger = value; if (seqType == BitcoinOpCodeConst.TYPE_RELATIVE_TIMELOCK && !isTypeBlock) { scriptIntiger |= 1 << 22; } @@ -57,6 +57,6 @@ class Sequence { @override String toString() { - return "Sequence{seqType: $seqType, value: $value, isTypeBlock: $isTypeBlock}"; + return 'Sequence{seqType: $seqType, value: $value, isTypeBlock: $isTypeBlock}'; } } diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 4b96fdb..b31e55a 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -3,6 +3,7 @@ import 'package:bitcoin_base/src/cash_token/cash_token.dart'; import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/crypto/crypto.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; +import 'package:blockchain_utils/helper/helper.dart'; import 'package:blockchain_utils/utils/utils.dart'; import 'package:blockchain_utils/crypto/quick_crypto.dart'; import 'package:collection/collection.dart'; @@ -30,11 +31,11 @@ class BtcTransaction { List? lock, List? version, this.hasSilentPayment = false, - }) : locktime = List.unmodifiable(lock ?? BitcoinOpCodeConst.DEFAULT_TX_LOCKTIME), - version = List.unmodifiable(version ?? BitcoinOpCodeConst.DEFAULT_TX_VERSION), - inputs = List.unmodifiable(inputs), - outputs = List.unmodifiable(outputs), - witnesses = List.unmodifiable(witnesses); + }) : locktime = lock?.immutable ?? BitcoinOpCodeConst.DEFAULT_TX_LOCKTIME, + version = version?.immutable ?? BitcoinOpCodeConst.DEFAULT_TX_VERSION, + inputs = inputs.immutable, + outputs = outputs.immutable, + witnesses = witnesses.immutable; final List inputs; final List outputs; final List locktime; @@ -59,8 +60,8 @@ class BtcTransaction { witnesses: witnesses ?? this.witnesses, hasSegwit: hasSegwit ?? this.hasSegwit, mwebBytes: mwebBytes, - lock: lock ?? List.from(locktime), - version: version ?? List.from(this.version), + lock: lock ?? locktime, + version: version ?? this.version, hasSilentPayment: hasSilentPayment, ); } @@ -80,8 +81,8 @@ class BtcTransaction { /// Instantiates a Transaction from serialized raw hexadacimal data (classmethod) static BtcTransaction fromRaw(String raw) { final rawtx = BytesUtils.fromHexString(raw); - final List version = rawtx.sublist(0, 4); - int cursor = 4; + final version = rawtx.sublist(0, 4); + var cursor = 4; List? flag; bool hasSegwit = false; bool hasMweb = false; @@ -95,12 +96,12 @@ class BtcTransaction { } cursor += 2; } - final vi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); + final vi = IntUtils.decodeVarint(rawtx.sublist(cursor)); cursor += vi.item2; bool canReplaceByFee = false; - List inputs = []; - for (int index = 0; index < vi.item1; index++) { + final inputs = []; + for (var index = 0; index < vi.item1; index++) { final inp = TxInput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); final input = inp.item1; @@ -112,35 +113,34 @@ class BtcTransaction { const ListEquality().equals(input.sequence, BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); } } - - List outputs = []; - final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); + final outputs = []; + final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor)); cursor += viOut.item2; - for (int index = 0; index < viOut.item1; index++) { - final inp = TxOutput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + for (var index = 0; index < viOut.item1; index++) { + final inp = TxOutput.deserialize(bytes: rawtx, hasSegwit: hasSegwit, cursor: cursor); outputs.add(inp.item1); cursor = inp.item2; } - List witnesses = []; + final witnesses = []; if (hasSegwit) { - for (int n = 0; n < inputs.length; n++) { - final input = inputs[n]; - if (input.scriptSig.script.isNotEmpty) continue; - - final wVi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); - cursor += wVi.item2; - List witnessesTmp = []; - for (int n = 0; n < wVi.item1; n++) { - List witness = []; - final wtVi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); - if (wtVi.item1 != 0) { - witness = rawtx.sublist(cursor + wtVi.item2, cursor + wtVi.item1 + wtVi.item2); + if (cursor + 4 < rawtx.length) { + // in this case the tx contains wintness data. + for (var n = 0; n < inputs.length; n++) { + final wVi = IntUtils.decodeVarint(rawtx.sublist(cursor)); + cursor += wVi.item2; + final witnessesTmp = []; + for (var n = 0; n < wVi.item1; n++) { + var witness = []; + final wtVi = IntUtils.decodeVarint(rawtx.sublist(cursor)); + if (wtVi.item1 != 0) { + witness = rawtx.sublist(cursor + wtVi.item2, cursor + wtVi.item1 + wtVi.item2); + } + cursor += wtVi.item1 + wtVi.item2; + witnessesTmp.add(BytesUtils.toHexString(witness)); } - cursor += wtVi.item1 + wtVi.item2; - witnessesTmp.add(BytesUtils.toHexString(witness)); - } - witnesses.add(TxWitnessInput(stack: witnessesTmp)); + witnesses.add(TxWitnessInput(stack: witnessesTmp)); + } } } List? mwebBytes; @@ -170,51 +170,49 @@ class BtcTransaction { {required int txInIndex, required Script script, int sighash = BitcoinOpCodeConst.SIGHASH_ALL}) { - BtcTransaction tx = copy(this); + var tx = copy(this); for (final i in tx.inputs) { i.scriptSig = Script(script: []); } tx.inputs[txInIndex].scriptSig = script; if ((sighash & 0x1f) == BitcoinOpCodeConst.SIGHASH_NONE) { - // tx.outputs.clear(); tx = tx.copyWith(outputs: []); - for (int i = 0; i < tx.inputs.length; i++) { + for (var i = 0; i < tx.inputs.length; i++) { if (i != txInIndex) { - tx.inputs[i].sequence = List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); + tx.inputs[i].sequence = BitcoinOpCodeConst.EMPTY_TX_SEQUENCE; } } } else if ((sighash & 0x1f) == BitcoinOpCodeConst.SIGHASH_SINGLE) { if (txInIndex >= tx.outputs.length) { - throw const BitcoinBasePluginException( - "Transaction index is greater than the available outputs"); + throw const DartBitcoinPluginException( + 'Transaction index is greater than the available outputs'); } - List outputs = []; - for (int i = 0; i < txInIndex; i++) { + final outputs = []; + for (var i = 0; i < txInIndex; i++) { outputs.add(TxOutput( amount: BigInt.from(BitcoinOpCodeConst.NEGATIVE_SATOSHI), scriptPubKey: Script(script: []))); } tx = tx.copyWith(outputs: [...outputs, tx.outputs[txInIndex]]); - for (int i = 0; i < tx.inputs.length; i++) { + for (var i = 0; i < tx.inputs.length; i++) { if (i != txInIndex) { - tx.inputs[i].sequence = List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); + tx.inputs[i].sequence = BitcoinOpCodeConst.EMPTY_TX_SEQUENCE; } } } if ((sighash & BitcoinOpCodeConst.SIGHASH_ANYONECANPAY) != 0) { tx = tx.copyWith(inputs: [tx.inputs[txInIndex]]); } - List txForSign = tx.toBytes(segwit: false); + var txForSign = tx.toBytes(segwit: false); - txForSign = List.from( - [...txForSign, ...IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little)]); + txForSign = [...txForSign, ...IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little)]; return QuickCrypto.sha256DoubleHash(txForSign); } /// Serializes Transaction to bytes List toBytes({bool segwit = false}) { - DynamicByteTracker data = DynamicByteTracker(); + final data = DynamicByteTracker(); data.add(version); var flag = 0; if (segwit) flag |= 1; @@ -260,22 +258,22 @@ class BtcTransaction { required BigInt amount, CashToken? token}) { final tx = copy(this); - List hashPrevouts = List.filled(32, 0); - List hashSequence = List.filled(32, 0); - List hashOutputs = List.filled(32, 0); - int basicSigHashType = sighash & 0x1F; - bool anyoneCanPay = (sighash & 0xF0) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; - bool signAll = (basicSigHashType != BitcoinOpCodeConst.SIGHASH_SINGLE) && + var hashPrevouts = List.filled(32, 0); + var hashSequence = List.filled(32, 0); + var hashOutputs = List.filled(32, 0); + final basicSigHashType = sighash & 0x1F; + final anyoneCanPay = (sighash & 0xF0) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; + final signAll = (basicSigHashType != BitcoinOpCodeConst.SIGHASH_SINGLE) && (basicSigHashType != BitcoinOpCodeConst.SIGHASH_NONE); if (!anyoneCanPay) { hashPrevouts = []; for (final txin in tx.inputs) { - List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); - hashPrevouts = List.from([ + final txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); + hashPrevouts = [ ...hashPrevouts, ...txidBytes, ...IntUtils.toBytes(txin.txIndex, length: 4, byteOrder: Endian.little) - ]); + ]; } hashPrevouts = QuickCrypto.sha256DoubleHash(hashPrevouts); } @@ -283,48 +281,46 @@ class BtcTransaction { if (!anyoneCanPay && signAll) { hashSequence = []; for (final i in tx.inputs) { - hashSequence = List.from([...hashSequence, ...i.sequence]); + hashSequence = [...hashSequence, ...i.sequence]; } hashSequence = QuickCrypto.sha256DoubleHash(hashSequence); } if (signAll) { hashOutputs = []; for (final i in tx.outputs) { - hashOutputs = List.from([...hashOutputs, ...i.toBytes()]); + hashOutputs = [...hashOutputs, ...i.toBytes()]; } hashOutputs = QuickCrypto.sha256DoubleHash(hashOutputs); } else if (basicSigHashType == BitcoinOpCodeConst.SIGHASH_SINGLE && txInIndex < tx.outputs.length) { final out = tx.outputs[txInIndex]; - List packedAmount = BigintUtils.toBytes(out.amount, length: 8, order: Endian.little); - final scriptBytes = out.scriptPubKey.toBytes(); - List lenScriptBytes = List.from([scriptBytes.length]); - hashOutputs = List.from([...packedAmount, ...lenScriptBytes, ...scriptBytes]); + final packedAmount = BigintUtils.toBytes(out.amount, length: 8, order: Endian.little); + final scriptBytes = IntUtils.prependVarint(out.scriptPubKey.toBytes()); + hashOutputs = [...packedAmount, ...scriptBytes]; hashOutputs = QuickCrypto.sha256DoubleHash(hashOutputs); } - DynamicByteTracker txForSigning = DynamicByteTracker(); + final txForSigning = DynamicByteTracker(); txForSigning.add(version); txForSigning.add(hashPrevouts); txForSigning.add(hashSequence); final txIn = inputs[txInIndex]; - List txidBytes = List.from(BytesUtils.fromHexString(txIn.txId).reversed.toList()); - txForSigning.add(List.from( - [...txidBytes, ...IntUtils.toBytes(txIn.txIndex, length: 4, byteOrder: Endian.little)])); + final txidBytes = BytesUtils.fromHexString(txIn.txId).reversed.toList(); + txForSigning.add( + [...txidBytes, ...IntUtils.toBytes(txIn.txIndex, length: 4, byteOrder: Endian.little)]); if (token != null) { txForSigning.add(token.toBytes()); } - txForSigning.add(List.from([script.toBytes().length])); - txForSigning.add(script.toBytes()); - List packedAmount = BigintUtils.toBytes(amount, length: 8, order: Endian.little); + final varintBytes = IntUtils.prependVarint(script.toBytes()); + txForSigning.add(varintBytes); + final packedAmount = BigintUtils.toBytes(amount, length: 8, order: Endian.little); txForSigning.add(packedAmount); txForSigning.add(txIn.sequence); txForSigning.add(hashOutputs); txForSigning.add(locktime); txForSigning.add(IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little)); - return QuickCrypto.sha256DoubleHash(txForSigning.toBytes()); } @@ -346,111 +342,95 @@ class BtcTransaction { int leafVar = BitcoinOpCodeConst.LEAF_VERSION_TAPSCRIPT, int sighash = BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL}) { final newTx = copy(this); - bool sighashNone = (sighash & 0x03) == BitcoinOpCodeConst.SIGHASH_NONE; - bool sighashSingle = (sighash & 0x03) == BitcoinOpCodeConst.SIGHASH_SINGLE; - bool anyoneCanPay = (sighash & 0x80) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; - DynamicByteTracker txForSign = DynamicByteTracker(); + final sighashNone = (sighash & 0x03) == BitcoinOpCodeConst.SIGHASH_NONE; + final sighashSingle = (sighash & 0x03) == BitcoinOpCodeConst.SIGHASH_SINGLE; + final anyoneCanPay = (sighash & 0x80) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; + final txForSign = DynamicByteTracker(); txForSign.add([0]); txForSign.add([sighash]); txForSign.add(version); txForSign.add(locktime); - List hashPrevouts = []; - List hashAmounts = []; - List hashScriptPubkeys = []; - List hashSequences = []; - List hashOutputs = []; + var hashPrevouts = []; + var hashAmounts = []; + var hashScriptPubkeys = []; + var hashSequences = []; + var hashOutputs = []; if (!anyoneCanPay) { for (final txin in newTx.inputs) { - List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); - hashPrevouts = List.from([ + final txidBytes = BytesUtils.fromHexString(txin.txId).reversed.toList(); + hashPrevouts = [ ...hashPrevouts, ...txidBytes, ...IntUtils.toBytes(txin.txIndex, length: 4, byteOrder: Endian.little) - ]); + ]; } hashPrevouts = QuickCrypto.sha256Hash(hashPrevouts); txForSign.add(hashPrevouts); for (final i in amounts) { - List bytes = BigintUtils.toBytes(i, length: 8, order: Endian.little); - - hashAmounts = List.from([...hashAmounts, ...bytes]); + final bytes = BigintUtils.toBytes(i, length: 8, order: Endian.little); + hashAmounts = [...hashAmounts, ...bytes]; } hashAmounts = QuickCrypto.sha256Hash(hashAmounts); txForSign.add(hashAmounts); for (final s in scriptPubKeys) { - final h = s.toHex(); - - /// must checked - int scriptLen = h.length ~/ 2; - List scriptBytes = BytesUtils.fromHexString(h); - List lenBytes = List.from([scriptLen]); - hashScriptPubkeys = List.from([...hashScriptPubkeys, ...lenBytes, ...scriptBytes]); + final scriptBytes = IntUtils.prependVarint(s.toBytes()); + hashScriptPubkeys = [...hashScriptPubkeys, ...scriptBytes]; } hashScriptPubkeys = QuickCrypto.sha256Hash(hashScriptPubkeys); txForSign.add(hashScriptPubkeys); for (final txIn in newTx.inputs) { - hashSequences = List.from([...hashSequences, ...txIn.sequence]); + hashSequences = [...hashSequences, ...txIn.sequence]; } hashSequences = QuickCrypto.sha256Hash(hashSequences); txForSign.add(hashSequences); } if (!(sighashNone || sighashSingle)) { for (final txOut in newTx.outputs) { - List packedAmount = BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); - - List scriptBytes = txOut.scriptPubKey.toBytes(); - final lenScriptBytes = List.from([scriptBytes.length]); - hashOutputs = - List.from([...hashOutputs, ...packedAmount, ...lenScriptBytes, ...scriptBytes]); + final packedAmount = BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); + final scriptBytes = IntUtils.prependVarint(txOut.scriptPubKey.toBytes()); + hashOutputs = [...hashOutputs, ...packedAmount, ...scriptBytes]; } hashOutputs = QuickCrypto.sha256Hash(hashOutputs); txForSign.add(hashOutputs); } - final int spendType = extFlags * 2 + 0; - txForSign.add(List.from([spendType])); + final spendType = extFlags * 2 + 0; + txForSign.add([spendType]); if (anyoneCanPay) { final txin = newTx.inputs[txIndex]; - List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); - List result = List.from( - [...txidBytes, ...IntUtils.toBytes(txin.txIndex, length: 4, byteOrder: Endian.little)]); + final txidBytes = BytesUtils.fromHexString(txin.txId).reversed.toList(); + final result = [ + ...txidBytes, + ...IntUtils.toBytes(txin.txIndex, length: 4, byteOrder: Endian.little) + ]; txForSign.add(result); txForSign.add(BigintUtils.toBytes(amounts[txIndex], length: 8, order: Endian.little)); - final sPubKey = scriptPubKeys[txIndex].toHex(); - final sLength = sPubKey.length ~/ 2; - txForSign.add([sLength]); - txForSign.add(BytesUtils.fromHexString(sPubKey)); + final scriptBytes = IntUtils.prependVarint(scriptPubKeys[txIndex].toBytes()); + txForSign.add(scriptBytes); txForSign.add(txin.sequence); } else { - int index = txIndex; - List indexBytes = List.filled(4, 0); - writeUint32LE(index, indexBytes); + final indexBytes = IntUtils.toBytes(txIndex, length: 4, byteOrder: Endian.little); txForSign.add(indexBytes); } if (sighashSingle) { final txOut = newTx.outputs[txIndex]; - - List packedAmount = BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); - final sBytes = txOut.scriptPubKey.toBytes(); - List lenScriptBytes = List.from([sBytes.length]); - - final hashOut = List.from([...packedAmount, ...lenScriptBytes, ...sBytes]); + final packedAmount = BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); + final scriptBytes = IntUtils.prependVarint(txOut.scriptPubKey.toBytes()); + final hashOut = [...packedAmount, ...scriptBytes]; txForSign.add(QuickCrypto.sha256Hash(hashOut)); } if (extFlags == 1) { - final leafVarBytes = - List.from([leafVar, ...IntUtils.prependVarint(script?.toBytes() ?? [])]); - txForSign.add(taggedHash(leafVarBytes, "TapLeaf")); + final leafVarBytes = [leafVar, ...IntUtils.prependVarint(script?.toBytes() ?? [])]; + txForSign.add(taggedHash(leafVarBytes, 'TapLeaf')); txForSign.add([0]); txForSign.add(List.filled(4, mask8)); } final bytes = txForSign.toBytes(); - - return taggedHash(bytes, "TapSighash"); + return taggedHash(bytes, 'TapSighash'); } /// converts result of to_bytes to hexadecimal string @@ -472,16 +452,16 @@ class BtcTransaction { /// Calculates the tx segwit size int getVSize() { if (!hasSegwit) return getSize(); - int markerSize = 2; - int witSize = 0; - List data = []; + const markerSize = 2; + var witSize = 0; + var data = []; for (final w in witnesses) { final countBytes = List.from([w.stack.length]); data = List.from([...data, ...countBytes, ...w.toBytes()]); } witSize = data.length; - int size = getSize() - (markerSize + witSize); - double vSize = size + (markerSize + witSize) / 4; + final size = getSize() - (markerSize + witSize); + final vSize = size + (markerSize + witSize) / 4; return vSize.ceil(); } @@ -492,6 +472,16 @@ class BtcTransaction { return BytesUtils.toHexString(reversedHash); } + Map toJson() { + return { + 'inputs': inputs.map((e) => e.toJson()).toList(), + 'outputs': outputs.map((e) => e.toJson()).toList(), + 'locktime': BytesUtils.toHexString(locktime), + 'version': BytesUtils.toHexString(version), + 'witnesses': witnesses.map((e) => e.toJson()).toList() + }; + } + @override String toString() { return "BtcTransaction{inputs: ${inputs.join(", ")}, outputs: ${outputs.join(", ")}, locktime: ${BytesUtils.toHexString(locktime)}}, version: ${BytesUtils.toHexString(version)}, hasSegwit: $hasSegwit, witnesses:${witnesses.join(",")} "; diff --git a/lib/src/bitcoin/script/witness.dart b/lib/src/bitcoin/script/witness.dart index 88c7aca..7a9ba58 100644 --- a/lib/src/bitcoin/script/witness.dart +++ b/lib/src/bitcoin/script/witness.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:blockchain_utils/helper/extensions/extensions.dart'; import 'package:blockchain_utils/utils/utils.dart'; class ScriptWitness { @@ -17,7 +18,7 @@ class ScriptWitness { /// [stack] the witness items (hex str) list class TxWitnessInput { TxWitnessInput({required List stack, ScriptWitness? scriptWitness}) - : stack = List.unmodifiable(stack), + : stack = stack.immutable, scriptWitness = scriptWitness ?? ScriptWitness(); final List stack; @@ -30,16 +31,20 @@ class TxWitnessInput { /// returns a serialized byte version of the witness items list List toBytes() { - List stackBytes = []; + var stackBytes = []; - for (String item in stack) { - List itemBytes = IntUtils.prependVarint(BytesUtils.fromHexString(item)); + for (final item in stack) { + final itemBytes = IntUtils.prependVarint(BytesUtils.fromHexString(item)); stackBytes = [...stackBytes, ...itemBytes]; } return stackBytes; } + Map toJson() { + return {'stack': stack}; + } + @override String toString() { return "TxWitnessInput{stack: ${stack.join(", ")}}"; diff --git a/lib/src/bitcoin/silent_payments/address.dart b/lib/src/bitcoin/silent_payments/address.dart index 4c3afea..783918e 100644 --- a/lib/src/bitcoin/silent_payments/address.dart +++ b/lib/src/bitcoin/silent_payments/address.dart @@ -17,7 +17,6 @@ class SilentPaymentOwner extends SilentPaymentAddress { factory SilentPaymentOwner.fromPrivateKeys({ required ECPrivate b_scan, required ECPrivate b_spend, - required BasedUtxoNetwork network, int? version, }) { return SilentPaymentOwner( diff --git a/lib/src/bitcoin/silent_payments/utils.dart b/lib/src/bitcoin/silent_payments/utils.dart index 1f68c07..8b82da1 100644 --- a/lib/src/bitcoin/silent_payments/utils.dart +++ b/lib/src/bitcoin/silent_payments/utils.dart @@ -75,13 +75,13 @@ ECPublic? getPubkeyFromInput(VinInfo vin) { break; case P2shAddressType.p2pkhInP2sh: final redeemScript = vin.scriptSig.sublist(1); - if (Script.fromRaw(byteData: redeemScript).getAddressType() == SegwitAddresType.p2wpkh) { + if (Script.fromRaw(bytes: redeemScript).getAddressType() == SegwitAddressType.p2wpkh) { return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); } break; - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: final witnessStack = vin.txinwitness.scriptWitness.stack; if (witnessStack.isNotEmpty) { if (witnessStack.length > 1 && witnessStack.last.buffer.asUint8List()[0] == 0x50) { diff --git a/lib/src/bitcoin_cash/bcmr.dart b/lib/src/bitcoin_cash/bcmr.dart index 58ccc41..9d97032 100644 --- a/lib/src/bitcoin_cash/bcmr.dart +++ b/lib/src/bitcoin_cash/bcmr.dart @@ -4,7 +4,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// Bitcoin Cash Metadata Registries Script to convert uris and content hash to bitcoin output script class BCMR implements BitcoinScriptOutput { /// Bitcoin Cash Metadata Registries PREFIX - static const String _pcmrInHex = "42434d52"; + static const String _pcmrInHex = '42434d52'; BCMR({required this.uris, required this.hash}) : assert( () { @@ -16,8 +16,8 @@ class BCMR implements BitcoinScriptOutput { return true; }(), uris.isEmpty - ? "URIs must not be empty." - : "The BCMR hash should be the SHA-256 hash of the URI contents", + ? 'URIs must not be empty.' + : 'The BCMR hash should be the SHA-256 hash of the URI contents', ); /// list of uris @@ -29,7 +29,7 @@ class BCMR implements BitcoinScriptOutput { /// 'script' property from the interface to define the specific script for Bitcoin Cash Metadata Registries OP_RETURN script. @override Script get script => Script(script: [ - "OP_RETURN", + 'OP_RETURN', _pcmrInHex, hash, ...uris.map((e) => BytesUtils.toHexString(StringUtils.encode(e))) diff --git a/lib/src/bitcoin_cash/bcmr_registery.dart b/lib/src/bitcoin_cash/bcmr_registery.dart index 8d1af2d..3df2009 100644 --- a/lib/src/bitcoin_cash/bcmr_registery.dart +++ b/lib/src/bitcoin_cash/bcmr_registery.dart @@ -314,7 +314,7 @@ class NftCategoryField { const NftCategoryField({required this.identifier}); final Map identifier; factory NftCategoryField.fromJson(Map json) { - final Map parsedIdentifier = {}; + final parsedIdentifier = {}; json.forEach((key, value) { parsedIdentifier[key] = NftCategoryFieldType.fromJson(value); }); @@ -700,26 +700,16 @@ class ChainSnapshot extends IdentitySnapshot { ); } const ChainSnapshot({ - required String name, - required TokenCategory token, - String? description, - List? tags, - String? migrated, - String? status, - String? splitId, - URIs? uris, - Extensions? extensions, - }) : super( - name: name, - description: description, - tags: tags, - migrated: migrated, - token: token, - status: status, - splitId: splitId, - uris: uris, - extensions: extensions, - ); + required super.name, + required TokenCategory super.token, + super.description, + super.tags, + super.migrated, + super.status, + super.splitId, + super.uris, + super.extensions, + }); } class RegistryTimestampKeyedValues { @@ -732,8 +722,7 @@ class RegistryTimestampKeyedValues { } class ChainHistory extends RegistryTimestampKeyedValues { - const ChainHistory({required Map timestampMap}) - : super(timestampMap: timestampMap); + const ChainHistory({required super.timestampMap}); factory ChainHistory.fromJson(Map json) { return ChainHistory( @@ -745,8 +734,7 @@ class ChainHistory extends RegistryTimestampKeyedValues { } class IdentityHistory extends RegistryTimestampKeyedValues { - const IdentityHistory({required Map timestampMap}) - : super(timestampMap: timestampMap); + const IdentityHistory({required super.timestampMap}); factory IdentityHistory.fromJson(Map json) { return IdentityHistory( @@ -989,7 +977,7 @@ class Registry { }); Map toJson() { - final Map js = { + final js = { r'$schema': schema, 'version': version.toJson(), 'latestRevision': latestRevision, @@ -1006,21 +994,21 @@ class Registry { for (final i in identities!.entries) { jsIdentities[i.key] = i.value.toJson(); } - js["identities"] = jsIdentities; + js['identities'] = jsIdentities; } if (chains != null) { final jsChains = {}; for (final i in chains!.entries) { jsChains[i.key] = i.value.toJson(); } - js["chains"] = jsChains; + js['chains'] = jsChains; } if (tags != null) { final jsTags = {}; for (final i in tags!.entries) { jsTags[i.key] = i.value.toJson(); } - js["tags"] = jsTags; + js['tags'] = jsTags; } return js..removeWhere((key, value) => value == null); } diff --git a/lib/src/cash_token/cash_token.dart b/lib/src/cash_token/cash_token.dart index 53f852c..7a86d9b 100644 --- a/lib/src/cash_token/cash_token.dart +++ b/lib/src/cash_token/cash_token.dart @@ -34,17 +34,17 @@ class CashTokenCapability { /// (NFTs without a capability) cannot have their commitment modified when spent. static const CashTokenCapability noCapability = - CashTokenCapability._(0x00, "none"); + CashTokenCapability._(0x00, 'none'); /// Each Mutable token (NFTs with the mutable capability) allows the spending transaction /// to create one NFT of the same category, with any commitment and (optionally) the mutable capability. static const CashTokenCapability mutable = - CashTokenCapability._(0x01, "mutable"); + CashTokenCapability._(0x01, 'mutable'); /// Minting tokens (NFTs with the minting capability) allow the spending transaction to create any number of new NFTs of the same category, /// each with any commitment and (optionally) the minting or mutable capability. static const CashTokenCapability minting = - CashTokenCapability._(0x02, "minting"); + CashTokenCapability._(0x02, 'minting'); /// static int _getCapability(int bitfield) { @@ -60,11 +60,11 @@ class CashTokenCapability { /// correct capality from bitfield static CashTokenCapability fromBitfield(int bitfield) { try { - final int intCapability = _getCapability(bitfield); + final intCapability = _getCapability(bitfield); return values.firstWhere((element) => element.value == intCapability); } on StateError { - throw const BitcoinBasePluginException( - "Invalid CashToken NFT Capability"); + throw const DartBitcoinPluginException( + 'Invalid CashToken NFT Capability'); } } @@ -73,8 +73,8 @@ class CashTokenCapability { try { return values.firstWhere((element) => element.name == name); } on StateError { - throw const BitcoinBasePluginException( - "Invalid CashToken NFT Capability Name"); + throw const DartBitcoinPluginException( + 'Invalid CashToken NFT Capability Name'); } } @@ -111,20 +111,20 @@ class CashTokenUtils { bool hasAmount = false, bool hasCommitmentLength = false, CashTokenCapability? capability}) { - int capabilityInt = 0; + var capabilityInt = 0; if (hasNFT) { if (capability != null) { // throw const MessageException("Capability is necessary for NFTs."); capabilityInt = capability.value; } } - final int nft = hasNFT ? _hasNFT : 0; - int commitmentLength = 0; + final nft = hasNFT ? _hasNFT : 0; + var commitmentLength = 0; if (hasNFT && hasCommitmentLength) { commitmentLength = hasCommitmentLength ? _hasCommitmentLength : 0; } - final int amount = hasAmount ? _hasAmount : 0; + final amount = hasAmount ? _hasAmount : 0; return nft | commitmentLength | amount | capabilityInt; } @@ -160,8 +160,8 @@ class CashTokenUtils { } static Tuple _decodeVarintBigInt(List byteint) { - int ni = byteint[0]; - int size = 0; + final ni = byteint[0]; + var size = 0; if (ni < 253) { return Tuple(BigInt.from(ni), 1); @@ -174,7 +174,7 @@ class CashTokenUtils { } else { size = 8; } - BigInt value = BigintUtils.fromBytes(byteint.sublist(1, 1 + size), + final value = BigintUtils.fromBytes(byteint.sublist(1, 1 + size), byteOrder: Endian.little); return Tuple(value, size + 1); } @@ -208,7 +208,7 @@ class CashTokenUtils { /// [bitfield]: The bitfield to be validated. /// Returns true if the bitfield is valid, otherwise false. static bool isValidBitfield(int bitfield) { - final int highNibble = bitfield & 0xF0; + final highNibble = bitfield & 0xF0; if (highNibble >= 0x80 || highNibble == 0x00) { return false; } @@ -230,17 +230,17 @@ class CashTokenUtils { class CashToken { factory CashToken.fromJson(Map json) { - final String category = json["category"]; - final BigInt amount = BigintUtils.tryParse(json["amount"]) ?? BigInt.zero; + final String category = json['category']; + final amount = BigintUtils.tryParse(json['amount']) ?? BigInt.zero; CashTokenCapability? capability; List? commitment; - if (json.containsKey("nft")) { - capability = CashTokenCapability.fromName(json["nft"]["capability"]); - commitment = ((json["nft"]["commitment"] ?? "") as String).isEmpty + if (json.containsKey('nft')) { + capability = CashTokenCapability.fromName(json['nft']['capability']); + commitment = ((json['nft']['commitment'] ?? '') as String).isEmpty ? null - : BytesUtils.fromHexString(json["nft"]["commitment"]); + : BytesUtils.fromHexString(json['nft']['commitment']); } - int bitfield = CashTokenUtils.buildBitfield( + final bitfield = CashTokenUtils.buildBitfield( hasNFT: capability != null, capability: capability ?? CashTokenCapability.noCapability, hasAmount: amount > BigInt.zero, @@ -261,6 +261,7 @@ class CashToken { /// The commitment contents of the NFT held in this output (0 to 40 bytes). This field is omitted if no NFT is present. final List commitment; final int bitfield; + CashToken.noValidate( {required this.category, required this.amount, @@ -273,31 +274,31 @@ class CashToken { List? commitment, required int bitfield}) { if (!CashTokenUtils.isValidBitfield(bitfield)) { - throw const BitcoinBasePluginException("Invalid bitfield"); + throw const DartBitcoinPluginException('Invalid bitfield'); } if (CashTokenUtils.hasAmount(bitfield) && amount == null) { - throw const BitcoinBasePluginException( - "Invalid cash token: the bitfield indicates an amount, but the amount is null."); + throw const DartBitcoinPluginException( + 'Invalid cash token: the bitfield indicates an amount, but the amount is null.'); } if (amount != null) { if (amount < BigInt.zero || amount > CashTokenUtils.maxTokenAmount) { - throw const BitcoinBasePluginException( - "Invalid amount. Amount must be between zero and 99."); + throw const DartBitcoinPluginException( + 'Invalid amount. Amount must be between zero and 99.'); } } if (!StringUtils.isHexBytes(category)) { - throw const BitcoinBasePluginException( - "Invalid category hexadecimal bytes."); + throw const DartBitcoinPluginException( + 'Invalid category hexadecimal bytes.'); } final toBytes = BytesUtils.fromHexString(category); if (toBytes.length != CashTokenUtils.idBytesLength) { - throw const BitcoinBasePluginException( - "Invalid category. The category should consist of 32 bytes."); + throw const DartBitcoinPluginException( + 'Invalid category. The category should consist of 32 bytes.'); } if (CashTokenUtils.hasCommitmentLength(bitfield) && (commitment == null || commitment.isEmpty)) { - throw const BitcoinBasePluginException( - "Invalid cash token: the bitfield indicates an commitment, but the commitment is null or empty."); + throw const DartBitcoinPluginException( + 'Invalid cash token: the bitfield indicates an commitment, but the commitment is null or empty.'); } return CashToken.noValidate( category: category, @@ -310,14 +311,14 @@ class CashToken { scriptBytes[0] != CashTokenUtils.cashTokenPrefix) { return const Tuple(null, 0); } - int cursor = 1; - List id = + var cursor = 1; + final id = scriptBytes.sublist(cursor, cursor + CashTokenUtils.idBytesLength); cursor += CashTokenUtils.idBytesLength; - final int bitfield = scriptBytes[cursor]; + final bitfield = scriptBytes[cursor]; cursor += 1; - List commitment = []; + var commitment = []; if (CashTokenUtils.hasCommitmentLength(bitfield)) { final vi = IntUtils.decodeVarint( scriptBytes.sublist(cursor, scriptBytes.length)); @@ -325,7 +326,7 @@ class CashToken { commitment = scriptBytes.sublist(cursor, cursor + vi.item1); cursor += vi.item1; } - BigInt amount = BigInt.zero; + var amount = BigInt.zero; if (CashTokenUtils.hasAmount(bitfield)) { final vi = CashTokenUtils._decodeVarintBigInt( scriptBytes.sublist(cursor, scriptBytes.length)); @@ -336,7 +337,7 @@ class CashToken { amount < BigInt.zero || amount > CashTokenUtils.maxTokenAmount || CashTokenUtils.hasCommitmentLength(bitfield) && commitment.isEmpty) { - throw const BitcoinBasePluginException('Invalid cash token'); + throw const DartBitcoinPluginException('Invalid cash token'); } return Tuple( CashToken( @@ -351,7 +352,7 @@ class CashToken { /// /// Returns a list of integers representing the serialized byte representation of the [CashToken]. List toBytes() { - DynamicByteTracker bytes = DynamicByteTracker(); + final bytes = DynamicByteTracker(); bytes.add([CashTokenUtils.cashTokenPrefix]); bytes.add(BytesUtils.fromHexString(category).reversed.toList()); bytes.add([bitfield]); @@ -412,9 +413,17 @@ class CashToken { /// Initialized only if the Cash Token has a commitment length. late final String? commitmentInHex = hasCommitment ? BytesUtils.toHexString(commitment) : null; + Map toJson() { + return { + 'category': category, + 'amount': amount.toString(), + 'bitfield': bitfield, + 'commitment': BytesUtils.toHexString(commitment), + }; + } @override String toString() { - return "CashToken{bitfield: $bitfield, commitment: $commitmentInHex, amount: $amount, category: $category}"; + return 'CashToken{bitfield: $bitfield, commitment: $commitmentInHex, amount: $amount, category: $category}'; } } diff --git a/lib/src/crypto/crypto.dart b/lib/src/crypto/crypto.dart index 2af7a21..c7fd1bc 100644 --- a/lib/src/crypto/crypto.dart +++ b/lib/src/crypto/crypto.dart @@ -1,4 +1,4 @@ -library bitcoin_crypto; +library; import 'package:bitcoin_base/src/bitcoin/script/op_code/constant_lib.dart'; import 'package:blockchain_utils/crypto/quick_crypto.dart'; @@ -9,9 +9,9 @@ export 'keypair/ec_public.dart'; /// Function: taggedHash /// Description: Computes a tagged hash of the input data with a provided tag. /// Input: -/// - List data - The data to be hashed. +/// - `List` data - The data to be hashed. /// - String tag - A unique tag to differentiate the hash. -/// Output: List - The resulting tagged hash. +/// Output: `List` - The resulting tagged hash. /// Note: This function combines the provided tag with the input data to create a unique /// hash by applying a double SHA-256 hash. List taggedHash(List data, String tag) { @@ -30,5 +30,5 @@ List toTapleafTaggedHash(List scriptBytes) { BitcoinOpCodeConst.LEAF_VERSION_TAPSCRIPT, ...IntUtils.prependVarint(scriptBytes) ]; - return taggedHash(leafVarBytes, "TapLeaf"); + return taggedHash(leafVarBytes, 'TapLeaf'); } diff --git a/lib/src/crypto/keypair/ec_private.dart b/lib/src/crypto/keypair/ec_private.dart index c5ace75..91d62cc 100644 --- a/lib/src/crypto/keypair/ec_private.dart +++ b/lib/src/crypto/keypair/ec_private.dart @@ -9,7 +9,7 @@ import 'package:bip32/src/utils/ecurve.dart' as ecc; /// Represents an ECDSA private key. class ECPrivate { final Bip32PrivateKey prive; - ECPrivate(this.prive); + const ECPrivate(this.prive); /// creates an object from hex factory ECPrivate.fromHex(String keyHex) { @@ -45,12 +45,10 @@ class ECPrivate { } /// returns as WIFC (compressed) or WIF format (string) - String toWif({bool compressed = true, BitcoinNetwork? network}) { - List bytes = [...(network ?? BitcoinNetwork.mainnet).wifNetVer, ...toBytes()]; - if (compressed) { - bytes = [...bytes, 0x01]; - } - return Base58Encoder.checkEncode(bytes); + String toWif( + {PubKeyModes pubKeyMode = PubKeyModes.compressed, + BitcoinNetwork network = BitcoinNetwork.mainnet}) { + return WifEncoder.encode(toBytes(), netVer: network.wifNetVer, pubKeyMode: pubKeyMode); } /// returns the key's raw bytes @@ -96,11 +94,20 @@ class ECPrivate { /// sign transaction digest and returns the signature. String signInput(List txDigest, {int sigHash = BitcoinOpCodeConst.SIGHASH_ALL}) { final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signature = btcSigner.signTransaction(txDigest); + var signature = btcSigner.signTransaction(txDigest); signature = [...signature, sigHash]; return BytesUtils.toHexString(signature); } + String signSchnorr(List txDigest, {int sighash = BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL}) { + final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); + var signatur = btcSigner.signSchnorrTransaction(txDigest, tapScripts: [], tweak: false); + if (sighash != BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL) { + signatur = [...signatur, sighash]; + } + return BytesUtils.toHexString(signatur); + } + /// sign taproot transaction digest and returns the signature. String signTapRoot(List txDigest, {int sighash = BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL, @@ -112,16 +119,16 @@ class ECPrivate { } return true; }(), - "When the tweak is false, the `tapScripts` are ignored, to use the tap script path, you need to consider the tweak value to be true."); + 'When the tweak is false, the `tapScripts` are ignored, to use the tap script path, you need to consider the tweak value to be true.'); final tapScriptBytes = !tweak ? [] : tapScripts.map((e) => e.map((e) => e.toBytes()).toList()).toList(); final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signatur = + var signature = btcSigner.signSchnorrTransaction(txDigest, tapScripts: tapScriptBytes, tweak: tweak); if (sighash != BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL) { - signatur = [...signatur, sighash]; + signature = [...signature, sighash]; } - return BytesUtils.toHexString(signatur); + return BytesUtils.toHexString(signature); } ECPrivate toTweakedTaprootKey() { diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index 045f993..60db5c8 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -5,13 +5,15 @@ import 'package:bitcoin_base/src/models/network.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/crypto/crypto/cdsa/point/base.dart'; +typedef PublicKeyType = PubKeyModes; + class ECPublic { final Bip32PublicKey publicKey; const ECPublic._(this.publicKey); factory ECPublic.fromBip32(Bip32PublicKey publicKey) { if (publicKey.curveType != EllipticCurveTypes.secp256k1) { - throw const BitcoinBasePluginException("invalid public key curve for bitcoin"); + throw const DartBitcoinPluginException('invalid public key curve for bitcoin'); } return ECPublic._(publicKey); } @@ -30,8 +32,8 @@ class ECPublic { /// toHex converts the ECPublic key to a hex-encoded string. /// If 'compressed' is true, the key is in compressed format. - String toHex({bool compressed = true}) { - if (compressed) { + String toHex({PublicKeyType mode = PublicKeyType.compressed}) { + if (mode.isCompressed) { return BytesUtils.toHexString(publicKey.compressed); } return BytesUtils.toHexString(publicKey.uncompressed); @@ -39,58 +41,71 @@ class ECPublic { /// _toHash160 computes the RIPEMD160 hash of the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - List _toHash160({bool compressed = true}) { - final bytes = BytesUtils.fromHexString(toHex(compressed: compressed)); + List _toHash160({PublicKeyType mode = PublicKeyType.compressed}) { + final bytes = BytesUtils.fromHexString(toHex(mode: mode)); return QuickCrypto.hash160(bytes); } /// toHash160 computes the RIPEMD160 hash of the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - String toHash160({bool compressed = true}) { - final bytes = BytesUtils.fromHexString(toHex(compressed: compressed)); + String toHash160({PublicKeyType mode = PublicKeyType.compressed}) { + final bytes = BytesUtils.fromHexString(toHex(mode: mode)); return BytesUtils.toHexString(QuickCrypto.hash160(bytes)); } /// toP2pkhAddress generates a P2PKH (Pay-to-Public-Key-Hash) address from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2pkhAddress toP2pkhAddress({bool compressed = true}) { - final h16 = _toHash160(compressed: compressed); + P2pkhAddress toAddress({PublicKeyType mode = PublicKeyType.compressed}) { + final h16 = _toHash160(mode: mode); final toHex = BytesUtils.toHexString(h16); return P2pkhAddress.fromHash160(h160: toHex); } + P2pkhAddress toP2pkhAddress({PublicKeyType mode = PublicKeyType.compressed}) { + return toAddress(mode: mode); + } + /// toP2wpkhAddress generates a P2WPKH (Pay-to-Witness-Public-Key-Hash) SegWit address /// from the ECPublic key. If 'compressed' is true, the key is in compressed format. - P2wpkhAddress toP2wpkhAddress({bool compressed = true}) { - final h16 = _toHash160(compressed: compressed); + P2wpkhAddress toSegwitAddress() { + final h16 = _toHash160(); final toHex = BytesUtils.toHexString(h16); return P2wpkhAddress.fromProgram(program: toHex); } + P2wpkhAddress toP2wpkhAddress() { + return toSegwitAddress(); + } + /// toP2pkAddress generates a P2PK (Pay-to-Public-Key) address from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2pkAddress toP2pkAddress({bool compressed = true}) { - return P2pkAddress(publicKey: this); + P2pkAddress toP2pkAddress({PublicKeyType mode = PublicKeyType.compressed}) { + final h = toHex(mode: mode); + return P2pkAddress(publicKey: ECPublic.fromHex(h)); } /// toRedeemScript generates a redeem script from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - Script toRedeemScript({bool compressed = true}) { - final redeem = toHex(compressed: compressed); - return Script(script: [redeem, "OP_CHECKSIG"]); + Script toRedeemScript({PublicKeyType mode = PublicKeyType.compressed}) { + final redeem = toHex(mode: mode); + return Script(script: [redeem, 'OP_CHECKSIG']); } /// toP2pkhInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2PK (Pay-to-Public-Key) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2shAddress toP2pkhInP2sh({bool compressed = true, useBCHP2sh32 = false}) { - final addr = toP2pkhAddress(compressed: compressed); + P2shAddress toP2pkhInP2sh({ + PublicKeyType mode = PublicKeyType.compressed, + useBCHP2sh32 = false, + }) { + final addr = toAddress(mode: mode); final script = addr.toScriptPubKey(); if (useBCHP2sh32) { return P2shAddress.fromHash160( - h160: BytesUtils.toHexString(QuickCrypto.sha256DoubleHash(script.toBytes())), - type: P2shAddressType.p2pkhInP2sh32); + h160: BytesUtils.toHexString(QuickCrypto.sha256DoubleHash(script.toBytes())), + type: P2shAddressType.p2pkhInP2sh32, + ); } return P2shAddress.fromRedeemScript(script: script, type: P2shAddressType.p2pkhInP2sh); } @@ -98,8 +113,11 @@ class ECPublic { /// toP2pkInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2PK (Pay-to-Public-Key) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2shAddress toP2pkInP2sh({bool compressed = true, bool useBCHP2sh32 = false}) { - final script = toRedeemScript(compressed: compressed); + P2shAddress toP2pkInP2sh({ + PublicKeyType mode = PublicKeyType.compressed, + bool useBCHP2sh32 = false, + }) { + final script = toRedeemScript(mode: mode); if (useBCHP2sh32) { return P2shAddress.fromHash160( h160: BytesUtils.toHexString(QuickCrypto.sha256DoubleHash(script.toBytes())), @@ -123,29 +141,29 @@ class ECPublic { /// 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. - P2shAddress toP2wpkhInP2sh({bool compressed = true}) { - final addr = toP2wpkhAddress(compressed: compressed); + P2shAddress toP2wpkhInP2sh() { + final addr = toSegwitAddress(); return P2shAddress.fromRedeemScript( script: addr.toScriptPubKey(), type: P2shAddressType.p2wpkhInP2sh); } /// toP2wshScript generates a P2WSH (Pay-to-Witness-Script-Hash) script /// derived from the ECPublic key. If 'compressed' is true, the key is in compressed format. - Script toP2wshRedeemScript({bool compressed = true}) { - return Script(script: ['OP_1', toHex(compressed: compressed), "OP_1", "OP_CHECKMULTISIG"]); + Script toP2wshRedeemScript() { + return Script(script: ['OP_1', toHex(), 'OP_1', 'OP_CHECKMULTISIG']); } /// toP2wshAddress generates a P2WSH (Pay-to-Witness-Script-Hash) address /// from the ECPublic key. If 'compressed' is true, the key is in compressed format. - P2wshAddress toP2wshAddress({bool compressed = true}) { - return P2wshAddress.fromRedeemScript(script: toP2wshRedeemScript(compressed: compressed)); + P2wshAddress toP2wshAddress() { + return P2wshAddress.fromRedeemScript(script: toP2wshRedeemScript()); } /// toP2wshInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2WSH (Pay-to-Witness-Script-Hash) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2shAddress toP2wshInP2sh({bool compressed = true}) { - final p2sh = toP2wshAddress(compressed: compressed); + P2shAddress toP2wshInP2sh() { + final p2sh = toP2wshAddress(); return P2shAddress.fromRedeemScript( script: p2sh.toScriptPubKey(), type: P2shAddressType.p2wshInP2sh); } diff --git a/lib/src/exception/exception.dart b/lib/src/exception/exception.dart index f06d838..0e031c3 100644 --- a/lib/src/exception/exception.dart +++ b/lib/src/exception/exception.dart @@ -1,9 +1,5 @@ import 'package:blockchain_utils/blockchain_utils.dart'; -class BitcoinBasePluginException extends BlockchainUtilsException { - @override - final String message; - @override - final Map? details; - const BitcoinBasePluginException(this.message, {this.details}); +class DartBitcoinPluginException extends BlockchainUtilsException { + const DartBitcoinPluginException(super.message, {super.details}); } diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index adeaafc..0607bef 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -1,9 +1,7 @@ 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/bip/bip/bip.dart'; -import 'package:blockchain_utils/bip/coin_conf/coin_conf.dart'; -import 'package:blockchain_utils/bip/coin_conf/coins_conf.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; /// Abstract class representing a base for UTXO-based cryptocurrency networks. abstract class BasedUtxoNetwork implements Enumerate { @@ -25,7 +23,7 @@ abstract class BasedUtxoNetwork implements Enumerate { abstract final List supportedAddress; @override - operator ==(other) { + bool operator ==(other) { if (identical(other, this)) return true; return other is BasedUtxoNetwork && other.runtimeType == runtimeType && value == other.value; } @@ -46,7 +44,9 @@ abstract class BasedUtxoNetwork implements Enumerate { BitcoinCashNetwork.testnet, BitcoinSVNetwork.mainnet, BitcoinSVNetwork.testnet, - PepeNetwork.mainnet + PepeNetwork.mainnet, + ElectraProtocolNetwork.mainnet, + ElectraProtocolNetwork.testnet ]; static BasedUtxoNetwork fromName(String name) { @@ -63,11 +63,11 @@ abstract class BasedUtxoNetwork implements Enumerate { class BitcoinSVNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const BitcoinSVNetwork mainnet = - BitcoinSVNetwork._("BitcoinSVMainnet", CoinsConf.bitcoinSvMainNet); + BitcoinSVNetwork._('BitcoinSVMainnet', CoinsConf.bitcoinSvMainNet); /// Testnet configuration with associated `CoinConf`. static const BitcoinSVNetwork testnet = - BitcoinSVNetwork._("BitcoinSVTestnet", CoinsConf.bitcoinSvTestNet); + BitcoinSVNetwork._('BitcoinSVTestnet', CoinsConf.bitcoinSvTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -114,11 +114,11 @@ class BitcoinSVNetwork implements BasedUtxoNetwork { class BitcoinNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const BitcoinNetwork mainnet = - BitcoinNetwork._("bitcoinMainnet", CoinsConf.bitcoinMainNet); + BitcoinNetwork._('bitcoinMainnet', CoinsConf.bitcoinMainNet); /// Testnet configuration with associated `CoinConf`. static const BitcoinNetwork testnet = - BitcoinNetwork._("bitcoinTestnet", CoinsConf.bitcoinTestNet); + BitcoinNetwork._('bitcoinTestnet', CoinsConf.bitcoinTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -154,10 +154,10 @@ class BitcoinNetwork implements BasedUtxoNetwork { @override List get supportedAddress => [ P2pkhAddressType.p2pkh, - SegwitAddresType.p2wpkh, + SegwitAddressType.p2wpkh, PubKeyAddressType.p2pk, - SegwitAddresType.p2tr, - SegwitAddresType.p2wsh, + SegwitAddressType.p2tr, + SegwitAddressType.p2wsh, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -187,11 +187,11 @@ class BitcoinNetwork implements BasedUtxoNetwork { class LitecoinNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const LitecoinNetwork mainnet = - LitecoinNetwork._("litecoinMainnet", CoinsConf.litecoinMainNet); + LitecoinNetwork._('litecoinMainnet', CoinsConf.litecoinMainNet); /// Testnet configuration with associated `CoinConf`. static const LitecoinNetwork testnet = - LitecoinNetwork._("litecoinTestnet", CoinsConf.litecoinTestNet); + LitecoinNetwork._('litecoinTestnet', CoinsConf.litecoinTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -226,10 +226,10 @@ class LitecoinNetwork implements BasedUtxoNetwork { @override final List supportedAddress = const [ P2pkhAddressType.p2pkh, - SegwitAddresType.p2wpkh, + SegwitAddressType.p2wpkh, PubKeyAddressType.p2pk, - SegwitAddresType.p2wsh, - SegwitAddresType.mweb, + SegwitAddressType.p2wsh, + SegwitAddressType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -248,10 +248,10 @@ class LitecoinNetwork implements BasedUtxoNetwork { /// Class representing a Dash network, implementing the `BasedUtxoNetwork` abstract class. class DashNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. - static const DashNetwork mainnet = DashNetwork._("dashMainnet", CoinsConf.dashMainNet); + static const DashNetwork mainnet = DashNetwork._('dashMainnet', CoinsConf.dashMainNet); /// Testnet configuration with associated `CoinConf`. - static const DashNetwork testnet = DashNetwork._("dashTestnet", CoinsConf.dashTestNet); + static const DashNetwork testnet = DashNetwork._('dashTestnet', CoinsConf.dashTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -275,7 +275,7 @@ class DashNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override String get p2wpkhHrp => - throw const BitcoinBasePluginException("DashNetwork network does not support P2WPKH/P2WSH"); + throw const DartBitcoinPluginException('DashNetwork network does not support P2WPKH/P2WSH'); /// Checks if the current network is the mainnet. @override @@ -303,11 +303,11 @@ class DashNetwork implements BasedUtxoNetwork { class DogecoinNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const DogecoinNetwork mainnet = - DogecoinNetwork._("dogeMainnet", CoinsConf.dogecoinMainNet); + DogecoinNetwork._('dogeMainnet', CoinsConf.dogecoinMainNet); /// Testnet configuration with associated `CoinConf`. static const DogecoinNetwork testnet = - DogecoinNetwork._("dogeTestnet", CoinsConf.dogecoinTestNet); + DogecoinNetwork._('dogeTestnet', CoinsConf.dogecoinTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -333,8 +333,8 @@ class DogecoinNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( - "DogecoinNetwork network does not support P2WPKH/P2WSH"); + String get p2wpkhHrp => throw const DartBitcoinPluginException( + 'DogecoinNetwork network does not support P2WPKH/P2WSH'); /// Checks if the current network is the mainnet. @override @@ -359,11 +359,11 @@ class DogecoinNetwork implements BasedUtxoNetwork { class BitcoinCashNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const BitcoinCashNetwork mainnet = - BitcoinCashNetwork._("bitcoinCashMainnet", CoinsConf.bitcoinCashMainNet); + BitcoinCashNetwork._('bitcoinCashMainnet', CoinsConf.bitcoinCashMainNet); /// Testnet configuration with associated `CoinConf`. static const BitcoinCashNetwork testnet = - BitcoinCashNetwork._("bitcoinCashTestnet", CoinsConf.bitcoinCashTestNet); + BitcoinCashNetwork._('bitcoinCashTestnet', CoinsConf.bitcoinCashTestNet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -402,7 +402,7 @@ class BitcoinCashNetwork implements BasedUtxoNetwork { /// from the associated `CoinConf`. @override String get p2wpkhHrp => - throw const BitcoinBasePluginException("network does not support p2wpkh HRP"); + throw const DartBitcoinPluginException('network does not support p2wpkh HRP'); String get networkHRP => conf.params.p2pkhStdHrp!; @@ -435,7 +435,7 @@ class BitcoinCashNetwork implements BasedUtxoNetwork { /// Class representing a Dogecoin network, implementing the `BasedUtxoNetwork` abstract class. class PepeNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. - static const PepeNetwork mainnet = PepeNetwork._("pepecoinMainnet", CoinsConf.pepeMainnet); + static const PepeNetwork mainnet = PepeNetwork._('pepecoinMainnet', CoinsConf.pepeMainnet); /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. @override @@ -461,8 +461,8 @@ class PepeNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( - "DogecoinNetwork network does not support P2WPKH/P2WSH"); + String get p2wpkhHrp => throw const DartBitcoinPluginException( + 'DogecoinNetwork network does not support P2WPKH/P2WSH'); /// Checks if the current network is the mainnet. @override @@ -484,3 +484,68 @@ class PepeNetwork implements BasedUtxoNetwork { return [Bip44Coins.pepecoinTestnet, Bip49Coins.pepecoinTestnet]; } } + +/// Class representing a Electra Protocol network, implementing the `BasedUtxoNetwork` abstract class. +class ElectraProtocolNetwork implements BasedUtxoNetwork { + /// Mainnet configuration with associated `CoinConf`. + static const ElectraProtocolNetwork mainnet = + ElectraProtocolNetwork._('electraProtocolMainnet', CoinsConf.electraProtocolMainNet); + + /// Testnet configuration with associated `CoinConf`. + static const ElectraProtocolNetwork testnet = + ElectraProtocolNetwork._('electraProtocolTestnet', CoinsConf.electraProtocolTestNet); + + /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. + @override + final CoinConf conf; + @override + final String value; + + /// Constructor for creating a Electra Protocol network with a specific configuration. + const ElectraProtocolNetwork._(this.value, this.conf); + + /// Retrieves the Wallet Import Format (WIF) version bytes from the associated `CoinConf`. + @override + List get wifNetVer => conf.params.wifNetVer!; + + /// Retrieves the Pay-to-Public-Key-Hash (P2PKH) version bytes from the associated `CoinConf`. + @override + List get p2pkhNetVer => conf.params.p2pkhNetVer!; + + /// Retrieves the Pay-to-Script-Hash (P2SH) version bytes from the associated `CoinConf`. + @override + List get p2shNetVer => conf.params.p2shNetVer!; + + /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses + /// from the associated `CoinConf`. + @override + String get p2wpkhHrp => conf.params.p2wpkhHrp!; + + /// Checks if the current network is the mainnet. + @override + bool get isMainnet => this == ElectraProtocolNetwork.mainnet; + + @override + final List supportedAddress = const [ + P2pkhAddressType.p2pkh, + SegwitAddressType.p2wpkh, + PubKeyAddressType.p2pk, + SegwitAddressType.p2wsh, + P2shAddressType.p2wshInP2sh, + P2shAddressType.p2wpkhInP2sh, + P2shAddressType.p2pkhInP2sh, + P2shAddressType.p2pkInP2sh, + ]; + + @override + List get coins { + if (isMainnet) { + return [Bip44Coins.electraProtocol, Bip49Coins.electraProtocol, Bip84Coins.electraProtocol]; + } + return [ + Bip44Coins.electraProtocolTestnet, + Bip49Coins.electraProtocolTestnet, + Bip84Coins.electraProtocolTestnet + ]; + } +} diff --git a/lib/src/provider/api_provider.dart b/lib/src/provider/api_provider.dart index c043b56..a1ef855 100644 --- a/lib/src/provider/api_provider.dart +++ b/lib/src/provider/api_provider.dart @@ -1,5 +1,4 @@ export 'models/models.dart'; -export 'transaction_builder/builder.dart'; -export 'service/services.dart'; -export 'api_provider/providers.dart'; +export 'providers/explorer.dart'; export 'electrum_methods/methods.dart'; +export 'services/explorer.dart'; diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index 2df2205..e4603b2 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -1,6 +1,7 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; import 'package:rxdart/rxdart.dart'; typedef ListenerCallback = StreamSubscription Function( @@ -46,13 +47,13 @@ class ElectrumApiProvider { 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 topDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 1)); + final middleDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 5)); + final bottomDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 10)); - final top = (topDoubleString.toInt() / 1000).round(); - final middle = (middleDoubleString.toInt() / 1000).round(); - final bottom = (bottomDoubleString.toInt() / 1000).round(); + final top = (topDoubleString!.toInt() / 1000).round(); + final middle = (middleDoubleString!.toInt() / 1000).round(); + final bottom = (bottomDoubleString!.toInt() / 1000).round(); return [bottom, middle, top]; } catch (_) { @@ -67,7 +68,7 @@ class ElectrumApiProvider { void ping() async { try { - return await request(ElectrumPing()); + return await request(ElectrumRequestPing()); } catch (_) {} } } diff --git a/lib/src/provider/api_provider/providers.dart b/lib/src/provider/api_provider/providers.dart deleted file mode 100644 index df92a29..0000000 --- a/lib/src/provider/api_provider/providers.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'api_provider.dart'; -export 'electrum_api_provider.dart'; diff --git a/lib/src/provider/constant/constant.dart b/lib/src/provider/constant/constant.dart index 172fdee..f10a9e0 100644 --- a/lib/src/provider/constant/constant.dart +++ b/lib/src/provider/constant/constant.dart @@ -1,18 +1,18 @@ class BtcApiConst { static const String blockCypherBaseURL = - "https://api.blockcypher.com/v1/btc/test3"; - static const String mempoolBaseURL = "https://mempool.space/testnet/api"; + 'https://api.blockcypher.com/v1/btc/test3'; + static const String mempoolBaseURL = 'https://mempool.space/testnet/api'; static const String blockstreamBaseURL = - "https://blockstream.info/testnet/api"; + 'https://blockstream.info/testnet/api'; static const String blockCypherMainBaseURL = - "https://api.blockcypher.com/v1/btc/main"; - static const String mempoolMainBaseURL = "https://mempool.space/api"; - static const String blockstreamMainBaseURL = "https://blockstream.info/api"; + 'https://api.blockcypher.com/v1/btc/main'; + static const String mempoolMainBaseURL = 'https://mempool.space/api'; + static const String blockstreamMainBaseURL = 'https://blockstream.info/api'; // static const String blockCypherDashBaseUri = - "https://api.blockcypher.com/v1/dash/main"; + 'https://api.blockcypher.com/v1/dash/main'; static const String blockCypherDogeBaseUri = - "https://api.blockcypher.com/v1/doge/main"; + 'https://api.blockcypher.com/v1/doge/main'; static const String blockCypherLitecoinBaseUri = - "https://api.blockcypher.com/v1/ltc/main"; + 'https://api.blockcypher.com/v1/ltc/main'; } diff --git a/lib/src/provider/electrum_methods/methods/add_peer.dart b/lib/src/provider/electrum_methods/methods/add_peer.dart index 2488bb1..106046b 100644 --- a/lib/src/provider/electrum_methods/methods/add_peer.dart +++ b/lib/src/provider/electrum_methods/methods/add_peer.dart @@ -1,10 +1,10 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// A newly-started server uses this call to get itself into other servers’ peers lists. /// It should not be used by wallet clients. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumAddPeer extends ElectrumRequest { - ElectrumAddPeer({required this.features}); +class ElectrumRequestAddPeer extends ElectrumRequest { + ElectrumRequestAddPeer({required this.features}); /// The same information that a call to the sender’s server.features() RPC call would return. final Map features; diff --git a/lib/src/provider/electrum_methods/methods/block_headers.dart b/lib/src/provider/electrum_methods/methods/block_headers.dart index 2f24a6a..73a7adc 100644 --- a/lib/src/provider/electrum_methods/methods/block_headers.dart +++ b/lib/src/provider/electrum_methods/methods/block_headers.dart @@ -1,9 +1,12 @@ 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 ElectrumRequestBlockHeaders + extends ElectrumRequest, Map> { + ElectrumRequestBlockHeaders( + {required this.startHeight, required this.count, required this.cpHeight}); /// The height of the first header requested, a non-negative integer. final int startHeight; diff --git a/lib/src/provider/electrum_methods/methods/broad_cast.dart b/lib/src/provider/electrum_methods/methods/broad_cast.dart index 11ae936..29b993f 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -1,10 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Broadcast a transaction to the network. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBroadCastTransaction extends ElectrumRequest { - ElectrumBroadCastTransaction({required this.transactionRaw}); +class ElectrumRequestBroadCastTransaction extends ElectrumRequest { + ElectrumRequestBroadCastTransaction({required this.transactionRaw}); /// The raw transaction as a hexadecimal string. final String transactionRaw; diff --git a/lib/src/provider/electrum_methods/methods/donate_address.dart b/lib/src/provider/electrum_methods/methods/donate_address.dart index f66127f..21ba0bd 100644 --- a/lib/src/provider/electrum_methods/methods/donate_address.dart +++ b/lib/src/provider/electrum_methods/methods/donate_address.dart @@ -1,8 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a server donation address. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumDonationAddress extends ElectrumRequest { +class ElectrumRequestDonationAddress extends ElectrumRequest { /// server.donation_address @override String get method => ElectrumRequestMethods.serverDontionAddress.method; diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index d080b7d..1df15f8 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -1,9 +1,12 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumVersion extends ElectrumRequest, List> { - ElectrumVersion({required this.clientName, required this.protocolVersion}); +class ElectrumRequestVersion + extends ElectrumRequest, List> { + ElectrumRequestVersion( + {required this.clientName, required this.protocolVersion}); /// A string identifying the connecting client software. final String clientName; diff --git a/lib/src/provider/electrum_methods/methods/estimate_fee.dart b/lib/src/provider/electrum_methods/methods/estimate_fee.dart index 5e59971..30eed89 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -3,8 +3,8 @@ import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumEstimateFee extends ElectrumRequest { - ElectrumEstimateFee({this.numberOfBlock = 2}); +class ElectrumRequestEstimateFee extends ElectrumRequest { + ElectrumRequestEstimateFee({this.numberOfBlock = 2}); /// The number of blocks to target for confirmation. final int numberOfBlock; @@ -20,7 +20,9 @@ class ElectrumEstimateFee extends ElectrumRequest { /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResponse(result) { - return BtcUtils.toSatoshi(result.toString()).abs(); + BigInt? onResponse(result) { + final fee = BtcUtils.toSatoshi(result.toString()); + if (fee.isNegative) return null; + return fee; } } diff --git a/lib/src/provider/electrum_methods/methods/get_balance.dart b/lib/src/provider/electrum_methods/methods/get_balance.dart index a8a0ef9..cc12b03 100644 --- a/lib/src/provider/electrum_methods/methods/get_balance.dart +++ b/lib/src/provider/electrum_methods/methods/get_balance.dart @@ -1,11 +1,11 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return the confirmed and unconfirmed balances of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetScriptHashBalance +class ElectrumRequestGetScriptHashBalance extends ElectrumRequest, Map> { - ElectrumGetScriptHashBalance({required this.scriptHash}); + ElectrumRequestGetScriptHashBalance({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; 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 512da7a..7baf063 100644 --- a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart +++ b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart @@ -1,9 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a histogram of the fee rates paid by transactions in the memory pool, weighted by transaction size. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetFeeHistogram extends ElectrumRequest>, List> { +class ElectrumRequestGetFeeHistogram extends ElectrumRequest>, List> { /// mempool.get_fee_histogram @override String get method => ElectrumRequestMethods.getFeeHistogram.method; @@ -18,7 +18,7 @@ class ElectrumGetFeeHistogram extends ElectrumRequest>, List> onResponse(result) { - return result.map((e) => List.from(e)).toList(); + List> onResponse(result) { + return result.map((e) => (e as List).map((e) => (e as num).toDouble()).toList()).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_history.dart b/lib/src/provider/electrum_methods/methods/get_history.dart index ccf3ccd..4d99d77 100644 --- a/lib/src/provider/electrum_methods/methods/get_history.dart +++ b/lib/src/provider/electrum_methods/methods/get_history.dart @@ -1,11 +1,11 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return the confirmed and unconfirmed history of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetHistory +class ElectrumRequestScriptHashGetHistory extends ElectrumRequest>, List> { - ElectrumScriptHashGetHistory({required this.scriptHash}); + ElectrumRequestScriptHashGetHistory({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; diff --git a/lib/src/provider/electrum_methods/methods/get_mempool.dart b/lib/src/provider/electrum_methods/methods/get_mempool.dart index 3fc91a7..a68e2a4 100644 --- a/lib/src/provider/electrum_methods/methods/get_mempool.dart +++ b/lib/src/provider/electrum_methods/methods/get_mempool.dart @@ -1,11 +1,11 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return the unconfirmed transactions of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetMempool +class ElectrumRequestScriptHashGetMempool extends ElectrumRequest>, List> { - ElectrumScriptHashGetMempool({required this.scriptHash}); + ElectrumRequestScriptHashGetMempool({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; diff --git a/lib/src/provider/electrum_methods/methods/get_merkle.dart b/lib/src/provider/electrum_methods/methods/get_merkle.dart index a4d196c..409cc54 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -1,10 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return the merkle branch to a confirmed transaction given its hash and height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetMerkle extends ElectrumRequest, Map> { - ElectrumGetMerkle({required this.transactionHash, required this.height}); +class ElectrumRequestGetMerkle extends ElectrumRequest, Map> { + ElectrumRequestGetMerkle({required this.transactionHash, required this.height}); /// The transaction hash as a hexadecimal string. final String transactionHash; diff --git a/lib/src/provider/electrum_methods/methods/get_transaction.dart b/lib/src/provider/electrum_methods/methods/get_transaction.dart index 2fbb74a..e8393f1 100644 --- a/lib/src/provider/electrum_methods/methods/get_transaction.dart +++ b/lib/src/provider/electrum_methods/methods/get_transaction.dart @@ -1,10 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a raw transaction. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetTransactionHex extends ElectrumRequest { - ElectrumGetTransactionHex({required this.transactionHash}); +class ElectrumRequestGetTransaction extends ElectrumRequest { + ElectrumRequestGetTransaction({required this.transactionHash}); /// The transaction hash as a hexadecimal string. final String transactionHash; diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index c9e3dd1..8a75094 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -1,11 +1,12 @@ import 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return an ordered list of UTXOs sent to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashListUnspent extends ElectrumRequest, List> { - ElectrumScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); +class ElectrumRequestScriptHashListUnspent + extends ElectrumRequest, List> { + ElectrumRequestScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -19,7 +20,7 @@ class ElectrumScriptHashListUnspent extends ElectrumRequest, @override List toJson() { - return [scriptHash, if (includeTokens) "include_tokens"]; + return [scriptHash, if (includeTokens) 'include_tokens']; } /// A list of unspent outputs in blockchain order. @@ -28,7 +29,7 @@ class ElectrumScriptHashListUnspent extends ElectrumRequest, /// Any output that is spent in the mempool does not appear. @override List onResponse(result) { - final List utxos = result.map((e) => ElectrumUtxo.fromJson(e)).toList(); + final utxos = result.map((e) => ElectrumUtxo.fromJson(e)).toList(); return utxos; } } diff --git a/lib/src/provider/electrum_methods/methods/get_value_proof.dart b/lib/src/provider/electrum_methods/methods/get_value_proof.dart index 8bfe134..edf7faf 100644 --- a/lib/src/provider/electrum_methods/methods/get_value_proof.dart +++ b/lib/src/provider/electrum_methods/methods/get_value_proof.dart @@ -1,10 +1,12 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns a name resolution proof, suitable for low-latency (single round-trip) resolution. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetValueProof +class ElectrumRequestGetValueProof extends ElectrumRequest, dynamic> { - ElectrumGetValueProof({required this.scriptHash, required this.cpHeight}); + ElectrumRequestGetValueProof( + {required this.scriptHash, required this.cpHeight}); /// Script hash of the name being resolved. final String scriptHash; diff --git a/lib/src/provider/electrum_methods/methods/header.dart b/lib/src/provider/electrum_methods/methods/header.dart index f961340..28a832f 100644 --- a/lib/src/provider/electrum_methods/methods/header.dart +++ b/lib/src/provider/electrum_methods/methods/header.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return the block header at the given height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeader extends ElectrumRequest { - ElectrumBlockHeader({required this.startHeight, required this.cpHeight}); +class ElectrumRequestBlockHeader extends ElectrumRequest { + ElectrumRequestBlockHeader( + {required this.startHeight, required this.cpHeight}); final int startHeight; final int cpHeight; diff --git a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart index 2d681b5..1b4351e 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -17,7 +17,7 @@ class ElectrumHeaderResponse { /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumHeaderSubscribe +class ElectrumRequestHeaderSubscribe extends ElectrumRequest> { /// blockchain.headers.subscribe @override 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 cf7b2e8..79589ce 100644 --- a/lib/src/provider/electrum_methods/methods/id_from_pos.dart +++ b/lib/src/provider/electrum_methods/methods/id_from_pos.dart @@ -1,10 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a transaction hash and optionally a merkle proof, given a block height and a position in the block. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumIdFromPos extends ElectrumRequest { - ElectrumIdFromPos( +class ElectrumRequestIdFromPos extends ElectrumRequest { + ElectrumRequestIdFromPos( {required this.height, required this.txPos, this.merkle = false}); /// The main chain block height, a non-negative integer. 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 64be82a..bda9b6c 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart @@ -1,11 +1,13 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Pass through the masternode announce message to be broadcast by the daemon. /// Whenever a masternode comes online or a client is syncing, /// they will send this message which describes the masternode entry and how to validate messages from it. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeAnnounceBroadcast extends ElectrumRequest { - ElectrumMasternodeAnnounceBroadcast({required this.signmnb}); +class ElectrumRequestMasternodeAnnounceBroadcast + extends ElectrumRequest { + ElectrumRequestMasternodeAnnounceBroadcast({required this.signmnb}); final String signmnb; /// masternode.announce.broadcast diff --git a/lib/src/provider/electrum_methods/methods/masternode_list.dart b/lib/src/provider/electrum_methods/methods/masternode_list.dart index 316767d..70e25e0 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_list.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_list.dart @@ -1,9 +1,10 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns the list of masternodes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeList extends ElectrumRequest, List> { - ElectrumMasternodeList({required this.payees}); +class ElectrumRequestMasternodeList extends ElectrumRequest, List> { + ElectrumRequestMasternodeList({required this.payees}); /// An array of masternode payee addresses. final List payees; diff --git a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart index e9b1445..27d5d52 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns the status of masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeSubscribe extends ElectrumRequest { - ElectrumMasternodeSubscribe({required this.collateral}); +class ElectrumRequestMasternodeSubscribe + extends ElectrumRequest { + ElectrumRequestMasternodeSubscribe({required this.collateral}); /// The txId and the index of the collateral. Example ("8c59133e714797650cf69043d05e409bbf45670eed7c4e4a386e52c46f1b5e24-0") final String collateral; diff --git a/lib/src/provider/electrum_methods/methods/ping.dart b/lib/src/provider/electrum_methods/methods/ping.dart index d29fdb8..6283845 100644 --- a/lib/src/provider/electrum_methods/methods/ping.dart +++ b/lib/src/provider/electrum_methods/methods/ping.dart @@ -1,8 +1,9 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Ping the server to ensure it is responding, and to keep the session alive. The server may disconnect clients that have sent no requests for roughly 10 minutes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumPing extends ElectrumRequest { +class ElectrumRequestPing extends ElectrumRequest { @override String get method => ElectrumRequestMethods.ping.method; diff --git a/lib/src/provider/electrum_methods/methods/protx_diff.dart b/lib/src/provider/electrum_methods/methods/protx_diff.dart index 7004fae..08f91c1 100644 --- a/lib/src/provider/electrum_methods/methods/protx_diff.dart +++ b/lib/src/provider/electrum_methods/methods/protx_diff.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns a diff between two deterministic masternode lists. The result also contains proof data.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXDiff extends ElectrumRequest, dynamic> { - ElectrumProtXDiff({required this.baseHeight, required this.height}); +class ElectrumRequestProtXDiff + extends ElectrumRequest, dynamic> { + ElectrumRequestProtXDiff({required this.baseHeight, required this.height}); /// The starting block height final int baseHeight; diff --git a/lib/src/provider/electrum_methods/methods/protx_info.dart b/lib/src/provider/electrum_methods/methods/protx_info.dart index 1f78505..79f7b81 100644 --- a/lib/src/provider/electrum_methods/methods/protx_info.dart +++ b/lib/src/provider/electrum_methods/methods/protx_info.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns detailed information about a deterministic masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXInfo extends ElectrumRequest, dynamic> { - ElectrumProtXInfo({required this.protxHash}); +class ElectrumRequestProtXInfo + extends ElectrumRequest, dynamic> { + ElectrumRequestProtXInfo({required this.protxHash}); /// The hash of the initial ProRegTx final String protxHash; diff --git a/lib/src/provider/electrum_methods/methods/relay_fee.dart b/lib/src/provider/electrum_methods/methods/relay_fee.dart index aea1478..73123bf 100644 --- a/lib/src/provider/electrum_methods/methods/relay_fee.dart +++ b/lib/src/provider/electrum_methods/methods/relay_fee.dart @@ -1,9 +1,10 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumRelayFee extends ElectrumRequest { +class ElectrumRequestRelayFee extends ElectrumRequest { /// blockchain.relayfee @override String get method => ElectrumRequestMethods.relayFee.method; diff --git a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart index 90cfcb8..54b50da 100644 --- a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart +++ b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart @@ -1,9 +1,10 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Unsubscribe from a script hash, preventing future notifications if its status changes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashUnSubscribe extends ElectrumRequest { - ElectrumScriptHashUnSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashUnSubscribe extends ElectrumRequest { + ElectrumRequestScriptHashUnSubscribe({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; diff --git a/lib/src/provider/electrum_methods/methods/server_banner.dart b/lib/src/provider/electrum_methods/methods/server_banner.dart index 4a2c0ac..8bc5b73 100644 --- a/lib/src/provider/electrum_methods/methods/server_banner.dart +++ b/lib/src/provider/electrum_methods/methods/server_banner.dart @@ -1,8 +1,9 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a banner to be shown in the Electrum console. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerBanner extends ElectrumRequest { +class ElectrumRequestServerBanner extends ElectrumRequest { @override String get method => ElectrumRequestMethods.serverBanner.method; diff --git a/lib/src/provider/electrum_methods/methods/server_features.dart b/lib/src/provider/electrum_methods/methods/server_features.dart index 725d6ef..94d3c9c 100644 --- a/lib/src/provider/electrum_methods/methods/server_features.dart +++ b/lib/src/provider/electrum_methods/methods/server_features.dart @@ -1,8 +1,9 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a list of features and services supported by the server. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerFeatures extends ElectrumRequest { +class ElectrumRequestServerFeatures extends ElectrumRequest { /// server.features @override String get method => ElectrumRequestMethods.serverFeatures.method; 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 04e8709..a924cd4 100644 --- a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart @@ -1,8 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a list of peer servers. Despite the name this is not a subscription and the server must send no notifications.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerPeersSubscribe extends ElectrumRequest, List> { +class ElectrumRequestServerPeersSubscribe extends ElectrumRequest, List> { /// server.peers.subscribe @override String get method => ElectrumRequestMethods.serverPeersSubscribe.method; diff --git a/lib/src/provider/electrum_methods/methods/status.dart b/lib/src/provider/electrum_methods/methods/status.dart index 2967676..aca06cb 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/status.dart @@ -1,9 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashSubscribe extends ElectrumRequest { - ElectrumScriptHashSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashSubscribe extends ElectrumRequest, dynamic> { + ElectrumRequestScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -19,7 +19,7 @@ class ElectrumScriptHashSubscribe extends ElectrumRequest { /// The status of the script hash. @override - String onResponse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/models/block_cypher/block_cypher_models.dart b/lib/src/provider/models/block_cypher/block_cypher_models.dart index 5d4761b..759c8f3 100644 --- a/lib/src/provider/models/block_cypher/block_cypher_models.dart +++ b/lib/src/provider/models/block_cypher/block_cypher_models.dart @@ -99,7 +99,7 @@ class BlockCypherUtxo { } List toUtxoWithOwner(UtxoAddressDetails owner) { - List utxos = txRefs.map((ref) { + final utxos = txRefs.map((ref) { return UtxoWithAddress( utxo: ref.toUtxo(owner.address.type), ownerDetails: owner, diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 3421194..057b2dd 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -5,19 +5,6 @@ import 'package:bitcoin_base/src/provider/constant/constant.dart'; enum APIType { mempool, blockCypher } class APIConfig { - APIConfig({ - required this.url, - required this.feeRate, - required this.transaction, - required this.transactions, - required this.sendTransaction, - required this.apiType, - required this.network, - required this.blockHeight, - this.block, - this.blockTimestamp, - }); - final String url; final String feeRate; final String transaction; @@ -39,8 +26,8 @@ class APIConfig { } String getUtxoUrl(String address) { - String baseUrl = url; - return baseUrl.replaceAll("###", address); + final baseUrl = url; + return baseUrl.replaceAll('###', address); } String getFeeApiUrl() { @@ -48,13 +35,13 @@ class APIConfig { } String getTransactionUrl(String transactionId) { - String baseUrl = transaction; - return baseUrl.replaceAll("###", transactionId); + final baseUrl = transaction; + return baseUrl.replaceAll('###', transactionId); } String getBlockUrl(String blockHash) { if (block == null) { - throw const BitcoinBasePluginException("block url is not available"); + throw const DartBitcoinPluginException("block url is not available"); } String baseUrl = block!; @@ -63,7 +50,7 @@ class APIConfig { String getBlockTimestampUrl(int timestamp) { if (blockTimestamp == null) { - throw const BitcoinBasePluginException("block timestamp url is not available"); + throw const DartBitcoinPluginException("block timestamp url is not available"); } String baseUrl = blockTimestamp!; @@ -71,13 +58,13 @@ class APIConfig { } String getTransactionsUrl(String address) { - String baseUrl = transactions; - return baseUrl.replaceAll("###", address); + final baseUrl = transactions; + return baseUrl.replaceAll('###', address); } String getBlockHeight(int blockHaight) { - String baseUrl = blockHeight; - return baseUrl.replaceAll("###", "$blockHaight"); + final baseUrl = blockHeight; + return baseUrl.replaceAll('###', '$blockHaight'); } factory APIConfig.fromBlockCypher(BasedUtxoNetwork network) { @@ -99,8 +86,8 @@ class APIConfig { baseUrl = BtcApiConst.blockCypherLitecoinBaseUri; break; default: - throw BitcoinBasePluginException( - "blockcypher does not support ${network.conf.coinName.name}, u must use your own provider"); + throw DartBitcoinPluginException( + 'blockcypher does not support ${network.conf.coinName.name}, u must use your own provider'); } return APIConfig( @@ -125,7 +112,7 @@ class APIConfig { baseUrl = BtcApiConst.mempoolBaseURL; break; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "mempool does not support ${network.conf.coinName.name}"); } } @@ -143,4 +130,17 @@ class APIConfig { blockTimestamp: "$baseUrl/mining/blocks/timestamp/###", ); } + + APIConfig({ + required this.url, + required this.feeRate, + required this.transaction, + required this.transactions, + required this.sendTransaction, + required this.apiType, + required this.network, + required this.blockHeight, + this.block, + this.blockTimestamp, + }); } diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart index 9ec03d7..2607381 100644 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ b/lib/src/provider/models/electrum/electrum_utxo.dart @@ -5,14 +5,14 @@ import 'package:bitcoin_base/src/provider/api_provider.dart'; class ElectrumUtxo implements UTXO { factory ElectrumUtxo.fromJson(Map json) { CashToken? token; - if (json.containsKey("token_data")) { - token = CashToken.fromJson(json["token_data"]); + if (json.containsKey('token_data')) { + token = CashToken.fromJson(json['token_data']); } return ElectrumUtxo._( - height: json["height"], - txId: json["tx_hash"], - vout: json["tx_pos"], - value: BigInt.parse((json["value"].toString())), + height: json['height'], + txId: json['tx_hash'], + vout: json['tx_pos'], + value: BigInt.parse((json['value'].toString())), token: token); } const ElectrumUtxo._( diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 910a825..3a18446 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -115,8 +115,8 @@ BigInt _parseMempoolFees(dynamic data) { } else if (data is int) { return BigInt.from((data * kb)); } else { - throw BitcoinBasePluginException( - "cannot parse mempool fees excepted double, string got ${data.runtimeType}"); + throw DartBitcoinPluginException( + 'cannot parse mempool fees excepted double, string got ${data.runtimeType}'); } } diff --git a/lib/src/provider/models/mempool/mempol_models.dart b/lib/src/provider/models/mempool/mempol_models.dart index b1ea5b5..46cd8c8 100644 --- a/lib/src/provider/models/mempool/mempol_models.dart +++ b/lib/src/provider/models/mempool/mempol_models.dart @@ -186,7 +186,7 @@ class MempolUtxo implements UTXO { extension MempoolUtxoExtentions on List { List toUtxoWithOwnerList(UtxoAddressDetails owner) { - List utxos = map((e) => UtxoWithAddress( + final utxos = map((e) => UtxoWithAddress( utxo: BitcoinUtxo( txHash: e.txid, value: e.value, diff --git a/lib/src/provider/models/multisig_script.dart b/lib/src/provider/models/multisig_script.dart index 0fb091a..de956b4 100644 --- a/lib/src/provider/models/multisig_script.dart +++ b/lib/src/provider/models/multisig_script.dart @@ -6,7 +6,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// signers in a multi-signature scheme. A multi-signature signer typically includes /// information about their public key and weight within the scheme. class MultiSignatureSigner { - MultiSignatureSigner._(this.publicKey, this.weight); + MultiSignatureSigner._(this.publicKey, this.weight, this.keyType); /// PublicKey returns the public key associated with the signer. final String publicKey; @@ -15,12 +15,15 @@ class MultiSignatureSigner { /// The weight is used to determine the number of signatures required for a valid transaction. final int weight; + final PublicKeyType keyType; + /// creates a new instance of a multi-signature signer with the /// specified public key and weight. factory MultiSignatureSigner( {required String publicKey, required int weight}) { ECPublic.fromHex(publicKey); - return MultiSignatureSigner._(publicKey, weight); + return MultiSignatureSigner._( + publicKey, weight, BtcUtils.isCompressedPubKey(publicKey)); } } @@ -34,6 +37,7 @@ class MultiSignatureAddress { P2shAddressType.p2pkhInP2shwt, P2shAddressType.p2pkhInP2sh32wt ]; + final bool canSelectSegwit; /// Signers is a collection of signers participating in the multi-signature scheme. final List signers; @@ -51,8 +55,12 @@ class MultiSignatureAddress { BitcoinBaseAddress toP2wshAddress({required BasedUtxoNetwork network}) { if (network is! LitecoinNetwork && network is! BitcoinNetwork) { - throw BitcoinBasePluginException( - "${network.conf.coinName.name} Bitcoin forks that do not support Segwit. use toP2shAddress"); + throw DartBitcoinPluginException( + '${network.conf.coinName.name} Bitcoin forks that do not support Segwit. use toP2shAddress'); + } + if (!canSelectSegwit) { + throw const DartBitcoinPluginException( + "One of the signer's accounts used an uncompressed public key."); } return P2wshAddress.fromScriptPubkey(script: multiSigScript); } @@ -66,7 +74,7 @@ class MultiSignatureAddress { BitcoinBaseAddress toP2shAddress( [P2shAddressType addressType = P2shAddressType.p2pkhInP2sh]) { if (!legacySupportP2shTypes.contains(addressType)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "invalid p2sh type please use one of them ${legacySupportP2shTypes.map((e) => "$e").join(", ")}"); } @@ -79,12 +87,11 @@ class MultiSignatureAddress { return P2shAddress.fromScriptPubkey(script: multiSigScript, type: addressType); } - BitcoinBaseAddress fromType({ - required BasedUtxoNetwork network, - required BitcoinAddressType addressType, - }) { + BitcoinBaseAddress fromType( + {required BasedUtxoNetwork network, + required BitcoinAddressType addressType}) { switch (addressType) { - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return toP2wshAddress(network: network); case P2shAddressType.p2wshInP2sh: return toP2wshInP2shAddress(network: network); @@ -94,37 +101,35 @@ class MultiSignatureAddress { case P2shAddressType.p2pkhInP2sh32wt: return toP2shAddress(addressType as P2shAddressType); default: - throw const BitcoinBasePluginException( - "invalid multisig address type. use of of them [BitcoinAddressType.p2wsh, BitcoinAddressType.p2wshInP2sh, BitcoinAddressType.p2pkhInP2sh]"); + throw const DartBitcoinPluginException( + 'invalid multisig address type. use of of them [BitcoinAddressType.p2wsh, BitcoinAddressType.p2wshInP2sh, BitcoinAddressType.p2pkhInP2sh]'); } } MultiSignatureAddress._( {required this.signers, required this.threshold, - // required this.address, - required this.multiSigScript}); + required this.multiSigScript, + required this.canSelectSegwit}); /// CreateMultiSignatureAddress creates a new instance of a MultiSignatureAddress, representing /// a multi-signature Bitcoin address configuration. It allows you to specify the minimum number /// of required signatures (threshold), provide the collection of signers participating in the /// multi-signature scheme, and specify the address type. - factory MultiSignatureAddress({ - required int threshold, - required List signers, - }) { + factory MultiSignatureAddress( + {required int threshold, required List signers}) { final sumWeight = signers.fold(0, (sum, signer) => sum + signer.weight); if (threshold > 16 || threshold < 1) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The threshold should be between 1 and 16'); } if (sumWeight > 16) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The total weight of the owners should not exceed 16'); } if (sumWeight < threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The total weight of the signatories should reach the threshold'); } final multiSigScript = ['OP_$threshold']; @@ -136,6 +141,9 @@ class MultiSignatureAddress { multiSigScript.addAll(['OP_$sumWeight', 'OP_CHECKMULTISIG']); final script = Script(script: multiSigScript); return MultiSignatureAddress._( - signers: signers, threshold: threshold, multiSigScript: script); + signers: signers, + threshold: threshold, + multiSigScript: script, + canSelectSegwit: signers.every((e) => e.keyType.isCompressed)); } } diff --git a/lib/src/provider/models/utxo_details.dart b/lib/src/provider/models/utxo_details.dart index f6299e3..501bf6a 100644 --- a/lib/src/provider/models/utxo_details.dart +++ b/lib/src/provider/models/utxo_details.dart @@ -7,7 +7,7 @@ abstract class UTXO { class UtxoAddressDetails { /// PublicKey is the public key associated with the UTXO owner. - final String? _publicKey; + final String? publicKey; /// Address is the Bitcoin address associated with the UTXO owner. final BitcoinBaseAddress address; @@ -16,19 +16,22 @@ class UtxoAddressDetails { /// associated with the UTXO owner. It may be null if the UTXO owner is not using a multi-signature scheme. final MultiSignatureAddress? _multiSigAddress; - UtxoAddressDetails({ - required String publicKey, + UtxoAddressDetails._({ + required String this.publicKey, required this.address, - }) : _multiSigAddress = null, - _publicKey = publicKey; + }) : _multiSigAddress = null; + factory UtxoAddressDetails({required String publicKey, required BitcoinBaseAddress address}) { + ECPublic.fromHex(publicKey); + return UtxoAddressDetails._(publicKey: publicKey, address: address); + } UtxoAddressDetails.multiSigAddress( {required MultiSignatureAddress multiSigAddress, required this.address}) - : _publicKey = null, + : publicKey = null, _multiSigAddress = multiSigAddress; UtxoAddressDetails.watchOnly(this.address) - : _publicKey = null, + : publicKey = null, _multiSigAddress = null; } @@ -41,30 +44,37 @@ class UtxoWithAddress { /// OwnerDetails is a UtxoAddressDetails instance containing information about the UTXO owner. final UtxoAddressDetails ownerDetails; - UtxoWithAddress({ - required this.utxo, - required this.ownerDetails, - }); + const UtxoWithAddress._({required this.utxo, required this.ownerDetails, required this.keyType}); + factory UtxoWithAddress({required BitcoinUtxo utxo, required UtxoAddressDetails ownerDetails}) { + return UtxoWithAddress._( + utxo: utxo, + ownerDetails: ownerDetails, + keyType: ownerDetails.publicKey != null && !utxo.isSegwit + ? BtcUtils.isCompressedPubKey(ownerDetails.publicKey!) + : PublicKeyType.compressed); + } ECPublic public() { if (isMultiSig()) { - throw const BitcoinBasePluginException("Cannot access public key in multi-signature address"); + throw const DartBitcoinPluginException('Cannot access public key in multi-signature address'); } - if (ownerDetails._publicKey == null) { - throw const BitcoinBasePluginException( - "Cannot access public key in watch only address; use UtxoAddressDetails constractor instead `UtxoAddressDetails.watchOnly`"); + if (ownerDetails.publicKey == null) { + throw const DartBitcoinPluginException( + 'Cannot access public key in watch only address; use UtxoAddressDetails constractor instead `UtxoAddressDetails.watchOnly`'); } - return ECPublic.fromHex(ownerDetails._publicKey!); + return ECPublic.fromHex(ownerDetails.publicKey!); } + final PublicKeyType keyType; + bool isMultiSig() { return ownerDetails._multiSigAddress != null; } MultiSignatureAddress get multiSigAddress => isMultiSig() ? ownerDetails._multiSigAddress! - : throw const BitcoinBasePluginException( - "The address is not associated with a multi-signature setup"); + : throw const DartBitcoinPluginException( + 'The address is not associated with a multi-signature setup'); } /// Abstract base class representing a generic Bitcoin output. @@ -122,10 +132,7 @@ class BitcoinScriptOutput implements BitcoinBaseOutput { /// The value (amount) of the Bitcoin output. final BigInt value; - const BitcoinScriptOutput({ - required this.script, - required this.value, - }); + const BitcoinScriptOutput({required this.script, required this.value}); /// Convert the custom script output to a standard TxOutput. @override @@ -142,12 +149,8 @@ class BitcoinTokenOutput implements BitcoinSpendableBaseOutput { final BigInt value; final CashToken token; final String? utxoHash; - BitcoinTokenOutput({ - required this.address, - required this.value, - required this.token, - this.utxoHash, - }); + BitcoinTokenOutput( + {required this.address, required this.value, required this.token, this.utxoHash}); /// Convert the custom script output to a standard TxOutput. @override @@ -170,11 +173,7 @@ class BitcoinBurnableOutput extends BitcoinBaseOutput { /// The value (amount) of the burnable output (optional only for token with hasAmount flags). final BigInt? value; - BitcoinBurnableOutput({ - required this.categoryID, - this.utxoHash, - this.value, - }); + BitcoinBurnableOutput({required this.categoryID, this.utxoHash, this.value}); @override TxOutput get toOutput => throw UnimplementedError(); @@ -202,32 +201,49 @@ class BitcoinUtxo { /// BlockHeight represents the block height at which this UTXO was confirmed. final int? blockHeight; - BitcoinUtxo({ + BitcoinUtxo._({ required this.txHash, required this.value, required this.vout, required this.scriptType, this.blockHeight, this.token, + required this.isP2tr, + required this.isP2shSegwit, + required this.isSegwit, this.isSilentPayment, }); + factory BitcoinUtxo( + {required String txHash, + required BigInt value, + required int vout, + required BitcoinAddressType scriptType, + int? blockHeight, + CashToken? token}) { + final isP2shSegwit = + scriptType == P2shAddressType.p2wpkhInP2sh || scriptType == P2shAddressType.p2wshInP2sh; + return BitcoinUtxo._( + txHash: txHash, + value: value, + blockHeight: blockHeight, + token: token, + vout: vout, + scriptType: scriptType, + isP2tr: scriptType == SegwitAddressType.p2tr, + isP2shSegwit: isP2shSegwit, + isSegwit: isP2shSegwit || scriptType.isSegwit); + } /// check if utxos is p2tr - bool isP2tr() { - return scriptType == SegwitAddresType.p2tr; - } + final bool isP2tr; bool? isSilentPayment; /// check if utxos is segwit - bool isSegwit() { - return scriptType.isSegwit || isP2shSegwit(); - } + final bool isSegwit; - /// checl if utxos is p2sh neasted segwit - bool isP2shSegwit() { - return scriptType == P2shAddressType.p2wpkhInP2sh || scriptType == P2shAddressType.p2wshInP2sh; - } + /// check if utxos is p2sh neasted segwit + final bool isP2shSegwit; /// convert utxos to transaction input with specify sequence like ReplaceByeFee (4Bytes) TxInput toInput([List? sequence]) { @@ -236,7 +252,7 @@ class BitcoinUtxo { @override String toString() { - return "txid: $txHash vout: $vout script: ${scriptType.value} value: $value blockHeight: $blockHeight"; + return 'txid: $txHash vout: $vout script: ${scriptType.value} value: $value blockHeight: $blockHeight'; } } @@ -244,8 +260,8 @@ class BitcoinUtxo { extension Calculate on List { /// sum of utxos network values BigInt sumOfUtxosValue() { - BigInt sum = BigInt.zero; - for (var utxo in this) { + var sum = BigInt.zero; + for (final utxo in this) { sum += utxo.utxo.value; } return sum; @@ -253,8 +269,8 @@ extension Calculate on List { /// sum of utxos cash token (FToken) amounts Map sumOfTokenUtxos() { - final Map tokens = {}; - for (var utxo in this) { + final tokens = {}; + for (final utxo in this) { if (utxo.utxo.token == null) continue; final token = utxo.utxo.token!; if (!token.hasAmount) continue; diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/providers/explorer.dart similarity index 81% rename from lib/src/provider/api_provider/api_provider.dart rename to lib/src/provider/providers/explorer.dart index a31580b..6aabeb7 100644 --- a/lib/src/provider/api_provider/api_provider.dart +++ b/lib/src/provider/providers/explorer.dart @@ -1,19 +1,20 @@ import 'dart:convert'; import 'package:bitcoin_base/src/provider/models/models.dart'; -import 'package:bitcoin_base/src/provider/service/http/http_service.dart'; +import 'package:bitcoin_base/src/provider/services/explorer.dart'; import 'package:bitcoin_base/src/models/network.dart'; import 'package:blockchain_utils/utils/string/string.dart'; class ApiProvider { ApiProvider({required this.api, Map? header, required this.service}) - : _header = header ?? {"Content-Type": "application/json"}; + : _header = header ?? {'Content-Type': 'application/json'}; factory ApiProvider.fromMempool( - BasedUtxoNetwork network, { + BasedUtxoNetwork network, + ApiService service, { Map? header, String? baseUrl, }) { final api = APIConfig.mempool(network, baseUrl); - return ApiProvider(api: api, header: header, service: BitcoinApiService()); + return ApiProvider(api: api, header: header, service: service); } factory ApiProvider.fromBlocCypher(BasedUtxoNetwork network, ApiService service, {Map? header}) { @@ -30,20 +31,20 @@ class ApiProvider { return response; } - Future _postReqiest(String url, Object? data) async { + Future _postRequest(String url, Object? data) async { final response = await service.post(url, body: data, headers: _header); return response; } Future> testmempool(List params) async { - final Map data = { - "jsonrpc": "2.0", - "method": "testmempoolaccept", - "id": DateTime.now().millisecondsSinceEpoch.toString(), - "params": params + final data = { + 'jsonrpc': '2.0', + 'method': 'testmempoolaccept', + 'id': DateTime.now().millisecondsSinceEpoch.toString(), + 'params': params }; - final response = await _postReqiest>( - "https://btc.getblock.io/786c97b8-f53f-427b-80f7-9af7bd5bdb84/testnet/", json.encode(data)); + final response = await _postRequest>( + 'https://btc.getblock.io/786c97b8-f53f-427b-80f7-9af7bd5bdb84/testnet/', json.encode(data)); return response; } @@ -68,14 +69,14 @@ class ApiProvider { switch (api.apiType) { case APIType.mempool: - final response = await _postReqiest(url, txDigest); + final response = await _postRequest(url, txDigest); return response; default: - final Map digestData = {"tx": txDigest}; - final response = await _postReqiest>(url, json.encode(digestData)); + final digestData = {'tx': txDigest}; + final response = await _postRequest>(url, json.encode(digestData)); BlockCypherTransaction? tr; - if (response["tx"] != null) { - tr = BlockCypherTransaction.fromJson(response["tx"]); + if (response['tx'] != null) { + tr = BlockCypherTransaction.fromJson(response['tx']); } tr ??= BlockCypherTransaction.fromJson(response); @@ -119,8 +120,8 @@ class ApiProvider { return transactions; default: if (response is Map) { - if (response.containsKey("txs")) { - final transactions = (response["txs"] as List) + if (response.containsKey('txs')) { + final transactions = (response['txs'] as List) .map((e) => BlockCypherTransaction.fromJson(e) as T) .toList(); return transactions; @@ -159,7 +160,7 @@ class ApiProvider { return response; default: final toJson = StringUtils.toJson>(response); - return toJson["hash"]; + return toJson['hash']; } } diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart index 0d89497..4222bd8 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; import 'package:blockchain_utils/exception/exceptions.dart'; class ElectrumSSLService implements BitcoinBaseElectrumRPCService { diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart index bddecb1..c7ed19e 100644 --- a/lib/src/provider/service/electrum/electrum_tcp_service.dart +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/service.dart'; import 'package:blockchain_utils/exception/exceptions.dart'; class ElectrumTCPService implements BitcoinBaseElectrumRPCService { diff --git a/lib/src/provider/service/services.dart b/lib/src/provider/service/services.dart index 69edf79..46c73c1 100644 --- a/lib/src/provider/service/services.dart +++ b/lib/src/provider/service/services.dart @@ -1,2 +1 @@ -export 'http/http_service.dart'; export 'electrum/electrum.dart'; diff --git a/lib/src/provider/service/http/http_service.dart b/lib/src/provider/services/explorer.dart similarity index 96% rename from lib/src/provider/service/http/http_service.dart rename to lib/src/provider/services/explorer.dart index c0ddd78..7367169 100644 --- a/lib/src/provider/service/http/http_service.dart +++ b/lib/src/provider/services/explorer.dart @@ -18,7 +18,7 @@ abstract class ApiService { /// - [headers]: A map of headers to be included in the request. /// - [body]: The request body, typically in JSON format. Future post(String url, - {Map headers = const {"Content-Type": "application/json"}, Object? body}); + {Map headers = const {'Content-Type': 'application/json'}, Object? body}); } class ApiProviderException implements Exception { diff --git a/lib/src/provider/transaction_builder/builder.dart b/lib/src/transaction_builder/builder.dart similarity index 100% rename from lib/src/provider/transaction_builder/builder.dart rename to lib/src/transaction_builder/builder.dart diff --git a/lib/src/provider/transaction_builder/core.dart b/lib/src/transaction_builder/core.dart similarity index 100% rename from lib/src/provider/transaction_builder/core.dart rename to lib/src/transaction_builder/core.dart diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/transaction_builder/forked_transaction_builder.dart similarity index 69% rename from lib/src/provider/transaction_builder/forked_transaction_builder.dart rename to lib/src/transaction_builder/forked_transaction_builder.dart index 7436315..362ab29 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/transaction_builder/forked_transaction_builder.dart @@ -46,9 +46,12 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { void _validateBuilder() { if (network is! BitcoinCashNetwork && network is! BitcoinSVNetwork) { - throw const BitcoinBasePluginException( - "invalid network. use ForkedTransactionBuilder for BitcoinCashNetwork and BSVNetwork otherwise use BitcoinTransactionBuilder"); + throw const DartBitcoinPluginException( + 'invalid network. use ForkedTransactionBuilder for BitcoinCashNetwork and BSVNetwork otherwise use BitcoinTransactionBuilder'); } + + /// validate every address is related to network + /// exception if failed. for (final i in utxosInfo) { i.ownerDetails.address.toAddress(network); } @@ -99,9 +102,9 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { /// and doesn't generate errors when determining the transaction size. isFakeTransaction: true); - /// 71 bytes (64 byte signature, 6-7 byte Der encoding length) - const String fakeECDSASignatureBytes = - "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; + /// 72 bytes (64 byte signature, 6-7 byte Der encoding length + sighash) + const fakeECDSASignatureBytes = + '010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101'; final transaction = transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { @@ -171,8 +174,8 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { case P2shAddressType.p2pkhInP2sh32wt: return script; default: - throw BitcoinBasePluginException( - "unsuported multi-sig type ${utxo.utxo.scriptType} for ${network.conf.coinName.name}"); + throw DartBitcoinPluginException( + 'unsuported multi-sig type ${utxo.utxo.scriptType} for ${network.conf.coinName.name}'); } } @@ -183,17 +186,17 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { case P2shAddressType.p2pkInP2shwt: case P2shAddressType.p2pkInP2sh32: case P2shAddressType.p2pkInP2sh32wt: - return senderPub.toRedeemScript(); + return senderPub.toRedeemScript(mode: utxo.keyType); case P2pkhAddressType.p2pkh: case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2pkhInP2sh32: case P2pkhAddressType.p2pkhwt: case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32wt: - return senderPub.toP2pkhAddress().toScriptPubKey(); + return senderPub.toP2pkAddress(mode: utxo.keyType).toScriptPubKey(); default: - throw BitcoinBasePluginException( - "${utxo.utxo.scriptType} does not sudpport on ${network.conf.coinName.name}"); + throw DartBitcoinPluginException( + '${utxo.utxo.scriptType} does not sudpport on ${network.conf.coinName.name}'); } } @@ -211,19 +214,19 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - tapRootPubKeys: A List of of Script representing taproot public keys for P2TR inputs (ignored for non-P2TR inputs). // /// Returns: - /// - List: representing the transaction digest to be used for signing the input. + /// - `List`: representing the transaction digest to be used for signing the input. List _generateTransactionDigest( - Script scriptPubKeys, - int input, - UtxoWithAddress utox, - BtcTransaction transaction, - ) { + {required Script scriptPubKeys, + required int input, + required UtxoWithAddress utox, + required BtcTransaction transaction, + int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED}) { return transaction.getTransactionSegwitDigit( txInIndex: input, script: scriptPubKeys, amount: utox.utxo.value, token: utox.utxo.token, - sighash: BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); + sighash: sighash); } /// buildP2wshOrP2shScriptSig constructs and returns a script signature (represented as a List of strings) @@ -235,7 +238,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - utx: A UtxoWithAddress instance representing the unspent transaction output (UTXO) and its owner details. // /// Returns: - /// - List: A List of strings representing the script signature for the P2WSH or P2SH input. + /// - `List`: A List of strings representing the script signature for the P2WSH or P2SH input. List _buildMiltisigUnlockingScript(List signedDigest, UtxoWithAddress utx) { /// The constructed script signature consists of the signed digest elements followed by /// the script details of the multi-signature address. @@ -255,27 +258,27 @@ that demonstrate the right to spend the bitcoins associated with the correspondi return [signedDigest]; case P2pkhAddressType.p2pkh: case P2pkhAddressType.p2pkhwt: - return [signedDigest, senderPub.toHex()]; + return [signedDigest, senderPub.toHex(mode: utx.keyType)]; case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32: case P2shAddressType.p2pkhInP2sh32wt: - final script = senderPub.toP2pkhAddress().toScriptPubKey(); - return [signedDigest, senderPub.toHex(), script.toHex()]; + final script = senderPub.toAddress(mode: utx.keyType).toScriptPubKey(); + return [signedDigest, senderPub.toHex(mode: utx.keyType), script.toHex()]; case P2shAddressType.p2pkInP2sh: case P2shAddressType.p2pkInP2shwt: case P2shAddressType.p2pkInP2sh32: case P2shAddressType.p2pkInP2sh32wt: - final script = senderPub.toRedeemScript(); + final script = senderPub.toRedeemScript(mode: utx.keyType); return [signedDigest, script.toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( 'Cannot send from this type of address ${utx.utxo.scriptType}'); } } Tuple, List> _buildInputs() { - List sortedUtxos = List.from(utxosInfo); + var sortedUtxos = List.from(utxosInfo); if (inputOrdering == BitcoinOrdering.shuffle) { sortedUtxos = sortedUtxos..shuffle(); @@ -291,7 +294,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi }, ); } - List inputs = sortedUtxos.map((e) => e.utxo.toInput()).toList(); + final inputs = sortedUtxos.map((e) => e.utxo.toInput()).toList(); if (enableRBF && inputs.isNotEmpty) { inputs[0] = inputs[0].copyWith(sequence: BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); } @@ -300,7 +303,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } List _buildOutputs() { - List builtOutputs = outputs + var builtOutputs = outputs .where((element) => element is! BitcoinBurnableOutput) .map((e) => e.toOutput) .toList(); @@ -334,12 +337,12 @@ be retrieved by anyone who examines the blockchain's history. */ Script _opReturn(String message) { final toHex = BytesUtils.toHexString(StringUtils.toBytes(message)); - return Script(script: ["OP_RETURN", toHex]); + return Script(script: ['OP_RETURN', toHex]); } /// Total amount to spend excluding fees BigInt _sumOutputAmounts(List outputs) { - BigInt sum = BigInt.zero; + var sum = BigInt.zero; for (final e in outputs) { sum += e.amount; } @@ -348,8 +351,8 @@ be retrieved by anyone who examines the blockchain's history. /// Total token amount to spend. Map _sumTokenOutputAmounts(List outputs) { - final Map tokens = {}; - for (var utxo in outputs) { + final tokens = {}; + for (final utxo in outputs) { if (utxo.cashToken == null) continue; final token = utxo.cashToken!; if (!token.hasAmount) continue; @@ -368,47 +371,47 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumAmountsWithFee, required BigInt sumUtxoAmount, required BigInt sumOutputAmounts}) { - if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { - throw BitcoinBasePluginException('Sum value of utxo not spending', - details: {"inputAmount": sumUtxoAmount, "fee": fee, "outputAmount": sumOutputAmounts}); + if (isFakeTransaction) return; + if (sumAmountsWithFee != sumUtxoAmount) { + throw DartBitcoinPluginException('Sum value of utxo not spending', + details: {'inputAmount': sumUtxoAmount, 'fee': fee, 'outputAmount': sumOutputAmounts}); } - if (!isFakeTransaction) { - /// sum of token amounts - final sumOfTokenUtxos = utxos.sumOfTokenUtxos(); - - /// sum of token output amounts - final sumTokenOutputAmouts = _sumTokenOutputAmounts(outputs); - for (final i in sumOfTokenUtxos.entries) { - if (sumTokenOutputAmouts[i.key] != i.value) { - BigInt amount = sumTokenOutputAmouts[i.key] ?? BigInt.zero; - amount += outputs - .whereType() - .where((element) => element.categoryID == i.key) - .fold(BigInt.zero, - (previousValue, element) => previousValue + (element.value ?? BigInt.zero)); - - if (amount != i.value) { - throw BitcoinBasePluginException( - 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', - details: {"token": i.key, "inputValue": i.value, "outputValue": amount}); - } + + /// sum of token amounts + final sumOfTokenUtxos = utxos.sumOfTokenUtxos(); + + /// sum of token output amounts + final sumTokenOutputAmouts = _sumTokenOutputAmounts(outputs); + for (final i in sumOfTokenUtxos.entries) { + if (sumTokenOutputAmouts[i.key] != i.value) { + var amount = sumTokenOutputAmouts[i.key] ?? BigInt.zero; + amount += this + .outputs + .whereType() + .where((element) => element.categoryID == i.key) + .fold(BigInt.zero, + (previousValue, element) => previousValue + (element.value ?? BigInt.zero)); + + if (amount != i.value) { + throw DartBitcoinPluginException( + 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', + details: {'token': i.key, 'inputValue': i.value, 'outputValue': amount}); } } - for (final i in utxos) { - if (i.utxo.token != null) { - final token = i.utxo.token!; - if (token.hasAmount) continue; - if (!token.hasNFT) continue; - final hasOneoutput = outputs.whereType().any((element) => - element.utxoHash == i.utxo.txHash && element.token.category == token.category); - if (hasOneoutput) continue; - final hasBurnableOutput = outputs.whereType().any((element) => - element.utxoHash == i.utxo.txHash && element.categoryID == token.category); - if (hasBurnableOutput) continue; - throw BitcoinBasePluginException( - 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', - details: {"category id": token.category}); - } + } + for (final i in utxos) { + final token = i.utxo.token; + if (token != null && token.hasNFT) { + if (token.hasAmount) continue; + final hasOneoutput = this.outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.token.category == token.category); + if (hasOneoutput) continue; + final hasBurnableOutput = this.outputs.whereType().any( + (element) => element.utxoHash == i.utxo.txHash && element.categoryID == token.category); + if (hasBurnableOutput) continue; + throw DartBitcoinPluginException( + 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', + details: {'category id': token.category}); } } } @@ -416,23 +419,23 @@ be retrieved by anyone who examines the blockchain's history. @override Map getSignatureCount() { final sortedInputs = _buildInputs(); - final List inputs = sortedInputs.item1; - final List utxos = sortedInputs.item2; - final Map count = {}; + final inputs = sortedInputs.item1; + final utxos = sortedInputs.item2; + final count = {}; - for (int i = 0; i < inputs.length; i++) { + for (var i = 0; i < inputs.length; i++) { final indexUtxo = utxos[i]; if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; - int sumMultiSigWeight = 0; + var sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { - for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { + for (var ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (var weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } - mutlsiSigSignatures.add(""); + mutlsiSigSignatures.add(''); count[multiSigAddress.signers[ownerIndex].publicKey] = (count[multiSigAddress.signers[ownerIndex].publicKey] ?? 0) + 1; } @@ -441,8 +444,8 @@ be retrieved by anyone who examines the blockchain's history. break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException("some multisig signature does not exist"); + if (sumMultiSigWeight < multiSigAddress.threshold) { + throw const DartBitcoinPluginException('some multisig signature does not exist'); } continue; } @@ -457,9 +460,9 @@ be retrieved by anyone who examines the blockchain's history. /// build inputs final sortedInputs = _buildInputs(); - final List inputs = sortedInputs.item1; + final inputs = sortedInputs.item1; - final List utxos = sortedInputs.item2; + final utxos = sortedInputs.item2; /// build outout final outputs = _buildOutputs(); @@ -481,31 +484,38 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not + // BtcTransaction transaction = BtcTransaction.fromRaw( + // "0200000001b8249c5a6cf8f24815377222e4c2bab32154151c367111134ef7ce11c651e4c600000000fd5b0300483045022100b4e774511598efbdf256aaa127f2104c5f25e29311936a75034857901c72fa180220232410ec31b9809b0293a7a2c8c0ade53ca4bdde064ac1baaa27d545678891b84147304402203014d1beaa73fc289d0201bff61a40d1e853103d624fb8520cc6b4afea00d3c10220643eed02e2830b7f751b7355a889e4ab4c02d01df0ca71621d4f5affe5cb3a5d41483045022100bf73bd48dcef332924c7578ff7a5b9d3817ecdb2ad2a4148f56069705365427c022044ce67ca87c3e1309ba43fcb3c23a3618ba0dbd44a802c22efab78b42b9a8e3e41483045022100a3d90b8f959850f9e2655b38e61a8e5323a363803bd62ab0560509e9cd675dae022064ba98f689c21f66e0839660bf15c7a0c44e0e65384d82996a4f3b73a55daef141473044022033128a7572b0bc7d22c6cd2080a882b7e03b15167b1d88b6405fa0131ab6254602207d960421e8a1a2e85431b6398a9af742fb492c967c3acac9fd38d82d7c54756e41483045022100c00232655b098d529deea53dcfffa5b25dc4a2015a52b1b587ae73c813a8d4a502201d100762ddb4779d1c484790a1b38cf369f634ee33541d8ec0ec45c2470141ba4147304402201503e2e7bab40cd5a083c0e03506ad1c260d17b821a142be2591e05fa5bdc86d02201d0280a2fc78a2af4bf7f1245bd8bf3384ada22098ba84645958ff1715655630414730440220090757399dc9f989ff1a7d998af385cd9490fe07c2c216b54967d4138067312a02204c3bb010902d72e90a1133cc8ae34cb629e139863f62ab259f25e5cac2125779414d13015821030f0fb9a244ad31a369ee02b7abfbbb0bfa3812b9a39ed93346d03d67d412d17721022f1b310f4c065331bc0d79ba4661bb9822d67d7c4a1b0a1892e1fd0cd23aa68d210299c2aa85d2b21a62f396907a802a58e521dafd5bddaccbd72786eea189bc4dc921021a7a569e91dbf60581509c7fc946d1003b60c7dee85299538db6353538d595742103a92c9b7cac68758de5783ed8e5123598e4ad137091e42987d3bad8a08e35bf3d21034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa21036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f721031d16453b3ab3132acb0a5bc16cc49690d819a585267a15cd5a064e2a0ad4059958ae0000000001f0780f000000000017a914e5d65da9624e4754827edb627fe31d4c75954a388700000000"); + // transaction = transaction.copyWith(inputs: inputs); final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - - const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input - for (int i = 0; i < inputs.length; i++) { + for (var i = 0; i < inputs.length; i++) { final indexUtxo = utxos[i]; /// We receive the owner's ScriptPubKey final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest( + scriptPubKeys: script, + input: i, + utox: indexUtxo, + transaction: transaction, + sighash: sighash); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; - int sumMultiSigWeight = 0; + var sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (var ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest final sig = sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { + for (var weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -516,8 +526,8 @@ be retrieved by anyone who examines the blockchain's history. break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException("some multisig signature does not exist"); + if (sumMultiSigWeight < multiSigAddress.threshold) { + throw const DartBitcoinPluginException('some multisig signature does not exist'); } _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); @@ -525,10 +535,11 @@ be retrieved by anyone who examines the blockchain's history. } /// now we need sign the transaction digest - final sig = sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); + final sig = sign(digest, indexUtxo, indexUtxo.ownerDetails.publicKey!, sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } - + final ea = BtcTransaction.fromRaw(transaction.serialize()); + assert(ea.serialize() == transaction.serialize(), transaction.serialize()); return transaction; } @@ -537,9 +548,9 @@ be retrieved by anyone who examines the blockchain's history. /// build inputs final sortedInputs = _buildInputs(); - final List inputs = sortedInputs.item1; + final inputs = sortedInputs.item1; - final List utxos = sortedInputs.item2; + final utxos = sortedInputs.item2; /// build outout final outputs = _buildOutputs(); @@ -563,41 +574,48 @@ be retrieved by anyone who examines the blockchain's history. /// create new transaction with inputs and outputs and isSegwit transaction or not final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input - for (int i = 0; i < inputs.length; i++) { + for (var i = 0; i < inputs.length; i++) { final indexUtxo = utxos[i]; /// We receive the owner's ScriptPubKey final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest( + scriptPubKeys: script, + input: i, + utox: indexUtxo, + transaction: transaction, + sighash: sighash); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; - int sumMultiSigWeight = 0; + var sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (var ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest final sig = await sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); + if (sig.isEmpty) continue; - for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { + for (var weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } mutlsiSigSignatures.add(sig); } + sumMultiSigWeight += multiSigAddress.signers[ownerIndex].weight; if (sumMultiSigWeight >= multiSigAddress.threshold) { break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException("some multisig signature does not exist"); + if (sumMultiSigWeight < multiSigAddress.threshold) { + throw const DartBitcoinPluginException('some multisig signature does not exist'); } _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); @@ -605,23 +623,18 @@ be retrieved by anyone who examines the blockchain's history. } /// now we need sign the transaction digest - final sig = await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); + final sig = await sign(digest, indexUtxo, indexUtxo.ownerDetails.publicKey!, sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } - return transaction; } - void _addScripts({ - required UtxoWithAddress utxo, - required TxInput input, - required List signatures, - }) { + void _addScripts( + {required UtxoWithAddress utxo, required TxInput input, required List signatures}) { /// ok we signed, now we need unlocking script for this input final scriptSig = utxo.isMultiSig() ? _buildMiltisigUnlockingScript(signatures, utxo) : _buildUnlockingScript(signatures.first, utxo); - input.scriptSig = Script(script: scriptSig); } } @@ -643,15 +656,15 @@ List _fakeAddressesFromTypes(List types) return fakePub.toP2wpkhInP2sh(); case P2shAddressType.p2wshInP2sh: return fakePub.toP2wshInP2sh(); - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return fakePub.toP2wpkhAddress(); - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return fakePub.toP2wshAddress(); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: case SilentPaymentsAddresType.p2sp: return fakePub.toTaprootAddress(); default: - throw const BitcoinBasePluginException("invalid bitcoin address type"); + throw DartBitcoinPluginException("invalid bitcoin address type"); } }).toList(); } diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/transaction_builder/transaction_builder.dart similarity index 81% rename from lib/src/provider/transaction_builder/transaction_builder.dart rename to lib/src/transaction_builder/transaction_builder.dart index 8ec7400..0143f29 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/transaction_builder/transaction_builder.dart @@ -10,7 +10,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// such as UTXOs, memo, enableRBF (Replace-By-Fee), and more. /// /// Parameters: -/// - [outPuts]: List of Bitcoin outputs to be included in the transaction. +/// - [outputs]: List of Bitcoin outputs to be included in the transaction. /// - [fee]: Transaction fee (BigInt) for processing the transaction. /// - [network]: The target Bitcoin network. /// - [utxosInfo]: List of UtxoWithAddress objects providing information about Unspent Transaction Outputs (UTXOs). @@ -54,14 +54,14 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// validate network and address suport before create transaction void _validateBuilder() { if (network is BitcoinCashNetwork || network is BitcoinSVNetwork) { - throw const BitcoinBasePluginException( - "invalid network for BitcoinCashNetwork and BSVNetwork use ForkedTransactionBuilder"); + throw const DartBitcoinPluginException( + 'invalid network for BitcoinCashNetwork and BSVNetwork use ForkedTransactionBuilder'); } final token = utxosInfo.any((element) => element.utxo.token != null); final tokenInput = outputs.whereType(); final burn = outputs.whereType(); if (token || tokenInput.isNotEmpty || burn.isNotEmpty) { - throw const BitcoinBasePluginException("Cash Token only work on Bitcoin cash network"); + throw const DartBitcoinPluginException('Cash Token only work on Bitcoin cash network'); } for (final i in utxosInfo) { /// Verify each input for its association with this network's address. Raise an exception if the address is incorrect. @@ -124,17 +124,20 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { ); /// 64 byte schnorr signature length - const String fakeSchnorSignatureBytes = - "01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; + const fakeSchnorSignaturBytes = + '01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101'; - /// 71 bytes (64 byte signature, 6-7 byte Der encoding length) - const String fakeECDSASignatureBytes = - "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; + /// 72 bytes (64 byte signature, 6-7 byte Der encoding length) + const fakeECDSASignatureBytes = + '010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101'; final transaction = transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { - if (utxo.utxo.isP2tr()) { - return fakeSchnorSignatureBytes; + if (utxo.utxo.isP2tr) { + if (sighash != BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL) { + return '${fakeSchnorSignaturBytes}01'; + } + return fakeSchnorSignaturBytes; } else { return fakeECDSASignatureBytes; } @@ -201,7 +204,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - bool: True if at least one UTXO in the list is a SegWit UTXO, false otherwise. bool _hasSegwit() { for (final element in utxosInfo) { - if (element.utxo.isSegwit()) { + if (element.utxo.isSegwit) { return true; } } @@ -216,7 +219,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - bool: True if at least one UTXO in the list is a P2TR UTXO, false otherwise. bool _hasTaproot() { for (final element in utxosInfo) { - if (element.utxo.isP2tr()) { + if (element.utxo.isP2tr) { return true; } } @@ -234,7 +237,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return multiSigAAddr.toP2wshInP2shAddress(network: network).toScriptPubKey(); } return script; - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: if (isTaproot) { return multiSigAAddr.toP2wshAddress(network: network).toScriptPubKey(); } @@ -245,37 +248,38 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { } return script; default: - throw BitcoinBasePluginException("unsuported multi-sig type ${utxo.utxo.scriptType}"); + throw DartBitcoinPluginException('unsuported multi-sig type ${utxo.utxo.scriptType}'); } } final senderPub = utxo.public(); switch (utxo.utxo.scriptType) { case PubKeyAddressType.p2pk: - return senderPub.toRedeemScript(); - case SegwitAddresType.p2wsh: + return senderPub.toRedeemScript(mode: utxo.keyType); + case SegwitAddressType.p2wsh: if (isTaproot) { return senderPub.toP2wshAddress().toScriptPubKey(); } return senderPub.toP2wshRedeemScript(); case P2pkhAddressType.p2pkh: - return senderPub.toP2pkhAddress().toScriptPubKey(); - case SegwitAddresType.p2wpkh: + return senderPub.toP2pkhAddress(mode: utxo.keyType).toScriptPubKey(); + case SegwitAddressType.p2wpkh: if (isTaproot) { return senderPub.toP2wpkhAddress().toScriptPubKey(); } return senderPub.toP2pkhAddress().toScriptPubKey(); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return senderPub .toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true) .toScriptPubKey(); - case SegwitAddresType.mweb: + case SegwitAddressType.mweb: return Script(script: []); + return senderPub.toAddress().toScriptPubKey(); case P2shAddressType.p2pkhInP2sh: if (isTaproot) { - return senderPub.toP2pkhInP2sh().toScriptPubKey(); + return senderPub.toP2pkhInP2sh(mode: utxo.keyType).toScriptPubKey(); } - return senderPub.toP2pkhAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress(mode: utxo.keyType).toScriptPubKey(); case P2shAddressType.p2wpkhInP2sh: if (isTaproot) { return senderPub.toP2wpkhInP2sh().toScriptPubKey(); @@ -288,11 +292,11 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return senderPub.toP2wshRedeemScript(); case P2shAddressType.p2pkInP2sh: if (isTaproot) { - return senderPub.toP2pkInP2sh().toScriptPubKey(); + return senderPub.toP2pkInP2sh(mode: utxo.keyType).toScriptPubKey(); } - return senderPub.toRedeemScript(); + return senderPub.toRedeemScript(mode: utxo.keyType); } - throw const BitcoinBasePluginException("invalid bitcoin address type"); + throw const DartBitcoinPluginException('invalid bitcoin address type'); } /// generateTransactionDigest generates and returns a transaction digest for a given input in the context of a Bitcoin @@ -309,11 +313,17 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - tapRootPubKeys: A List of of Script representing taproot public keys for P2TR inputs (ignored for non-P2TR inputs). // /// Returns: - /// - List: representing the transaction digest to be used for signing the input. - List _generateTransactionDigest(Script scriptPubKeys, int input, UtxoWithAddress utox, - BtcTransaction transaction, List taprootAmounts, List