diff --git a/README.md b/README.md index a02d9b5..16ba6b9 100644 --- a/README.md +++ b/README.md @@ -144,10 +144,10 @@ We have integrated three APIs—Mempool, BlockCypher, and Electrum—into the pl final publicKey = ECPublic.fromHex('.....'); // Generate a Pay-to-Public-Key-Hash (P2PKH) address from the public key. - final p2pkh = publicKey.toAddress(); + final p2pkh = publicKey.toP2pkhAddress(); // Generate a Pay-to-Witness-Public-Key-Hash (P2WPKH) Segregated Witness (SegWit) address from the public key. - final p2wpkh = publicKey.toSegwitAddress(); + final p2wpkh = publicKey.toP2wpkhAddress(); // Generate a Pay-to-Witness-Script-Hash (P2WSH) Segregated Witness (SegWit) address from the public key. final p2wsh = publicKey.toP2wshAddress(); @@ -286,8 +286,8 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li final privateKey = ECPrivate.fromHex( "76257aafc9b954351c7f6445b2d07277f681a5e83d515a1f32ebf54989c2af4f"); final examplePublicKey = privateKey.getPublic(); - final spender1 = examplePublicKey.toAddress(); - final spender2 = examplePublicKey.toSegwitAddress(); + final spender1 = examplePublicKey.toP2pkhAddress(); + final spender2 = examplePublicKey.toP2wpkhAddress(); final spender3 = examplePublicKey.toTaprootAddress(); final spender4 = examplePublicKey.toP2pkhInP2sh(); final spender5 = examplePublicKey.toP2pkInP2sh(); @@ -336,9 +336,9 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// P2pkhAddress.fromAddress(address: ".....", network: network); /// P2trAddress.fromAddress(address: "....", network: network) /// .... - final List outPuts = [ + final List outputs = [ BitcoinOutput( - address: examplePublicKey2.toSegwitAddress(), + address: examplePublicKey2.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( address: examplePublicKey2.toTaprootAddress(), @@ -365,11 +365,11 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li int transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: accountsUtxos, outputs: [ - ...outPuts, + ...outputs, /// I add more output for change value to get correct transaction size BitcoinOutput( - address: examplePublicKey2.toAddress(), value: BigInt.zero) + address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -396,13 +396,13 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li } //// if we have change value we back amount to account if (changeValue > BigInt.zero) { - outPuts.add(BitcoinOutput( - address: examplePublicKey2.toAddress(), value: changeValue)); + outputs.add(BitcoinOutput( + address: examplePublicKey2.toP2pkhAddress(), value: changeValue)); } /// create transaction builder final builder = BitcoinTransactionBuilder( - outPuts: outPuts, + outputs: outputs, fee: fee, network: network, utxos: accountsUtxos, @@ -465,7 +465,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li /// p2pkh with token address () final receiver1 = P2pkhAddress.fromHash160( - addrHash: publicKey.toAddress().addressProgram, + h160: publicKey.toP2pkhAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. @@ -514,7 +514,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li previousValue + element.utxo.token!.amount); final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change address for bch values (sum of bch amout - (outputs amount + fee)) BitcoinOutput( address: p2pkhAddress.baseAddress, @@ -678,7 +678,7 @@ In the [example](https://github.com/mrtnetwork/bitcoin_base/tree/main/example/li // index of utxo txInIndex: i, // spender script pub key - script: utxo[i].public().toAddress().toScriptPubKey(), + script: utxo[i].public().toP2pkhAddress().toScriptPubKey(), ); // sign transaction diff --git a/example/lib/bitcoin_cash/burn_token_example.dart b/example/lib/bitcoin_cash/burn_token_example.dart index 9b25036..4acdb9b 100644 --- a/example/lib/bitcoin_cash/burn_token_example.dart +++ b/example/lib/bitcoin_cash/burn_token_example.dart @@ -29,10 +29,10 @@ void main() async { /// p2pkh with token address () final receiver1 = P2pkhAddress.fromHash160( - addrHash: publicKey.toAddress().addressProgram, + h160: publicKey.toP2pkhAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -80,7 +80,7 @@ void main() async { previousValue + element.utxo.token!.amount); final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change address for bch values (sum of bch amout - (outputs amount + fee)) BitcoinOutput( address: p2pkhAddress.baseAddress, diff --git a/example/lib/bitcoin_cash/create_cash_token_example.dart b/example/lib/bitcoin_cash/create_cash_token_example.dart index bb93937..c0b8754 100644 --- a/example/lib/bitcoin_cash/create_cash_token_example.dart +++ b/example/lib/bitcoin_cash/create_cash_token_example.dart @@ -25,9 +25,9 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. - final p2pkhAddress = publicKey.toAddress(); + final p2pkhAddress = publicKey.toP2pkhAddress(); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.pubKeyHash(), @@ -66,7 +66,7 @@ void main() async { // print("vout $vout0Hash"); // return; final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ BitcoinTokenOutput( address: p2pkhAddress, diff --git a/example/lib/bitcoin_cash/create_nft_example.dart b/example/lib/bitcoin_cash/create_nft_example.dart index 28404cd..4db99f2 100644 --- a/example/lib/bitcoin_cash/create_nft_example.dart +++ b/example/lib/bitcoin_cash/create_nft_example.dart @@ -26,9 +26,9 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. final p2pkhAddress = - BitcoinCashAddress.fromBaseAddress(publicKey.toAddress()); + BitcoinCashAddress.fromBaseAddress(publicKey.toP2pkhAddress()); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -65,7 +65,7 @@ void main() async { return; } final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ BitcoinOutput( address: p2pkhAddress.baseAddress, value: sumOfUtxo - diff --git a/example/lib/bitcoin_cash/make_vout0_example.dart b/example/lib/bitcoin_cash/make_vout0_example.dart index aa71f8b..6415852 100644 --- a/example/lib/bitcoin_cash/make_vout0_example.dart +++ b/example/lib/bitcoin_cash/make_vout0_example.dart @@ -26,9 +26,9 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. - final p2pkhAddress = publicKey.toAddress(); + final p2pkhAddress = publicKey.toP2pkhAddress(); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.pubKeyHash(), @@ -47,7 +47,7 @@ void main() async { final sumOfUtxo = utxos.sumOfUtxosValue(); final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ BitcoinOutput( address: p2pkhAddress, value: sumOfUtxo - BtcUtils.toSatoshi("0.00003"), diff --git a/example/lib/bitcoin_cash/minting_nft_example.dart b/example/lib/bitcoin_cash/minting_nft_example.dart index 092fb24..6c2e1fa 100644 --- a/example/lib/bitcoin_cash/minting_nft_example.dart +++ b/example/lib/bitcoin_cash/minting_nft_example.dart @@ -28,7 +28,7 @@ void main() async { final p2pkhAddress = BitcoinCashAddress.fromBaseAddress( publicKey.toP2pkInP2sh(useBCHP2sh32: true)); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -54,7 +54,7 @@ void main() async { "3f0d87791e5996aaddbce16c12651dd8b5b881cf7338340504bb7b2c6c08bfc4"; final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ BitcoinOutput( address: p2pkhAddress.baseAddress, value: sumOfUtxo - diff --git a/example/lib/bitcoin_cash/old_examples/old_example.dart b/example/lib/bitcoin_cash/old_examples/old_example.dart index 15403f5..7f534ce 100644 --- a/example/lib/bitcoin_cash/old_examples/old_example.dart +++ b/example/lib/bitcoin_cash/old_examples/old_example.dart @@ -101,7 +101,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { ]); final b = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2shAddress and a value of 0.01 BCH BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.01")), @@ -121,7 +121,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// Specify the network for the litcoin transaction network: network, - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -138,13 +138,13 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { vout: 0, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey2.toAddress().type, + scriptType: examplePublicKey2.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toAddress())), + address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the ForkedTransactionBuilder @@ -247,7 +247,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { final b = ForkedTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2pkhAddress and a value of 0.01 BCH BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.01")), @@ -267,7 +267,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -324,13 +324,13 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { vout: 2, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey.toAddress().type, + scriptType: examplePublicKey.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toAddress())), + address: examplePublicKey.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction diff --git a/example/lib/bitcoin_cash/p2sh32_spend_example.dart b/example/lib/bitcoin_cash/p2sh32_spend_example.dart index d46b23d..c7af331 100644 --- a/example/lib/bitcoin_cash/p2sh32_spend_example.dart +++ b/example/lib/bitcoin_cash/p2sh32_spend_example.dart @@ -27,7 +27,7 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. final p2pkhAddress = - BitcoinCashAddress.fromBaseAddress(publicKey.toAddress()); + BitcoinCashAddress.fromBaseAddress(publicKey.toP2pkhAddress()); /// Initialize two P2SH32 addresses for receiving funds. /// bchtest:pvw39llgap0a4vm8jn9sjsvfsthah4wgemjlh6epdtzr3pl2fqtmsn3s4vcm7 @@ -42,7 +42,7 @@ void main() async { final p2sh32Example2 = BitcoinCashAddress.fromBaseAddress( publicKey.toP2pkInP2sh(useBCHP2sh32: true)); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( @@ -79,7 +79,7 @@ void main() async { return; } final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ BitcoinOutput( address: p2pkhAddress.baseAddress, value: BtcUtils.toSatoshi("0.00001"), diff --git a/example/lib/bitcoin_cash/send_ft_token_example.dart b/example/lib/bitcoin_cash/send_ft_token_example.dart index d875661..2e9b82d 100644 --- a/example/lib/bitcoin_cash/send_ft_token_example.dart +++ b/example/lib/bitcoin_cash/send_ft_token_example.dart @@ -30,10 +30,10 @@ void main() async { /// p2pkh with token address () final receiver1 = P2pkhAddress.fromHash160( - addrHash: publicKey.toAddress().addressProgram, + h160: publicKey.toP2pkhAddress().addressProgram, type: P2pkhAddressType.p2pkhwt); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -80,7 +80,7 @@ void main() async { previousValue + element.utxo.token!.amount); final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change address for bch values (sum of bch amout - (outputs amount + fee)) BitcoinOutput( address: p2pkhAddress.baseAddress, diff --git a/example/lib/bitcoin_cash/transfer_bch_example.dart b/example/lib/bitcoin_cash/transfer_bch_example.dart index f9631f9..12fdbd2 100644 --- a/example/lib/bitcoin_cash/transfer_bch_example.dart +++ b/example/lib/bitcoin_cash/transfer_bch_example.dart @@ -27,7 +27,7 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. final p2pkhAddress = - BitcoinCashAddress.fromBaseAddress(publicKey.toAddress()); + BitcoinCashAddress.fromBaseAddress(publicKey.toP2pkhAddress()); /// Initialize two P2SH32 addresses for receiving funds. final p2sh32Example1 = BitcoinCashAddress( @@ -37,7 +37,7 @@ void main() async { "bchtest:pvw39llgap0a4vm8jn9sjsvfsthah4wgemjlh6epdtzr3pl2fqtmsn3s4vcm7", network: network); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -59,7 +59,7 @@ void main() async { } final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change input (sumofutxos - spend) BitcoinOutput( address: p2pkhAddress.baseAddress, diff --git a/example/lib/global/bch_example.dart b/example/lib/global/bch_example.dart index 6b31303..3f5bdd4 100644 --- a/example/lib/global/bch_example.dart +++ b/example/lib/global/bch_example.dart @@ -24,7 +24,7 @@ void main() async { /// Derives a P2PKH address from the given public key and converts it to a Bitcoin Cash address /// for enhanced accessibility within the network. final p2pkhAddress = - BitcoinCashAddress.fromBaseAddress(publicKey.toAddress()); + BitcoinCashAddress.fromBaseAddress(publicKey.toP2pkhAddress()); /// Initialize two P2SH32 addresses for receiving funds. final p2sh32Example1 = BitcoinCashAddress( @@ -34,7 +34,7 @@ void main() async { "bchtest:pvw39llgap0a4vm8jn9sjsvfsthah4wgemjlh6epdtzr3pl2fqtmsn3s4vcm7", network: network); - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account. + /// 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( scriptHash: p2pkhAddress.baseAddress.pubKeyHash(), @@ -56,7 +56,7 @@ void main() async { } final bchTransaction = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// change input (sumofutxos - spend) BitcoinOutput( address: p2pkhAddress.baseAddress, diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index 0ef7b04..3ce647d 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -68,7 +68,7 @@ void _spendFromP2pkhTo10DifferentType() async { network: network); final out3 = P2wpkhAddress.fromAddress( address: "tb1q3zqgu9j368wgk8u5f9vtmkdwq8geetdxry690d", network: network); - final out4 = P2pkAddress(publicKey: examplePublicKey.publicKey.toHex()); + final out4 = P2pkAddress.fromPubkey(pubkey: examplePublicKey.publicKey.toHex()); final out5 = P2shAddress.fromAddress( address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); final out6 = P2shAddress.fromAddress( @@ -97,7 +97,7 @@ void _spendFromP2pkhTo10DifferentType() async { final builder = BitcoinTransactionBuilder( /// outputs and values - outPuts: [ + outputs: [ BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.001")), BitcoinOutput(address: out2, value: BtcUtils.toSatoshi("0.001")), BitcoinOutput(address: out3, value: BtcUtils.toSatoshi("0.001")), @@ -122,7 +122,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// memo memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ /// Create a UTXO using a BitcoinUtxo with specific details UtxoWithAddress( @@ -140,24 +140,24 @@ void _spendFromP2pkhTo10DifferentType() async { vout: 3, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey2.toAddress().type, + scriptType: examplePublicKey2.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toAddress())), + address: examplePublicKey2.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( txHash: "6ff0bdb2966f62f5e202c924e1cab1368b0258833e48986cc0a70fbca624ba93", value: BigInt.from(812830), vout: 0, - scriptType: examplePublicKey2.toAddress().type, + scriptType: examplePublicKey2.toP2pkhAddress().type, ), ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toAddress())), + address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder @@ -255,7 +255,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [BitcoinOutput(address: out1, value: change)], + outputs: [BitcoinOutput(address: out1, value: change)], /// Set the transaction fee fee: BtcUtils.toSatoshi("0.00005"), @@ -265,7 +265,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction. + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction. /// We are selecting 10 UTXOs for spending, and each UTXO has a different address type. /// These UTXOs are related to the previous example at the top of this page. utxos: [ @@ -282,13 +282,13 @@ void _spendFrom10DifferentTypeToP2pkh() async { vout: 0, /// Script type indicates the type of script associated with the UTXO's address - scriptType: childKey1PublicKey.toAddress().type, + scriptType: childKey1PublicKey.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: childKey1PublicKey.toHex(), - address: childKey1PublicKey.toAddress())), + address: childKey1PublicKey.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( txHash: @@ -306,11 +306,11 @@ void _spendFrom10DifferentTypeToP2pkh() async { "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 2, - scriptType: childKey1PublicKey.toSegwitAddress().type, + scriptType: childKey1PublicKey.toP2wpkhAddress().type, ), ownerDetails: UtxoAddressDetails( publicKey: childKey1PublicKey.toHex(), - address: childKey1PublicKey.toSegwitAddress())), + address: childKey1PublicKey.toP2wpkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( txHash: diff --git a/example/lib/global/old_examples/dash_example/dash.dart b/example/lib/global/old_examples/dash_example/dash.dart index 0479f3d..c4aa4d6 100644 --- a/example/lib/global/old_examples/dash_example/dash.dart +++ b/example/lib/global/old_examples/dash_example/dash.dart @@ -78,7 +78,7 @@ void _spendFromTwoP2shAndOneP2PKH() async { final b = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2shAddress and a value of 1.0 DASH BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("1")), @@ -112,13 +112,13 @@ void _spendFromTwoP2shAndOneP2PKH() async { vout: 2, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey2.toAddress().type, + scriptType: examplePublicKey2.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toAddress())), + address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder @@ -184,7 +184,7 @@ void _spendP2SH() async { final b = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ BitcoinOutput(address: out1, value: change), ], diff --git a/example/lib/global/old_examples/doge_example/doge_example.dart b/example/lib/global/old_examples/doge_example/doge_example.dart index 1ff568c..943fab4 100644 --- a/example/lib/global/old_examples/doge_example/doge_example.dart +++ b/example/lib/global/old_examples/doge_example/doge_example.dart @@ -85,7 +85,7 @@ void _spendFrom3P2shAddress() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2shAddress and a value of 1 DOGE BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("1")), @@ -119,13 +119,13 @@ void _spendFrom3P2shAddress() async { vout: 1, /// Script type indicates the type of script associated with the UTXO's address - scriptType: childKey1PublicKey.toAddress().type, + scriptType: childKey1PublicKey.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: childKey1PublicKey.toHex(), - address: childKey1PublicKey.toAddress())), + address: childKey1PublicKey.toP2pkhAddress())), ]); final tr = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { if (publicKey == childKey1PublicKey.toHex()) { @@ -213,7 +213,7 @@ void _spendFromP2pkhAndP2sh() async { final b = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2pkhAddress and a value of 1.0 DOGE BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("1")), diff --git a/example/lib/global/old_examples/litecoin_example/litecoin_example.dart b/example/lib/global/old_examples/litecoin_example/litecoin_example.dart index ccf5f5e..8e66f3a 100644 --- a/example/lib/global/old_examples/litecoin_example/litecoin_example.dart +++ b/example/lib/global/old_examples/litecoin_example/litecoin_example.dart @@ -78,7 +78,7 @@ void _spendLTCP2pkhAddress() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the first P2shAddress and a value of 0.0001 LTC BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.0001")), @@ -115,12 +115,12 @@ void _spendLTCP2pkhAddress() async { vout: 3, /// Script type indicates the type of script associated with the UTXO's address - scriptType: pub.toAddress().type, + scriptType: pub.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( - publicKey: pub.toHex(), address: pub.toAddress())), + publicKey: pub.toHex(), address: pub.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder @@ -200,7 +200,7 @@ void _spendFrom2P2shAddressAndOneMultiSigP2shAddress() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the third P2shAddress and a value equal to the 'change' variable BitcoinOutput(address: out1, value: change), ], @@ -338,7 +338,7 @@ void _spendFromNestedSegwitP2WPKHInP2SH() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the third P2wpkhAddress and a value equal to the 'change' variable BitcoinOutput(address: out1, value: change), ], @@ -456,7 +456,7 @@ void _spendFromSegwitP2WPKHAddress() async { final builder = BitcoinTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the third P2pkhAddress and a value equal to the 'change' variable BitcoinOutput(address: input1, value: change), ], @@ -487,13 +487,13 @@ void _spendFromSegwitP2WPKHAddress() async { vout: 0, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey.toSegwitAddress().type, + scriptType: examplePublicKey.toP2wpkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toSegwitAddress())), + address: examplePublicKey.toP2wpkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder instance (builder) diff --git a/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart b/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart index 060550f..3a17df5 100644 --- a/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart +++ b/example/lib/global/old_examples/spending_with_scripts/spending_builders.dart @@ -42,7 +42,7 @@ BtcTransaction buildP2wpkTransaction({ // index of input txInIndex: i, // script pub key of spender address - script: utxo[i].public().toAddress().toScriptPubKey(), + script: utxo[i].public().toP2pkhAddress().toScriptPubKey(), // amount of utxo amount: utxo[i].utxo.value); // sign transaction @@ -107,12 +107,12 @@ BtcTransaction buildP2WSHTransaction({ // index of utxo txInIndex: i, // P2WSH scripts - script: utxo[i].public().toP2wshScript(), + script: utxo[i].public().toP2wshRedeemScript(), // amount of utxo amount: utxo[i].utxo.value); // sign transaction - final signedTx = sign(txDigit, utxo[i].public().toP2wshScript().toHex(), + final signedTx = sign(txDigit, utxo[i].public().toP2wshRedeemScript().toHex(), BitcoinOpCodeConst.SIGHASH_ALL); // create unlock script @@ -164,7 +164,7 @@ BtcTransaction buildP2pkhTransaction({ // index of utxo txInIndex: i, // spender script pub key - script: utxo[i].public().toAddress().toScriptPubKey(), + script: utxo[i].public().toP2pkhAddress().toScriptPubKey(), ); // sign transaction @@ -205,10 +205,9 @@ BtcTransaction buildP2shNoneSegwitTransaction({ final tx = BtcTransaction(inputs: txin, outputs: txOut, hasSegwit: false); for (int i = 0; i < txin.length; i++) { final ownerPublic = utxo[i].public(); - final scriptPubKey = - utxo[i].ownerDetails.address.type == P2shAddressType.p2pkhInP2sh - ? ownerPublic.toAddress().toScriptPubKey() - : ownerPublic.toRedeemScript(); + final scriptPubKey = utxo[i].ownerDetails.address.type == P2shAddressType.p2pkhInP2sh + ? ownerPublic.toP2pkhAddress().toScriptPubKey() + : ownerPublic.toRedeemScript(); // For None-SegWit transactions, we use the 'getTransactionDigest' method // to obtain the input digest for signing. final txDigit = tx.getTransactionDigest( @@ -275,8 +274,8 @@ BtcTransaction buildP2SHSegwitTransaction({ final ownerPublic = utxo[i].public(); final scriptPubKey = utxo[i].ownerDetails.address.type == P2shAddressType.p2wpkhInP2sh - ? ownerPublic.toAddress().toScriptPubKey() - : ownerPublic.toP2wshScript(); + ? ownerPublic.toP2pkhAddress().toScriptPubKey() + : ownerPublic.toP2wshRedeemScript(); // For SegWit transactions (excluding P2TR), we use the 'getTransactionSegwitDigit' method // to obtain the input digest for signing. @@ -307,7 +306,7 @@ BtcTransaction buildP2SHSegwitTransaction({ switch (utxo[i].ownerDetails.address.type) { case P2shAddressType.p2wpkhInP2sh: witnesses.add(TxWitnessInput(stack: [signedTx, ownerPublic.toHex()])); - final script = ownerPublic.toSegwitAddress().toScriptPubKey(); + final script = ownerPublic.toP2wpkhAddress().toScriptPubKey(); txin[i].scriptSig = Script(script: [script.toHex()]); break; case P2shAddressType.p2wshInP2sh: 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 1c7acee..4f16538 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 @@ -26,7 +26,7 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // In this section, you can add any number of addresses with type P2PWPH to this transaction. final publicKey = sWallet.getPublic(); // P2WPKH - final sender = publicKey.toSegwitAddress(); + final sender = publicKey.toP2wpkhAddress(); // Read UTXOs of accounts from the BlockCypher API. final utxo = await api.getAccountUtxo( UtxoAddressDetails(address: sender, publicKey: publicKey.toHex())); @@ -44,7 +44,7 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { final prive = sWallet; final recPub = rWallet.getPublic(); // P2WPKH - final receiver = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); // P2TR final changeAddress = recPub.toTaprootAddress(); @@ -124,9 +124,9 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { final prive = sWallet; final recPub = rWallet.getPublic(); - final receiver = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); - final changeAddress = recPub.toSegwitAddress(); + final changeAddress = recPub.toP2wpkhAddress(); final List outputsAdress = [ BitcoinOutput(address: receiver, value: BigInt.zero), BitcoinOutput(address: changeAddress, value: BigInt.zero) @@ -160,7 +160,7 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { // and we use method `buildP2pkhTransaction` to create the transaction. final addr = sWallet.getPublic(); // P2PKH - final sender = addr.toAddress(); + final sender = addr.toP2pkhAddress(); final utxo = await api.getAccountUtxo( UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); @@ -173,8 +173,8 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { final prive = sWallet; final recPub = rWallet.getPublic(); - final receiver = recPub.toSegwitAddress(); - final changeAddress = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); + final changeAddress = recPub.toP2wpkhAddress(); final List outputsAdress = [ BitcoinOutput(address: receiver, value: BigInt.zero), BitcoinOutput(address: changeAddress, value: BigInt.zero) @@ -225,8 +225,8 @@ Future spendingP2SHNoneSegwit( final prive = sWallet; final recPub = rWallet.getPublic(); - final receiver = recPub.toSegwitAddress(); - final changeAddress = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); + final changeAddress = recPub.toP2wpkhAddress(); final List outputsAdress = [ BitcoinOutput(address: receiver, value: BigInt.zero), BitcoinOutput(address: changeAddress, value: BigInt.zero) @@ -275,9 +275,9 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { final prive = sWallet; final recPub = rWallet.getPublic(); - final receiver = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); - final changeAddress = recPub.toSegwitAddress(); + final changeAddress = recPub.toP2wpkhAddress(); final List outputsAdress = [ BitcoinOutput(address: receiver, value: BigInt.zero), BitcoinOutput(address: changeAddress, value: BigInt.zero) @@ -327,8 +327,8 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { final prive = sWallet; final recPub = rWallet.getPublic(); - final receiver = recPub.toSegwitAddress(); - final changeAddress = recPub.toSegwitAddress(); + final receiver = recPub.toP2wpkhAddress(); + final changeAddress = recPub.toP2wpkhAddress(); final List outputsAdress = [ BitcoinOutput(address: receiver, value: BigInt.zero), BitcoinOutput(address: changeAddress, value: BigInt.zero) 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 6b4542e..794deb6 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 @@ -60,7 +60,7 @@ void main() async { // tb1qxt3c7849m0m6cv3z3s35c3zvdna3my3yz0r609qd9g0dcyyk580sgyldhe final p2wshMultiSigAddress = - multiSignatureAddress.toP2wshAddress(network: network).toAddress(network); + multiSignatureAddress.toP2wshAddress(network: network).toP2pkhAddress(network); // p2sh(p2wsh) multisig final signerP2sh1 = @@ -80,12 +80,12 @@ void main() async { // 2N8co8bth9CNKtnWGfHW6HuUNgnNPNdpsMj final p2shMultisigAddress = p2shMultiSignature .toP2wshInP2shAddress(network: network) - .toAddress(network); + .toP2pkhAddress(network); // P2TR final exampleAddr2 = public2.toTaprootAddress(); // P2KH - final exampleAddr4 = public3.toAddress(); + final exampleAddr4 = public3.toP2pkhAddress(); // Spending List // i use some different address type for this // now i want to spending from 8 address in one transaction @@ -203,7 +203,7 @@ void main() async { // Now, we provide the UTXOs we want to spend. utxos: utxos, // We select transaction outputs - outPuts: [output1, output2, output3, output4], + outputs: [output1, output2, output3, output4], /* Transaction fee Ensure that you have accurately calculated the amounts. 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 5da3143..851259f 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 @@ -37,7 +37,7 @@ void main() async { final public4 = private4.getPublic(); // P2PKH ADDRESS - final exampleAddr1 = public1.toAddress(); + final exampleAddr1 = public1.toP2pkhAddress(); // P2TR final exampleAddr2 = public2.toTaprootAddress(); @@ -45,7 +45,7 @@ void main() async { // P2PKHINP2SH final exampleAddr3 = public2.toP2pkhInP2sh(); // P2KH - final exampleAddr4 = public3.toAddress(); + final exampleAddr4 = public3.toP2pkhAddress(); // P2PKHINP2SH final exampleAddr5 = public3.toP2pkhInP2sh(); // P2WSHINP2SH 1-1 multisig @@ -55,7 +55,7 @@ void main() async { // P2PKINP2SH final exampleAddr8 = public4.toP2pkInP2sh(); // P2WPKH - final exampleAddr9 = public3.toSegwitAddress(); + final exampleAddr9 = public3.toP2wpkhAddress(); // P2WSH 1-1 multisig final exampleAddr10 = public3.toP2wshAddress(); @@ -143,7 +143,7 @@ void main() async { // Now, we provide the UTXOs we want to spend. utxos: utxos, // We select transaction outputs - outPuts: [ + outputs: [ output1, output2, output3, 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 a441543..37c6e1d 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 @@ -25,8 +25,8 @@ void main() async { final privateKey = ECPrivate.fromHex( "76257aafc9b954351c7f6445b2d07277f681a5e83d515a1f32ebf54989c2af4f"); final examplePublicKey = privateKey.getPublic(); - final spender1 = examplePublicKey.toAddress(); - final spender2 = examplePublicKey.toSegwitAddress(); + final spender1 = examplePublicKey.toP2pkhAddress(); + final spender2 = examplePublicKey.toP2wpkhAddress(); final spender3 = examplePublicKey.toTaprootAddress(); final spender4 = examplePublicKey.toP2pkhInP2sh(); final spender5 = examplePublicKey.toP2pkInP2sh(); @@ -47,7 +47,7 @@ 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 + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account final elctrumUtxos = await provider .request(ElectrumScriptHashListUnspent(scriptHash: i.pubKeyHash())); @@ -75,9 +75,9 @@ void main() async { /// P2pkhAddress.fromAddress(address: ".....", network: network); /// P2trAddress.fromAddress(address: "....", network: network) /// .... - final List outPuts = [ + final List outputs = [ BitcoinOutput( - address: examplePublicKey2.toSegwitAddress(), + address: examplePublicKey2.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( address: examplePublicKey2.toTaprootAddress(), @@ -97,18 +97,18 @@ void main() async { const String memo = "https://github.com/mrtnetwork"; /// SUM OF OUTOUT AMOUNTS - final sumOfOutputs = outPuts.fold( + final sumOfOutputs = outputs.fold( BigInt.zero, (previousValue, element) => previousValue + element.value); /// Estimate transaction size int transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: accountsUtxos, outputs: [ - ...outPuts, + ...outputs, /// I add more output for change value to get correct transaction size BitcoinOutput( - address: examplePublicKey2.toAddress(), value: BigInt.zero) + address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -135,13 +135,13 @@ void main() async { } //// if we have change value we back amount to account if (changeValue > BigInt.zero) { - outPuts.add(BitcoinOutput( - address: examplePublicKey2.toAddress(), value: changeValue)); + outputs.add(BitcoinOutput( + address: examplePublicKey2.toP2pkhAddress(), value: changeValue)); } /// create transaction builder final builder = BitcoinTransactionBuilder( - outPuts: outPuts, + outputs: outputs, fee: fee, network: network, utxos: accountsUtxos, diff --git a/example/lib/global/transfer_to_8_account_example.dart b/example/lib/global/transfer_to_8_account_example.dart index 90f21f7..a8f6f6b 100644 --- a/example/lib/global/transfer_to_8_account_example.dart +++ b/example/lib/global/transfer_to_8_account_example.dart @@ -17,7 +17,7 @@ void main() async { 'cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo', netVersion: BitcoinNetwork.testnet.wifNetVer); final examplePublicKey2 = examplePrivateKey2.getPublic(); - final p2pkhAddress = examplePublicKey2.toAddress(); + final p2pkhAddress = examplePublicKey2.toP2pkhAddress(); /// receiver addresses i use public key for generate address final examplePublicKey = ECPublic.fromHex( @@ -25,9 +25,9 @@ void main() async { const network = BitcoinNetwork.testnet; - /// Reads all UTXOs (Unspent Transaction Outputs) associated with the account + /// Reads all UTXOs (Unspent Transaction outputs) associated with the account final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( - scriptHash: examplePublicKey2.toAddress().pubKeyHash())); + scriptHash: examplePublicKey2.toP2pkhAddress().pubKeyHash())); /// Converts all UTXOs to a list of UtxoWithAddress, containing UTXO information along with address details. /// read spender utxos @@ -48,12 +48,12 @@ void main() async { /// P2pkhAddress.fromAddress(address: ".....", network: network); /// P2trAddress.fromAddress(address: "....", network: network) /// .... - final List outPuts = [ + final List outputs = [ BitcoinOutput( - address: examplePublicKey.toAddress(), + address: examplePublicKey.toP2pkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( - address: examplePublicKey.toSegwitAddress(), + address: examplePublicKey.toP2wpkhAddress(), value: BtcUtils.toSatoshi("0.00001")), BitcoinOutput( address: examplePublicKey.toTaprootAddress(), @@ -76,18 +76,18 @@ void main() async { const String memo = "https://github.com/mrtnetwork"; /// SUM OF OUTOUT AMOUNTS - final sumOfOutputs = outPuts.fold( + final sumOfOutputs = outputs.fold( BigInt.zero, (previousValue, element) => previousValue + element.value); /// ESTIMATE TRANSACTION SIZE int estimateSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxos, outputs: [ - ...outPuts, + ...outputs, /// I add more output for change value to get correct transaction size BitcoinOutput( - address: examplePublicKey2.toAddress(), value: BigInt.zero) + address: examplePublicKey2.toP2pkhAddress(), value: BigInt.zero) ], /// network @@ -113,13 +113,13 @@ void main() async { //// if we have change value we back amount to account if (changeValue > BigInt.zero) { final changeOutput = BitcoinOutput( - address: examplePublicKey2.toAddress(), value: changeValue); - outPuts.add(changeOutput); + address: examplePublicKey2.toP2pkhAddress(), value: changeValue); + outputs.add(changeOutput); } /// create transaction builder final builder = BitcoinTransactionBuilder( - outPuts: outPuts, + outputs: outputs, fee: fee, network: network, utxos: utxos, diff --git a/example/lib/main.dart b/example/lib/main.dart index 5e85d9f..ed100b9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -101,7 +101,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { ]); final b = ForkedTransactionBuilder( - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2shAddress and a value of 0.01 BCH BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.01")), @@ -121,7 +121,7 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { /// Specify the network for the litcoin transaction network: network, - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -138,13 +138,13 @@ void _spendFrom2P2SHAnd2P2PKHAddress() async { vout: 0, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey2.toAddress().type, + scriptType: examplePublicKey2.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toAddress())), + address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the ForkedTransactionBuilder @@ -247,7 +247,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { final b = ForkedTransactionBuilder( /// outputs - outPuts: [ + outputs: [ /// Define a BitcoinOutput with the P2pkhAddress and a value of 0.01 BCH BitcoinOutput(address: out1, value: BtcUtils.toSatoshi("0.01")), @@ -267,7 +267,7 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { /// Add a memo to the transaction, linking to the GitHub repository memo: "https://github.com/mrtnetwork", - /// Define a list of Unspent Transaction Outputs (UTXOs) for the Bitcoin transaction + /// Define a list of Unspent Transaction outputs (UTXOs) for the Bitcoin transaction utxos: [ UtxoWithAddress( @@ -324,13 +324,13 @@ void _spendFrom2P2SHAnd1P2PKHAddress() async { vout: 2, /// Script type indicates the type of script associated with the UTXO's address - scriptType: examplePublicKey.toAddress().type, + scriptType: examplePublicKey.toP2pkhAddress().type, ), /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toAddress())), + address: examplePublicKey.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart deleted file mode 100644 index 362c306..0000000 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ /dev/null @@ -1,95 +0,0 @@ -/// Simple example how to send request to electurm with secure socket - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumSSLService with BitcoinBaseElectrumRPCService { - ElectrumSSLService._( - this.url, - SecureSocket channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); - } - SecureSocket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final parts = url.split(":"); - final channel = await SecureSocket.connect( - parts[0], - int.parse(parts[1]), - onBadCertificate: (certificate) => true, - ).timeout(connectionTimeOut); - - return ElectrumSSLService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(List event) { - final Map decode = json.decode(utf8.decode(event)); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toTCPParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/electrum_tcp_service.dart b/example/lib/services_examples/electrum/electrum_tcp_service.dart deleted file mode 100644 index 750aad0..0000000 --- a/example/lib/services_examples/electrum/electrum_tcp_service.dart +++ /dev/null @@ -1,92 +0,0 @@ -/// Simple example how to send request to electurm with tcp - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumTCPService with BitcoinBaseElectrumRPCService { - ElectrumTCPService._( - this.url, - Socket channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); - } - Socket? _socket; - StreamSubscription>? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.add(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final parts = url.split(":"); - final channel = await Socket.connect(parts[0], int.parse(parts[1])) - .timeout(connectionTimeOut); - - return ElectrumTCPService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(List event) { - final Map decode = json.decode(utf8.decode(event)); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/electrum_websocket_service.dart b/example/lib/services_examples/electrum/electrum_websocket_service.dart deleted file mode 100644 index 6ca10a2..0000000 --- a/example/lib/services_examples/electrum/electrum_websocket_service.dart +++ /dev/null @@ -1,91 +0,0 @@ -/// Simple example how to send request to electurm with websocket -import 'dart:async'; -import 'dart:convert'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/cross_platform_websocket/core.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; - -class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { - ElectrumWebSocketService._( - this.url, - WebSocketCore channel, { - this.defaultRequestTimeOut = const Duration(seconds: 30), - }) : _socket = channel { - _subscription = channel.stream - .cast() - .listen(_onMessge, onError: _onClose, onDone: _onDone); - } - WebSocketCore? _socket; - StreamSubscription? _subscription; - final Duration defaultRequestTimeOut; - - Map requests = {}; - bool _isDiscounnect = false; - - bool get isConnected => _isDiscounnect; - - @override - final String url; - - void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); - } - _socket?.sink(params); - } - - void _onClose(Object? error) { - _isDiscounnect = true; - - _socket = null; - _subscription?.cancel().catchError((e) {}); - _subscription = null; - } - - void _onDone() { - _onClose(null); - } - - void discounnect() { - _onClose(null); - } - - static Future connect( - String url, { - Iterable? protocols, - Duration defaultRequestTimeOut = const Duration(seconds: 30), - final Duration connectionTimeOut = const Duration(seconds: 30), - }) async { - final channel = - await WebSocketCore.connect(url, protocols: protocols?.toList()); - - return ElectrumWebSocketService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); - } - - void _onMessge(String event) { - final Map decode = json.decode(event); - if (decode.containsKey("id")) { - final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); - } - } - - @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); - - try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); - return result; - } finally { - requests.remove(params.id); - } - } -} diff --git a/example/lib/services_examples/electrum/request_completer.dart b/example/lib/services_examples/electrum/request_completer.dart deleted file mode 100644 index e6f00dc..0000000 --- a/example/lib/services_examples/electrum/request_completer.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:async'; - -class AsyncRequestCompleter { - AsyncRequestCompleter(this.params); - final Completer> completer = Completer(); - final Map params; -} diff --git a/example/pubspec.lock b/example/pubspec.lock index d7800dd..77038f1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bitcoin_base: dependency: "direct main" description: @@ -19,10 +27,11 @@ packages: blockchain_utils: dependency: "direct main" description: - name: blockchain_utils - sha256: aebc3a32b927b34f638817c4bfdb85f86a97e6ad35f0cd962660b0c6e8d5c56b - url: "https://pub.dev" - source: hosted + path: "." + ref: cake-update-v2 + resolved-ref: "2767a54ed2b0a23494e4e96a3fe5b5022b834b70" + url: "https://github.com/cake-tech/blockchain_utils" + source: git version: "3.3.0" boolean_selector: dependency: transitive @@ -32,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -52,10 +69,26 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -90,6 +123,14 @@ packages: description: flutter source: sdk version: "0.0.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" http: dependency: "direct main" description: @@ -106,22 +147,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -150,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: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" path: dependency: transitive description: @@ -170,11 +227,27 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -187,10 +260,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -203,10 +276,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -219,10 +292,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" typed_data: dependency: transitive description: @@ -243,10 +316,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" web: dependency: transitive description: @@ -256,5 +329,5 @@ packages: source: hosted version: "0.5.1" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ca4fb33..25d16d4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.2.3 <4.0.0' + sdk: '>=3.0.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -37,9 +37,10 @@ dependencies: cupertino_icons: ^1.0.2 bitcoin_base: path: ../ - # blockchain_utils: - # path: ../../blockchain_utils - blockchain_utils: ^3.3.0 + 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/bitcoin_base.dart b/lib/bitcoin_base.dart index ef45ef4..204e31a 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -8,6 +8,8 @@ library bitcoin_base; export 'package:bitcoin_base/src/bitcoin/address/address.dart'; +export 'package:bitcoin_base/src/bitcoin/address/util.dart'; + export 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; export 'package:bitcoin_base/src/crypto/crypto.dart'; @@ -16,8 +18,24 @@ export 'package:bitcoin_base/src/models/network.dart'; export 'package:bitcoin_base/src/provider/api_provider.dart'; -export 'package:bitcoin_base/src/utils/btc_utils.dart'; +export 'package:bitcoin_base/src/utils/utils.dart'; export 'package:bitcoin_base/src/cash_token/cash_token.dart'; export 'package:bitcoin_base/src/bitcoin_cash/bitcoin_cash.dart'; + +export 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +export 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; + +export 'src/exception/exception.dart'; + +export 'package:bitcoin_base/src/provider/transaction_builder/builder.dart'; + +export 'src/provider/models/electrum/electrum_utxo.dart'; + +export 'src/provider/service/electrum/electrum.dart'; + +export 'src/provider/service/electrum/electrum_version.dart'; + +export 'src/utils/btc_utils.dart'; diff --git a/lib/src/bitcoin/address/address.dart b/lib/src/bitcoin/address/address.dart index 8f1d988..4dd7d7e 100644 --- a/lib/src/bitcoin/address/address.dart +++ b/lib/src/bitcoin/address/address.dart @@ -12,6 +12,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/blockchain_utils.dart'; +import 'package:bitcoin_base/src/utils/script.dart'; part 'core.dart'; part 'legacy_address.dart'; part 'utils/address_utils.dart'; diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index 1211396..de0045d 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -9,8 +9,25 @@ 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 BitcoinBasePluginException('Invalid BitcoinAddressType: $value')); + } + + static BitcoinAddressType fromAddress(BitcoinBaseAddress address) { + if (address is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (address is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (address is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (address is P2trAddress) { + return SegwitAddresType.p2tr; + } else if (address is SilentPaymentsAddresType) { + return SilentPaymentsAddresType.p2sp; + } else if (address is P2wpkhAddress) { + return SegwitAddresType.p2wpkh; + } + + throw BitcoinBasePluginException('Invalid BitcoinAddressType: $address'); } /// Check if the address type is Pay-to-Script-Hash (P2SH). @@ -24,6 +41,7 @@ abstract class BitcoinAddressType implements Enumerate { SegwitAddresType.p2wpkh, SegwitAddresType.p2tr, SegwitAddresType.p2wsh, + SegwitAddresType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -34,20 +52,52 @@ abstract class BitcoinAddressType implements Enumerate { P2shAddressType.p2pkInP2sh32wt, P2shAddressType.p2pkhInP2shwt, P2shAddressType.p2pkInP2shwt, - P2pkhAddressType.p2pkhwt + P2pkhAddressType.p2pkhwt, + SilentPaymentsAddresType.p2sp ]; @override - String toString() { - return "BitcoinAddressType.$value"; - } + String toString() => value; } abstract class BitcoinBaseAddress { + BitcoinBaseAddress({this.network}); + 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, [ + BasedUtxoNetwork network = BitcoinNetwork.mainnet, + ]) { + if (network is BitcoinCashNetwork) { + if (!address.startsWith("bitcoincash:") && + (address.startsWith("q") || address.startsWith("p"))) { + address = "bitcoincash:$address"; + } + + return BitcoinCashAddress(address).baseAddress; + } + + if (P2pkhAddress.regex.hasMatch(address)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (P2shAddress.regex.hasMatch(address)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (P2wshAddress.regex.hasMatch(address)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (P2trAddress.regex.hasMatch(address)) { + return P2trAddress.fromAddress(address: address, network: network); + } else if (SilentPaymentAddress.regex.hasMatch(address)) { + return SilentPaymentAddress.fromAddress(address); + } else if (P2wpkhAddress.regex.hasMatch(address)) { + return P2wpkhAddress.fromAddress(address: address, network: network); + } + + throw BitcoinBasePluginException('Invalid BitcoinBaseAddress: $address'); + } } class PubKeyAddressType implements BitcoinAddressType { @@ -62,9 +112,7 @@ class PubKeyAddressType implements BitcoinAddressType { @override int get hashLength => 20; @override - String toString() { - return "PubKeyAddressType.$value"; - } + String toString() => value; } class P2pkhAddressType implements BitcoinAddressType { @@ -83,21 +131,19 @@ class P2pkhAddressType implements BitcoinAddressType { @override int get hashLength => 20; @override - String toString() { - return "P2pkhAddressType.$value"; - } + String toString() => value; } class P2shAddressType implements BitcoinAddressType { const P2shAddressType._(this.value, this.hashLength, this.withToken); - static const P2shAddressType p2wshInP2sh = P2shAddressType._( - "P2SH/P2WSH", _BitcoinAddressUtils.hash160DigestLength, false); - static const P2shAddressType p2wpkhInP2sh = P2shAddressType._( - "P2SH/P2WPKH", _BitcoinAddressUtils.hash160DigestLength, false); - static const P2shAddressType p2pkhInP2sh = P2shAddressType._( - "P2SH/P2PKH", _BitcoinAddressUtils.hash160DigestLength, false); - static const P2shAddressType p2pkInP2sh = P2shAddressType._( - "P2SH/P2PK", _BitcoinAddressUtils.hash160DigestLength, false); + static const P2shAddressType p2wshInP2sh = + P2shAddressType._("P2SH/P2WSH", _BitcoinAddressUtils.hash160DigestLength, false); + static const P2shAddressType p2wpkhInP2sh = + P2shAddressType._("P2SH/P2WPKH", _BitcoinAddressUtils.hash160DigestLength, false); + static const P2shAddressType p2pkhInP2sh = + P2shAddressType._("P2SH/P2PKH", _BitcoinAddressUtils.hash160DigestLength, false); + static const P2shAddressType p2pkInP2sh = + P2shAddressType._("P2SH/P2PK", _BitcoinAddressUtils.hash160DigestLength, false); @override bool get isP2sh => true; @override @@ -109,35 +155,33 @@ class P2shAddressType implements BitcoinAddressType { /// specify BCH NETWORK for now! /// Pay-to-Script-Hash-32 - static const P2shAddressType p2pkhInP2sh32 = P2shAddressType._( - "P2SH32/P2PKH", _BitcoinAddressUtils.scriptHashLenght, false); + static const P2shAddressType p2pkhInP2sh32 = + P2shAddressType._("P2SH32/P2PKH", _BitcoinAddressUtils.scriptHashLenght, false); //// Pay-to-Script-Hash-32 - static const P2shAddressType p2pkInP2sh32 = P2shAddressType._( - "P2SH32/P2PK", _BitcoinAddressUtils.scriptHashLenght, false); + static const P2shAddressType p2pkInP2sh32 = + P2shAddressType._("P2SH32/P2PK", _BitcoinAddressUtils.scriptHashLenght, false); /// Pay-to-Script-Hash-32-with-token - static const P2shAddressType p2pkhInP2sh32wt = P2shAddressType._( - "P2SH32WT/P2PKH", _BitcoinAddressUtils.scriptHashLenght, true); + static const P2shAddressType p2pkhInP2sh32wt = + P2shAddressType._("P2SH32WT/P2PKH", _BitcoinAddressUtils.scriptHashLenght, true); /// Pay-to-Script-Hash-32-with-token - static const P2shAddressType p2pkInP2sh32wt = P2shAddressType._( - "P2SH32WT/P2PK", _BitcoinAddressUtils.scriptHashLenght, true); + static const P2shAddressType p2pkInP2sh32wt = + P2shAddressType._("P2SH32WT/P2PK", _BitcoinAddressUtils.scriptHashLenght, true); /// Pay-to-Script-Hash-with-token - static const P2shAddressType p2pkhInP2shwt = P2shAddressType._( - "P2SHWT/P2PKH", _BitcoinAddressUtils.hash160DigestLength, true); + static const P2shAddressType p2pkhInP2shwt = + P2shAddressType._("P2SHWT/P2PKH", _BitcoinAddressUtils.hash160DigestLength, true); /// Pay-to-Script-Hash-with-token - static const P2shAddressType p2pkInP2shwt = P2shAddressType._( - "P2SHWT/P2PK", _BitcoinAddressUtils.hash160DigestLength, true); + static const P2shAddressType p2pkInP2shwt = + P2shAddressType._("P2SHWT/P2PK", _BitcoinAddressUtils.hash160DigestLength, true); @override final String value; @override - String toString() { - return "P2shAddressType.$value"; - } + String toString() => value; } class SegwitAddresType implements BitcoinAddressType { @@ -145,6 +189,7 @@ class SegwitAddresType implements BitcoinAddressType { static const SegwitAddresType p2wpkh = SegwitAddresType._("P2WPKH"); static const SegwitAddresType p2tr = SegwitAddresType._("P2TR"); static const SegwitAddresType p2wsh = SegwitAddresType._("P2WSH"); + static const SegwitAddresType mweb = SegwitAddresType._("MWEB"); @override bool get isP2sh => false; @override @@ -158,13 +203,33 @@ class SegwitAddresType implements BitcoinAddressType { switch (this) { case SegwitAddresType.p2wpkh: return 20; + case SegwitAddresType.mweb: + return 66; default: return 32; } } @override - String toString() { - return "SegwitAddresType.$value"; + String toString() => value; +} + +class SilentPaymentsAddresType implements BitcoinAddressType { + const SilentPaymentsAddresType._(this.value); + 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; } + + @override + String toString() => value; } diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index c3952cf..c2b9798 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -1,30 +1,77 @@ part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; -abstract class LegacyAddress implements BitcoinBaseAddress { +abstract class LegacyAddress extends BitcoinBaseAddress { /// Represents a Bitcoin address /// /// [addressProgram] the addressProgram string representation of the address; hash160 represents - /// two consequtive hashes of the public key or the redeam script or SHA256 for BCH(P2SH), first + /// two consequtive hashes of the public key or the redeem script or SHA256 for BCH(P2SH), first /// a SHA-256 and then an RIPEMD-160 - LegacyAddress.fromHash160(String addrHash, BitcoinAddressType addressType) - : _addressProgram = - _BitcoinAddressUtils.validateAddressProgram(addrHash, addressType); - LegacyAddress.fromAddress( - {required String address, required BasedUtxoNetwork network}) { - final decode = _BitcoinAddressUtils.decodeLagacyAddressWithNetworkAndType( - address: address, type: type, network: network); + 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) { + final decode = _BitcoinAddressUtils.decodeLegacyAddressWithNetworkAndType( + address: address, + type: type, + network: network, + ); + if (decode == null) { - throw BitcoinBasePluginException( - "Invalid ${network.conf.coinName} address"); + throw BitcoinBasePluginException("Invalid ${network.conf.coinName} address"); } + _addressProgram = decode; } - LegacyAddress.fromScript({required Script script}) + + LegacyAddress.fromPubkey({required ECPublic pubkey, super.network}) + : _pubkey = pubkey, + _addressProgram = _BitcoinAddressUtils.pubkeyToHash160(pubkey.toHex()); + + LegacyAddress.fromRedeemScript({required Script script, super.network}) : _addressProgram = _BitcoinAddressUtils.scriptToHash160(script); - LegacyAddress._(); + LegacyAddress.fromScriptSig({required Script script, super.network}) { + switch (type) { + case PubKeyAddressType.p2pk: + _signature = script.findScriptParam(0); + break; + case P2pkhAddressType.p2pkh: + if (script.script.length != 2) throw ArgumentError('Input is invalid'); + + _signature = script.findScriptParam(0); + + if (!isCanonicalScriptSignature(BytesUtils.fromHexString(_signature!))) { + throw ArgumentError('Input has invalid signature'); + } + + _pubkey = ECPublic.fromHex(script.findScriptParam(1)); + _addressProgram = _BitcoinAddressUtils.pubkeyToHash160(_pubkey!.toHex()); + break; + case P2shAddressType.p2wpkhInP2sh: + case P2shAddressType.p2wshInP2sh: + case P2shAddressType.p2pkhInP2sh: + case P2shAddressType.p2pkInP2sh: + _signature = script.findScriptParam(1); + _addressProgram = _BitcoinAddressUtils.scriptToHash160( + Script.fromRaw(hexData: script.findScriptParam(2))); + break; + default: + throw UnimplementedError(); + } + } + + ECPublic? _pubkey; + String? _signature; late final String _addressProgram; + ECPublic? get pubkey => _pubkey; + String? get signature => _signature; + @override String get addressProgram { if (type == PubKeyAddressType.p2pk) throw UnimplementedError(); @@ -32,9 +79,22 @@ abstract class LegacyAddress implements BitcoinBaseAddress { } @override - String toAddress(BasedUtxoNetwork network) { + String toAddress([BasedUtxoNetwork? network]) { + network ??= this.network; + + if (network == null) { + throw const BitcoinBasePluginException("Network is required"); + } + + if (!network.supportedAddress.contains(type)) { + throw BitcoinBasePluginException("network does not support ${type.value} address"); + } + return _BitcoinAddressUtils.legacyToAddress( - network: network, addressProgram: addressProgram, type: type); + network: network, + addressProgram: addressProgram, + type: type, + ); } @override @@ -44,91 +104,147 @@ abstract class LegacyAddress implements BitcoinBaseAddress { } class P2shAddress extends LegacyAddress { - P2shAddress.fromScript( - {required Script script, this.type = P2shAddressType.p2pkInP2sh}) - : super.fromScript(script: script); - - P2shAddress.fromAddress( - {required String address, - required BasedUtxoNetwork network, - this.type = P2shAddressType.p2pkInP2sh}) - : super.fromAddress(address: address, network: network); - P2shAddress.fromHash160( - {required String addrHash, this.type = P2shAddressType.p2pkInP2sh}) - : super.fromHash160(addrHash, type); + static RegExp get 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(); + + P2shAddress.fromAddress({ + required super.address, + required super.network, + this.type = P2shAddressType.p2pkInP2sh, + }) : super.fromAddress(); + + P2shAddress.fromHash160({ + required super.h160, + super.network, + this.type = P2shAddressType.p2pkInP2sh, + }) : super.fromHash160(type: type); + + factory P2shAddress.fromScriptPubkey({ + required Script script, + BasedUtxoNetwork? network, + type = P2shAddressType.p2pkInP2sh, + }) { + if (script.getAddressType() is! P2shAddressType) { + throw ArgumentError("Invalid scriptPubKey"); + } - @override - final P2shAddressType type; + return P2shAddress.fromHash160(h160: script.findScriptParam(1), network: network, type: type); + } @override - String toAddress(BasedUtxoNetwork network) { - if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( - "network does not support ${type.value} address"); - } - return super.toAddress(network); - } + final P2shAddressType type; /// Returns the scriptPubKey (P2SH) that corresponds to this address @override Script toScriptPubKey() { if (addressProgram.length == 64) { - return Script(script: ['OP_HASH256', addressProgram, 'OP_EQUAL']); + return Script( + script: [BitcoinOpCodeConst.OP_HASH256, addressProgram, BitcoinOpCodeConst.OP_EQUAL], + ); + } else { + return Script( + script: [BitcoinOpCodeConst.OP_HASH160, addressProgram, BitcoinOpCodeConst.OP_EQUAL], + ); } - return Script(script: ['OP_HASH160', addressProgram, 'OP_EQUAL']); } } class P2pkhAddress extends LegacyAddress { - P2pkhAddress.fromScript( - {required Script script, this.type = P2pkhAddressType.p2pkh}) - : super.fromScript(script: script); - P2pkhAddress.fromAddress( - {required String address, - required BasedUtxoNetwork network, - this.type = P2pkhAddressType.p2pkh}) - : super.fromAddress(address: address, network: network); - P2pkhAddress.fromHash160( - {required String addrHash, this.type = P2pkhAddressType.p2pkh}) - : super.fromHash160(addrHash, type); + static RegExp get 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); + } + + P2pkhAddress.fromAddress({ + required super.address, + required super.network, + this.type = P2pkhAddressType.p2pkh, + }) : super.fromAddress(); + + P2pkhAddress.fromHash160({ + required super.h160, + super.network, + this.type = P2pkhAddressType.p2pkh, + }) : super.fromHash160(type: type); + + P2pkhAddress.fromScriptSig({ + required super.script, + super.network, + this.type = P2pkhAddressType.p2pkh, + }) : super.fromScriptSig(); @override Script toScriptPubKey() { return Script(script: [ - 'OP_DUP', - 'OP_HASH160', - addressProgram, - 'OP_EQUALVERIFY', - 'OP_CHECKSIG' + BitcoinOpCodeConst.OP_DUP, + BitcoinOpCodeConst.OP_HASH160, + _addressProgram, + BitcoinOpCodeConst.OP_EQUALVERIFY, + BitcoinOpCodeConst.OP_CHECKSIG ]); } @override final P2pkhAddressType type; + + Script toScriptSig() { + return Script(script: [_signature, _pubkey]); + } } class P2pkAddress extends LegacyAddress { - P2pkAddress({required String publicKey}) : super._() { - final toBytes = BytesUtils.fromHexString(publicKey); - if (!Secp256k1PublicKeyEcdsa.isValidBytes(toBytes)) { - throw const BitcoinBasePluginException("Invalid secp256k1 public key"); + static RegExp get regex => RegExp(r'1([A-Za-z0-9]{34})'); + + P2pkAddress({required ECPublic publicKey, super.network}) + : _pubkeyHex = publicKey.toHex(), + super.fromPubkey(pubkey: publicKey); + + factory P2pkAddress.fromPubkey({required ECPublic pubkey}) => pubkey.toP2pkAddress(); + + P2pkAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(); + + factory P2pkAddress.fromScriptPubkey({required Script script}) { + if (script.getAddressType() is! PubKeyAddressType) { + throw ArgumentError("Invalid scriptPubKey"); } - publicHex = publicKey; + + return P2pkAddress.fromPubkey(pubkey: ECPublic.fromHex(script.script[0])); } - late final String publicHex; - /// Returns the scriptPubKey (P2SH) that corresponds to this address + late final String _pubkeyHex; + @override Script toScriptPubKey() { - return Script(script: [publicHex, 'OP_CHECKSIG']); + return Script(script: [_pubkeyHex, BitcoinOpCodeConst.OP_CHECKSIG]); } @override - String toAddress(BasedUtxoNetwork network) { + String toAddress([BasedUtxoNetwork? network]) { + network ??= this.network; + + if (network == null) { + throw const BitcoinBasePluginException("Network is required"); + } + return _BitcoinAddressUtils.legacyToAddress( - network: network, - addressProgram: _BitcoinAddressUtils.pubkeyToHash160(publicHex), - type: type); + network: network, + addressProgram: _BitcoinAddressUtils.pubkeyToHash160(_pubkeyHex), + type: type, + ); } @override diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index e1c4307..8b8fd7f 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(network); + return network == null ? address : baseAddress.toAddress(); } /// The type of the Bitcoin address. @@ -22,15 +22,13 @@ abstract class BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for Bitcoin network. class BitcoinAddress extends BitcoinNetworkAddress { const BitcoinAddress._(this.baseAddress, this.address); - factory BitcoinAddress(String address, - {BitcoinNetwork network = BitcoinNetwork.mainnet}) { - return BitcoinAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory BitcoinAddress(String address, {BitcoinNetwork network = BitcoinNetwork.mainnet}) { + return BitcoinAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } factory BitcoinAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinAddress._(baseAddress, baseAddress.toAddress()); } @override final BitcoinBaseAddress baseAddress; @@ -41,15 +39,13 @@ class BitcoinAddress extends BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for Doge network. class DogeAddress extends BitcoinNetworkAddress { const DogeAddress._(this.baseAddress, this.address); - factory DogeAddress(String address, - {DogecoinNetwork network = DogecoinNetwork.mainnet}) { - return DogeAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory DogeAddress(String address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { + return DogeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } 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()); } @override final BitcoinBaseAddress baseAddress; @@ -61,15 +57,13 @@ class DogeAddress extends BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for Pepecoin network. class PepeAddress extends BitcoinNetworkAddress { const PepeAddress._(this.baseAddress, this.address); - factory PepeAddress(String address, - {PepeNetwork network = PepeNetwork.mainnet}) { - return PepeAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory PepeAddress(String address, {PepeNetwork network = PepeNetwork.mainnet}) { + return PepeAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } 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()); } @override final BitcoinBaseAddress baseAddress; @@ -81,15 +75,13 @@ class PepeAddress extends BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for Litecoin network. class LitecoinAddress extends BitcoinNetworkAddress { LitecoinAddress._(this.baseAddress, this.address); - factory LitecoinAddress(String address, - {LitecoinNetwork network = LitecoinNetwork.mainnet}) { - return LitecoinAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory LitecoinAddress(String address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { + return LitecoinAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } 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()); } @override final BitcoinBaseAddress baseAddress; @@ -104,8 +96,10 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet, bool validateNetworkPrefix = false}) { final decodeAddress = _BitcoinAddressUtils.decodeBchAddress( - address, network, - validateNetworkHRP: validateNetworkPrefix); + address, + network, + validateNetworkHRP: validateNetworkPrefix, + ); if (decodeAddress == null) { throw BitcoinBasePluginException("Invalid ${network.value} address."); } @@ -114,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(network)); + return BitcoinCashAddress._(baseAddress, baseAddress.toAddress()); } @override final BitcoinBaseAddress baseAddress; @@ -133,15 +127,13 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for Dash network. class DashAddress extends BitcoinNetworkAddress { const DashAddress._(this.baseAddress, this.address); - factory DashAddress(String address, - {DashNetwork network = DashNetwork.mainnet}) { - return DashAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory DashAddress(String address, {DashNetwork network = DashNetwork.mainnet}) { + return DashAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } 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()); } @override final BitcoinBaseAddress baseAddress; @@ -152,15 +144,13 @@ class DashAddress extends BitcoinNetworkAddress { /// A concrete implementation of [BitcoinNetworkAddress] for bitcoinSV network. class BitcoinSVAddress extends BitcoinNetworkAddress { const BitcoinSVAddress._(this.baseAddress, this.address); - factory BitcoinSVAddress(String address, - {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { - return BitcoinSVAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + factory BitcoinSVAddress(String address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { + return BitcoinSVAddress._(_BitcoinAddressUtils.decodeAddress(address, network), address); } 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()); } @override final BitcoinBaseAddress baseAddress; diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index a68574f..740e1ea 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -1,42 +1,55 @@ part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; -abstract class SegwitAddress implements BitcoinBaseAddress { - SegwitAddress.fromAddress( - {required String address, - required BasedUtxoNetwork network, - required this.segwitVersion}) { - if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( - "network does not support ${type.value} address"); - } +abstract class SegwitAddress extends BitcoinBaseAddress { + SegwitAddress.fromAddress({ + required String address, + required BasedUtxoNetwork network, + required this.segwitVersion, + }) : super(network: network) { addressProgram = _BitcoinAddressUtils.toSegwitProgramWithVersionAndNetwork( - address: address, version: segwitVersion, network: network); + address: address, + version: segwitVersion, + network: network, + ); } - SegwitAddress.fromProgram( - {required String program, - required this.segwitVersion, - required SegwitAddresType addresType}) - : addressProgram = - _BitcoinAddressUtils.validateAddressProgram(program, addresType); - SegwitAddress.fromScript( - {required Script script, required this.segwitVersion}) - : addressProgram = _BitcoinAddressUtils.segwitScriptToSHA256(script); + + SegwitAddress.fromProgram({ + required String program, + required SegwitAddresType addressType, + super.network, + required this.segwitVersion, + this.pubkey, + }) : addressProgram = _BitcoinAddressUtils.validateAddressProgram(program, addressType), + super(); + + SegwitAddress.fromRedeemScript({ + required Script script, + super.network, + required this.segwitVersion, + }) : addressProgram = _BitcoinAddressUtils.segwitScriptToSHA256(script); @override late final String addressProgram; - final int segwitVersion; + ECPublic? pubkey; @override - String toAddress(BasedUtxoNetwork network) { + String toAddress([BasedUtxoNetwork? network]) { + network ??= this.network; + + if (network == null) { + throw const BitcoinBasePluginException("Network is required"); + } + if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( - "network does not support ${type.value} address"); + throw BitcoinBasePluginException("network does not support ${type.value} address"); } + return _BitcoinAddressUtils.segwitToAddress( - addressProgram: addressProgram, - network: network, - segwitVersion: segwitVersion); + addressProgram: addressProgram, + network: network, + segwitVersion: segwitVersion, + ); } @override @@ -46,26 +59,32 @@ abstract class SegwitAddress implements BitcoinBaseAddress { } class P2wpkhAddress extends SegwitAddress { - P2wpkhAddress.fromAddress( - {required String address, required BasedUtxoNetwork network}) - : super.fromAddress( - segwitVersion: _BitcoinAddressUtils.segwitV0, - address: address, - network: network); - - P2wpkhAddress.fromProgram({required String program}) + static RegExp get 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}) : super.fromProgram( - segwitVersion: _BitcoinAddressUtils.segwitV0, - program: program, - addresType: SegwitAddresType.p2wpkh); - P2wpkhAddress.fromScript({required Script script}) - : super.fromScript( - segwitVersion: _BitcoinAddressUtils.segwitV0, script: script); + segwitVersion: _BitcoinAddressUtils.segwitV0, + addressType: SegwitAddresType.p2wpkh, + ); + + P2wpkhAddress.fromRedeemScript({required super.script, super.network}) + : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + + factory P2wpkhAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddresType.p2wpkh) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2wpkhAddress.fromProgram(program: script.findScriptParam(1), network: network); + } /// returns the scriptPubKey of a P2WPKH witness script @override Script toScriptPubKey() { - return Script(script: ['OP_0', addressProgram]); + return Script(script: [BitcoinOpCodeConst.OP_0, addressProgram]); } /// returns the type of address @@ -74,25 +93,33 @@ class P2wpkhAddress extends SegwitAddress { } class P2trAddress extends SegwitAddress { - P2trAddress.fromAddress( - {required String address, required BasedUtxoNetwork network}) - : super.fromAddress( - segwitVersion: _BitcoinAddressUtils.segwitV1, - address: address, - network: network); - P2trAddress.fromProgram({required String program}) + static RegExp get 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}) : super.fromProgram( - segwitVersion: _BitcoinAddressUtils.segwitV1, - program: program, - addresType: SegwitAddresType.p2tr); - P2trAddress.fromScript({required Script script}) - : super.fromScript( - segwitVersion: _BitcoinAddressUtils.segwitV1, script: script); + segwitVersion: _BitcoinAddressUtils.segwitV1, + addressType: SegwitAddresType.p2tr, + ); + + P2trAddress.fromRedeemScript({required super.script, super.network}) + : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV1); + + factory P2trAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddresType.p2tr) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2trAddress.fromProgram(program: script.findScriptParam(1), network: network); + } /// returns the scriptPubKey of a P2TR witness script @override Script toScriptPubKey() { - return Script(script: ['OP_1', addressProgram]); + return Script(script: [BitcoinOpCodeConst.OP_1, addressProgram]); } /// returns the type of address @@ -101,28 +128,87 @@ class P2trAddress extends SegwitAddress { } class P2wshAddress extends SegwitAddress { - P2wshAddress.fromAddress( - {required String address, required BasedUtxoNetwork network}) - : super.fromAddress( - segwitVersion: _BitcoinAddressUtils.segwitV0, - address: address, - network: network); - P2wshAddress.fromProgram({required String program}) + static RegExp get 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}) : super.fromProgram( - segwitVersion: _BitcoinAddressUtils.segwitV0, - program: program, - addresType: SegwitAddresType.p2wsh); - P2wshAddress.fromScript({required Script script}) - : super.fromScript( - segwitVersion: _BitcoinAddressUtils.segwitV0, script: script); + segwitVersion: _BitcoinAddressUtils.segwitV0, + addressType: SegwitAddresType.p2wsh, + ); + + P2wshAddress.fromRedeemScript({required super.script, super.network}) + : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); + + factory P2wshAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + if (script.getAddressType() != SegwitAddresType.p2wsh) { + throw ArgumentError("Invalid scriptPubKey"); + } + + return P2wshAddress.fromProgram(program: script.findScriptParam(1), network: network); + } /// Returns the scriptPubKey of a P2WPKH witness script @override Script toScriptPubKey() { - return Script(script: ['OP_0', addressProgram]); + return Script(script: [BitcoinOpCodeConst.OP_0, addressProgram]); } /// Returns the type of address @override SegwitAddresType get type => SegwitAddresType.p2wsh; } + +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}) { + final decoded = Bech32DecoderBase.decodeBech32( + address, + Bech32Const.separator, + Bech32Const.checksumStrLen, + (hrp, data) => Bech32Utils.verifyChecksum(hrp, data, Bech32Encodings.bech32)); + final hrp = decoded.item1; + final data = decoded.item2; + if (hrp != 'ltcmweb') { + throw ArgumentException('Invalid format (HRP not valid, expected ltcmweb, got $hrp)'); + } + if (data[0] != _BitcoinAddressUtils.segwitV0) { + throw const ArgumentException("Invalid segwit version"); + } + final convData = Bech32BaseUtils.convertFromBase32(data.sublist(1)); + if (convData.length != 66) { + throw ArgumentException( + 'Invalid format (witness program length not valid: ${convData.length})'); + } + + return MwebAddress.fromProgram(program: BytesUtils.toHexString(convData)); + } + + MwebAddress.fromProgram({required super.program}) + : super.fromProgram( + segwitVersion: _BitcoinAddressUtils.segwitV0, + addressType: SegwitAddresType.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"); + } + return MwebAddress.fromProgram(program: BytesUtils.toHexString(script.script as List)); + } + + /// returns the scriptPubKey of a MWEB witness script + @override + Script toScriptPubKey() { + return Script(script: BytesUtils.fromHexString(addressProgram)); + } + + /// returns the type of address + @override + SegwitAddresType get type => SegwitAddresType.mweb; +} diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart new file mode 100644 index 0000000..7ec257d --- /dev/null +++ b/lib/src/bitcoin/address/util.dart @@ -0,0 +1,44 @@ +import 'package:bitcoin_base/src/utils/utils.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/models/network.dart'; + +class BitcoinAddressUtils { + static bool validateAddress({required String address, required BasedUtxoNetwork network}) { + try { + addressToOutputScript(address: address, network: network); + return true; + } catch (_) { + return false; + } + } + + static List addressToOutputScript( + {required String address, required BasedUtxoNetwork network}) { + final addressType = RegexUtils.addressTypeFromStr(address, network); + + if (addressType.type == SegwitAddresType.mweb) { + return BytesUtils.fromHexString( + MwebAddress.fromAddress(address: address, network: network).addressProgram, + ); + } + + return addressType.toScriptPubKey().toBytes(); + } + + static String scriptHash(String address, {required BasedUtxoNetwork network}) { + final outputScript = addressToOutputScript(address: address, network: network); + final parts = BytesUtils.toHexString(QuickCrypto.sha256Hash(outputScript)).split(''); + var res = ''; + + for (var i = parts.length - 1; i >= 0; i--) { + final char = parts[i]; + i--; + final nextChar = parts[i]; + res += nextChar; + res += char; + } + + return res; + } +} diff --git a/lib/src/bitcoin/address/utils/address_utils.dart b/lib/src/bitcoin/address/utils/address_utils.dart index f6d2baa..9a27306 100644 --- a/lib/src/bitcoin/address/utils/address_utils.dart +++ b/lib/src/bitcoin/address/utils/address_utils.dart @@ -18,15 +18,13 @@ class _BitcoinAddressUtils { /// /// [address]: The legacy Bitcoin address to be decoded. /// Returns a tuple with script bytes and version if decoding is successful, otherwise null. - static Tuple, List>? decodeLagacyAddress( - {required String address}) { + static Tuple, List>? decodeLagacyAddress({required String address}) { try { /// Decode the base58-encoded address. 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 List scriptBytes = decode.sublist(1, decode.length - Base58Const.checksumByteLen); /// Ensure the script bytes have the expected length. if (scriptBytes.length != hash160DigestLength) { @@ -35,14 +33,11 @@ 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); + List data = decode.sublist(0, decode.length - Base58Const.checksumByteLen); + List checksum = decode.sublist(decode.length - Base58Const.checksumByteLen); /// Verify the checksum. - List hash = QuickCrypto.sha256DoubleHash(data) - .sublist(0, Base58Const.checksumByteLen); + List hash = QuickCrypto.sha256DoubleHash(data).sublist(0, Base58Const.checksumByteLen); if (!BytesUtils.bytesEqual(checksum, hash)) { return null; } @@ -66,9 +61,9 @@ class _BitcoinAddressUtils { } final decodedHex = BytesUtils.toHexString(decode.item1); if (BytesUtils.bytesEqual(decode.item2, networks.p2pkhNetVer)) { - return P2pkhAddress.fromHash160(addrHash: decodedHex); + return P2pkhAddress.fromHash160(h160: decodedHex); } else if (BytesUtils.bytesEqual(decode.item2, networks.p2shNetVer)) { - return P2shAddress.fromHash160(addrHash: decodedHex); + return P2shAddress.fromHash160(h160: decodedHex); } return null; } @@ -83,9 +78,7 @@ class _BitcoinAddressUtils { /// /// Throws a [MessageException] if the witness version does not match the specified version. static String toSegwitProgramWithVersionAndNetwork( - {required String address, - required BasedUtxoNetwork network, - required int version}) { + {required String address, required BasedUtxoNetwork network, required int version}) { final convert = SegwitBech32Decoder.decode(network.p2wpkhHrp, address); final witnessVersion = convert.item1; if (witnessVersion != version) { @@ -102,8 +95,7 @@ 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) { + static SegwitAddress? toP2wpkhAddress(String address, BasedUtxoNetwork network) { try { final convert = SegwitBech32Decoder.decode(network.p2wpkhHrp, address); final witnessVersion = convert.item1; @@ -132,8 +124,7 @@ class _BitcoinAddressUtils { /// Returns the validated Bitcoin base address if it belongs to a supported type for the given network. /// /// Throws a [MessageException] if the address type is not supported by the specified network. - static BitcoinBaseAddress validateAddress( - BitcoinBaseAddress address, BasedUtxoNetwork network) { + static BitcoinBaseAddress validateAddress(BitcoinBaseAddress address, BasedUtxoNetwork network) { if (network.supportedAddress.contains(address.type)) { return address; } @@ -150,15 +141,19 @@ class _BitcoinAddressUtils { /// Returns a BitcoinBaseAddress instance representing the decoded address. /// /// Throws a [MessageException] if the address is invalid or not supported by the network. - static BitcoinBaseAddress decodeAddress( - String address, BasedUtxoNetwork network) { + static BitcoinBaseAddress decodeAddress(String address, BasedUtxoNetwork network) { BitcoinBaseAddress? baseAddress; if (network.supportedAddress.contains(SegwitAddresType.p2wpkh)) { - baseAddress = toSegwitAddress(address, network); + baseAddress = toP2wpkhAddress(address, network); } baseAddress ??= toLegacy(address, network); if (baseAddress == null) { - throw const BitcoinBasePluginException("Invalid Bitcoin address"); + try { + throw const BitcoinBasePluginException("test2 Bitcoin address"); + } catch (e, s) { + print(s); + } + throw const BitcoinBasePluginException("test Bitcoin address"); } return validateAddress(baseAddress, network); } @@ -171,8 +166,7 @@ class _BitcoinAddressUtils { /// Returns the validated hash160 value if its length matches the expected length for the specified address type. /// /// Throws a [MessageException] if the hash160 value is invalid or has an incorrect length. - static String validateAddressProgram( - String hash160, BitcoinAddressType addressType) { + static String validateAddressProgram(String hash160, BitcoinAddressType addressType) { try { final toBytes = BytesUtils.fromHexString(hash160); if (toBytes.length == addressType.hashLength) { @@ -193,18 +187,15 @@ class _BitcoinAddressUtils { /// /// Returns a LegacyAddress instance representing the decoded BCH address if successful, /// or null if the decoding process fails. - static LegacyAddress? decodeBchAddress( - String address, BitcoinCashNetwork network, + static LegacyAddress? decodeBchAddress(String address, BitcoinCashNetwork network, {bool validateNetworkHRP = false}) { try { - final String hrp = validateNetworkHRP - ? network.networkHRP - : address.substring(0, address.indexOf(":")); + final String hrp = + validateNetworkHRP ? network.networkHRP : address.substring(0, address.indexOf(":")); final decode = BchBech32Decoder.decode(hrp, address); final scriptBytes = decode.item2; final version = decode.item1; - return _validateBchScriptBytes( - network: network, scriptBytes: scriptBytes, version: version); + return _validateBchScriptBytes(network: network, scriptBytes: scriptBytes, version: version); } catch (e) { return null; } @@ -224,8 +215,7 @@ class _BitcoinAddressUtils { required BitcoinCashNetwork network}) { final scriptHex = BytesUtils.toHexString(scriptBytes); final scriptLength = scriptBytes.length; - if (scriptLength != hash160DigestLength && - scriptLength != scriptHashLenght) { + if (scriptLength != hash160DigestLength && scriptLength != scriptHashLenght) { return null; } if (scriptLength == hash160DigestLength) { @@ -234,28 +224,22 @@ class _BitcoinAddressUtils { if (BytesUtils.bytesEqual(network.p2pkhNetVer, version) || BytesUtils.bytesEqual(network.p2pkhWtNetVer, version)) { return P2pkhAddress.fromHash160( - addrHash: scriptHex, - type: - legacyP2pk ? P2pkhAddressType.p2pkh : P2pkhAddressType.p2pkhwt); + h160: scriptHex, type: legacyP2pk ? P2pkhAddressType.p2pkh : P2pkhAddressType.p2pkhwt); } final legacyP2sh = BytesUtils.bytesEqual(network.p2shNetVer, version); if (BytesUtils.bytesEqual(network.p2shNetVer, version) || BytesUtils.bytesEqual(network.p2shwt20NetVer, version)) { return P2shAddress.fromHash160( - addrHash: scriptHex, - type: legacyP2sh - ? P2shAddressType.p2pkhInP2sh - : P2shAddressType.p2pkhInP2shwt); + h160: scriptHex, + type: legacyP2sh ? P2shAddressType.p2pkhInP2sh : P2shAddressType.p2pkhInP2shwt); } } else { final legacyP2sh = BytesUtils.bytesEqual(network.p2sh32NetVer, version); if (BytesUtils.bytesEqual(network.p2sh32NetVer, version) || BytesUtils.bytesEqual(network.p2shwt32NetVer, version)) { return P2shAddress.fromHash160( - addrHash: scriptHex, - type: legacyP2sh - ? P2shAddressType.p2pkhInP2sh32 - : P2shAddressType.p2pkhInP2sh32wt); + h160: scriptHex, + type: legacyP2sh ? P2shAddressType.p2pkhInP2sh32 : P2shAddressType.p2pkhInP2sh32wt); } } return null; @@ -271,7 +255,7 @@ class _BitcoinAddressUtils { /// Returns the address program in hexadecimal format if successful, or null if decoding or validation fails. /// /// Throws a [MessageException] if the specified network does not support the given address type. - static String? decodeLagacyAddressWithNetworkAndType( + static String? decodeLegacyAddressWithNetworkAndType( {required String address, required BitcoinAddressType type, required BasedUtxoNetwork network}) { @@ -336,8 +320,7 @@ class _BitcoinAddressUtils { required BasedUtxoNetwork network, required int segwitVersion}) { final programBytes = BytesUtils.fromHexString(addressProgram); - return SegwitBech32Encoder.encode( - network.p2wpkhHrp, segwitVersion, programBytes); + return SegwitBech32Encoder.encode(network.p2wpkhHrp, segwitVersion, programBytes); } /// Converts a Bitcoin legacy address program to its corresponding Bitcoin Cash (BCH) address. @@ -352,11 +335,10 @@ class _BitcoinAddressUtils { required String addressProgram, required BitcoinAddressType type}) { List programBytes = BytesUtils.fromHexString(addressProgram); - final List netVersion = _getBchNetVersion( - network: network, type: type, secriptLength: programBytes.length); + final List netVersion = + _getBchNetVersion(network: network, type: type, secriptLength: programBytes.length); - return BchBech32Encoder.encode( - network.networkHRP, netVersion, programBytes); + return BchBech32Encoder.encode(network.networkHRP, netVersion, programBytes); } /// Helper method to obtain the Bitcoin Cash network version bytes based on the address type and script length. @@ -403,8 +385,7 @@ class _BitcoinAddressUtils { required String addressProgram, required BitcoinAddressType type}) { if (network is BitcoinCashNetwork) { - return legacyToBchAddress( - addressProgram: addressProgram, network: network, type: type); + return legacyToBchAddress(addressProgram: addressProgram, network: network, type: type); } List programBytes = BytesUtils.fromHexString(addressProgram); switch (type) { @@ -448,7 +429,7 @@ class _BitcoinAddressUtils { /// Returns the hexadecimal representation of the reversed SHA-256 hash160 of the script's static String pubKeyHash(Script scriptPubKey) { - return BytesUtils.toHexString(List.from( - QuickCrypto.sha256Hash(scriptPubKey.toBytes()).reversed)); + return BytesUtils.toHexString( + List.from(QuickCrypto.sha256Hash(scriptPubKey.toBytes()).reversed)); } } diff --git a/lib/src/bitcoin/script/input.dart b/lib/src/bitcoin/script/input.dart index b55b68a..149d545 100644 --- a/lib/src/bitcoin/script/input.dart +++ b/lib/src/bitcoin/script/input.dart @@ -14,9 +14,9 @@ class TxInput { {required this.txId, required this.txIndex, Script? scriptSig, - List? sequance}) + List? sequence}) : sequence = List.unmodifiable( - sequance ?? BitcoinOpCodeConst.DEFAULT_TX_SEQUENCE), + sequence ?? BitcoinOpCodeConst.DEFAULT_TX_SEQUENCE), scriptSig = scriptSig ?? Script(script: []); TxInput copyWith( {String? txId, int? txIndex, Script? scriptSig, List? sequence}) { @@ -24,7 +24,7 @@ class TxInput { txId: txId ?? this.txId, txIndex: txIndex ?? this.txIndex, scriptSig: scriptSig ?? this.scriptSig, - sequance: sequence ?? this.sequence); + sequence: sequence ?? this.sequence); } final String txId; @@ -35,7 +35,7 @@ class TxInput { /// creates a copy of the object TxInput copy() { return TxInput( - txId: txId, txIndex: txIndex, scriptSig: scriptSig, sequance: sequence); + txId: txId, txIndex: txIndex, scriptSig: scriptSig, sequence: sequence); } /// serializes TxInput to bytes @@ -82,7 +82,7 @@ class TxInput { scriptSig: Script.fromRaw( hexData: BytesUtils.toHexString(unlockingScript), hasSegwit: hasSegwit), - sequance: sequenceNumberData), + sequence: sequenceNumberData), cursor); } diff --git a/lib/src/bitcoin/script/op_code/constant.dart b/lib/src/bitcoin/script/op_code/constant.dart index cb6b952..bd62271 100644 --- a/lib/src/bitcoin/script/op_code/constant.dart +++ b/lib/src/bitcoin/script/op_code/constant.dart @@ -1,7 +1,126 @@ -/// ignore_for_file: constant_identifier_names, equal_keys_in_map, non_constant_identifier_names +/// ignore_for_file: constant_identifier_names, equal_keys_in_map, non_constant_identifier_names, camel_case_types /// Constants and identifiers used in the Bitcoin-related code. -// ignore_for_file: constant_identifier_names, non_constant_identifier_names, equal_keys_in_map +// ignore_for_file: constant_identifier_names, non_constant_identifier_names, equal_keys_in_map, camel_case_types + class BitcoinOpCodeConst { + static const OP_0 = "OP_0"; + static const OP_FALSE = "OP_FALSE"; + static const OP_PUSHDATA1 = "OP_PUSHDATA1"; + static const OP_PUSHDATA2 = "OP_PUSHDATA2"; + static const OP_PUSHDATA4 = "OP_PUSHDATA4"; + static const OP_1NEGATE = "OP_1NEGATE"; + static const OP_1 = "OP_1"; + static const OP_TRUE = "OP_TRUE"; + static const OP_2 = "OP_2"; + static const OP_3 = "OP_3"; + static const OP_4 = "OP_4"; + static const OP_5 = "OP_5"; + static const OP_6 = "OP_6"; + static const OP_7 = "OP_7"; + static const OP_8 = "OP_8"; + static const OP_9 = "OP_9"; + static const OP_10 = "OP_10"; + static const OP_11 = "OP_11"; + static const OP_12 = "OP_12"; + static const OP_13 = "OP_13"; + static const OP_14 = "OP_14"; + static const OP_15 = "OP_15"; + static const OP_16 = "OP_16"; + + /// flow control + static const OP_NOP = "OP_NOP"; + static const OP_IF = "OP_IF"; + static const OP_NOTIF = "OP_NOTIF"; + static const OP_ELSE = "OP_ELSE"; + static const OP_ENDIF = "OP_ENDIF"; + static const OP_VERIFY = "OP_VERIFY"; + static const OP_RETURN = "OP_RETURN"; + + /// stack + static const OP_TOALTSTACK = "OP_TOALTSTACK"; + static const OP_FROMALTSTACK = "OP_FROMALTSTACK"; + static const OP_IFDUP = "OP_IFDUP"; + static const OP_DEPTH = "OP_DEPTH"; + static const OP_DROP = "OP_DROP"; + static const OP_DUP = "OP_DUP"; + static const OP_NIP = "OP_NIP"; + static const OP_OVER = "OP_OVER"; + static const OP_PICK = "OP_PICK"; + static const OP_ROLL = "OP_ROLL"; + static const OP_ROT = "OP_ROT"; + static const OP_SWAP = "OP_SWAP"; + static const OP_TUCK = "OP_TUCK"; + static const OP_2DROP = "OP_2DROP"; + static const OP_2DUP = "OP_2DUP"; + static const OP_3DUP = "OP_3DUP"; + static const OP_2OVER = "OP_2OVER"; + static const OP_2ROT = "OP_2ROT"; + static const OP_2SWAP = "OP_2SWAP"; + + /// splice + /// 'OP_CAT': [0x7e], + /// 'OP_SUBSTR': [0x7f], + /// 'OP_LEFT': [0x80], + /// 'OP_RIGHT': [0x81], + static const OP_SIZE = "OP_SIZE"; + + /// bitwise logic + /// 'OP_INVERT': [0x83], + /// 'OP_AND': [0x84], + /// 'OP_OR': [0x85], + /// 'OP_XOR': [0x86], + static const OP_EQUAL = "OP_EQUAL"; + static const OP_EQUALVERIFY = "OP_EQUALVERIFY"; + + /// arithmetic + static const OP_1ADD = "OP_1ADD"; + static const OP_1SUB = "OP_1SUB"; + + /// 'OP_2MUL': [0x8d], + /// 'OP_2DIV': [0x8e], + static const OP_NEGATE = "OP_NEGATE"; + static const OP_ABS = "OP_ABS"; + static const OP_NOT = "OP_NOT"; + static const OP_0NOTEQUAL = "OP_0NOTEQUAL"; + static const OP_ADD = "OP_ADD"; + static const OP_SUB = "OP_SUB"; + + /// 'OP_MUL': [0x95], + /// 'OP_DIV': [0x96], + /// 'OP_MOD': [0x97], + /// 'OP_LSHIFT': [0x98], + /// 'OP_RSHIFT': [0x99], + static const OP_BOOLAND = "OP_BOOLAND"; + static const OP_BOOLOR = "OP_BOOLOR"; + static const OP_NUMEQUAL = "OP_NUMEQUAL"; + static const OP_NUMEQUALVERIFY = "OP_NUMEQUALVERIFY"; + static const OP_NUMNOTEQUAL = "OP_NUMNOTEQUAL"; + static const OP_LESSTHAN = "OP_LESSTHAN"; + static const OP_GREATERTHAN = "OP_GREATERTHAN"; + static const OP_LESSTHANOREQUAL = "OP_LESSTHANOREQUAL"; + static const OP_GREATERTHANOREQUAL = "OP_GREATERTHANOREQUAL"; + static const OP_MIN = "OP_MIN"; + static const OP_MAX = "OP_MAX"; + static const OP_WITHIN = "OP_WITHIN"; + + /// crypto + static const OP_RIPEMD160 = "OP_RIPEMD160"; + static const OP_SHA1 = "OP_SHA1"; + static const OP_SHA256 = "OP_SHA256"; + static const OP_HASH160 = "OP_HASH160"; + static const OP_HASH256 = "OP_HASH256"; + static const OP_CODESEPARATOR = "OP_CODESEPARATOR"; + static const OP_CHECKSIG = "OP_CHECKSIG"; + static const OP_CHECKSIGVERIFY = "OP_CHECKSIGVERIFY"; + static const OP_CHECKMULTISIG = "OP_CHECKMULTISIG"; + static const OP_CHECKMULTISIGVERIFY = "OP_CHECKMULTISIGVERIFY"; + + /// locktime + static const OP_NOP2 = "OP_NOP2"; + static const OP_CHECKLOCKTIMEVERIFY = "OP_CHECKLOCKTIMEVERIFY"; + static const OP_NOP3 = "OP_NOP3"; + static const OP_CHECKSEQUENCEVERIFY = "OP_CHECKSEQUENCEVERIFY"; + static const Map> OP_CODES = { 'OP_0': [0x00], 'OP_FALSE': [0x00], diff --git a/lib/src/bitcoin/script/outpoint.dart b/lib/src/bitcoin/script/outpoint.dart new file mode 100644 index 0000000..8af5748 --- /dev/null +++ b/lib/src/bitcoin/script/outpoint.dart @@ -0,0 +1,18 @@ +import 'package:blockchain_utils/blockchain_utils.dart'; + +class Outpoint { + Outpoint({required this.txid, required this.index, this.value}); + + String txid; + int index; + int? value; + + factory Outpoint.fromBytes(List txid, int index, {int? value}) { + return Outpoint(txid: BytesUtils.toHexString(txid), index: index, value: value); + } + + @override + String toString() { + return 'Outpoint{txid: $txid, index: $index, value: $value}'; + } +} diff --git a/lib/src/bitcoin/script/output.dart b/lib/src/bitcoin/script/output.dart index 8a25497..09b11e2 100644 --- a/lib/src/bitcoin/script/output.dart +++ b/lib/src/bitcoin/script/output.dart @@ -8,56 +8,58 @@ import 'package:blockchain_utils/utils/utils.dart'; /// [amount] the value we want to send to this output in satoshis /// [scriptPubKey] the script that will lock this amount class TxOutput { - const TxOutput( - {required this.amount, required this.scriptPubKey, this.cashToken}); + const TxOutput({ + required this.amount, + required this.scriptPubKey, + this.cashToken, + this.isSilentPayment = false, + this.isChange = false, + }); final CashToken? cashToken; final BigInt amount; final Script scriptPubKey; + final bool isSilentPayment; + final bool isChange; /// creates a copy of the object TxOutput copy() { return TxOutput( - amount: amount, - scriptPubKey: Script(script: List.from(scriptPubKey.script)), - cashToken: cashToken); + amount: amount, + scriptPubKey: Script(script: List.from(scriptPubKey.script)), + cashToken: cashToken, + isSilentPayment: isSilentPayment, + isChange: isChange, + ); } 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); + List 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); + 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)); cursor += vi.item2; final token = CashToken.fromRaw(txBytes.sublist(cursor)); - List lockScript = - txBytes.sublist(cursor + token.item2, cursor + vi.item1); + List 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), - hasSegwit: hasSegwit)), + hexData: BytesUtils.toHexString(lockScript), + hasSegwit: hasSegwit, + )), cursor); } diff --git a/lib/src/bitcoin/script/script.dart b/lib/src/bitcoin/script/script.dart index 7c46945..9cf8b2d 100644 --- a/lib/src/bitcoin/script/script.dart +++ b/lib/src/bitcoin/script/script.dart @@ -1,4 +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/models/network.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; /// A Script contains just a list of OP_CODES and also knows how to serialize into bytes @@ -16,10 +18,13 @@ class Script { script = List.unmodifiable(script); final List script; - static Script fromRaw({required String hexData, bool hasSegwit = false}) { + static Script fromRaw({List? byteData, String? hexData, bool hasSegwit = false}) { List commands = []; int index = 0; - final scriptBytes = BytesUtils.fromHexString(hexData); + final scriptBytes = byteData ?? (hexData != null ? BytesUtils.fromHexString(hexData) : null); + if (scriptBytes == null) { + throw ArgumentError("Invalid script"); + } while (index < scriptBytes.length) { int byte = scriptBytes[index]; if (BitcoinOpCodeConst.CODE_OPS.containsKey(byte)) { @@ -28,41 +33,121 @@ class Script { } else if (!hasSegwit && byte == 0x4c) { int bytesToRead = scriptBytes[index + 1]; index = index + 1; - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index, index + bytesToRead))); + 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))); + 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))); + commands.add(BytesUtils.toHexString(scriptBytes.sublist(index, index + bytesToRead))); index = index + bytesToRead; } else { - final viAndSize = - IntUtils.decodeVarint(scriptBytes.sublist(index, index + 9)); + final viAndSize = IntUtils.decodeVarint(scriptBytes.sublist(index, index + 9)); int dataSize = viAndSize.item1; int size = viAndSize.item2; final lastIndex = (index + size + dataSize) > scriptBytes.length ? scriptBytes.length : (index + size + dataSize); - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index + size, lastIndex))); + commands.add(BytesUtils.toHexString(scriptBytes.sublist(index + size, lastIndex))); index = index + dataSize + size; } } return Script(script: commands); } + dynamic findScriptParam(int index) { + if (index < script.length) { + return script[index]; + } + return null; + } + + BitcoinAddressType? getAddressType() { + if (script.isEmpty) return null; + + if (script.every((x) => x is int) && + script.length == 66 && + (script[0] == 2 || script[0] == 3) && + (script[33] == 2 || script[33] == 3)) { + return SegwitAddresType.mweb; + } + + final first = findScriptParam(0); + final sec = findScriptParam(1); + if (sec == null || sec is! String) { + return null; + } + + if (first == "OP_0") { + final lockingScriptBytes = opPushData(sec); + + if (lockingScriptBytes.length == 21) { + return SegwitAddresType.p2wpkh; + } else if (lockingScriptBytes.length == 33) { + return SegwitAddresType.p2wsh; + } + } else if (first == "OP_1") { + final lockingScriptBytes = opPushData(sec); + + if (lockingScriptBytes.length == 33) { + return SegwitAddresType.p2tr; + } + } + + final third = findScriptParam(2); + final fourth = findScriptParam(3); + final fifth = findScriptParam(4); + if (first == "OP_DUP") { + if (sec == "OP_HASH160" && + opPushData(third).length == 21 && + fourth == "OP_EQUALVERIFY" && + fifth == "OP_CHECKSIG") { + return P2pkhAddressType.p2pkh; + } + } else if (first == "OP_HASH160" && opPushData(sec).length == 21 && third == "OP_EQUAL") { + return P2shAddressType.p2pkhInP2sh; + } else if (sec == "OP_CHECKSIG") { + if (first.length == 66) { + return PubKeyAddressType.p2pk; + } + } + + return null; + } + + String toAddress({BasedUtxoNetwork? network}) { + final addressType = getAddressType(); + final _network = network ?? BitcoinNetwork.mainnet; + if (addressType == null) { + throw ArgumentError("Invalid script"); + } + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromScriptPubkey(script: this).toAddress(_network); + case P2shAddressType.p2pkhInP2sh: + return P2shAddress.fromScriptPubkey(script: this).toAddress(_network); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromScriptPubkey(script: this).toAddress(_network); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromScriptPubkey(script: this).toAddress(_network); + case SegwitAddresType.p2tr: + return P2trAddress.fromScriptPubkey(script: this).toAddress(_network); + } + + throw ArgumentError("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) { if (BitcoinOpCodeConst.OP_CODES.containsKey(token)) { diff --git a/lib/src/bitcoin/script/scripts.dart b/lib/src/bitcoin/script/scripts.dart index edeea0a..e74ba6c 100644 --- a/lib/src/bitcoin/script/scripts.dart +++ b/lib/src/bitcoin/script/scripts.dart @@ -6,3 +6,4 @@ export 'sequence.dart'; export 'transaction.dart'; export 'witness.dart'; export 'op_code/constant_lib.dart'; +export 'outpoint.dart'; diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index da9b409..4e53e71 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -5,6 +5,7 @@ import 'package:bitcoin_base/src/crypto/crypto.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/utils/utils.dart'; import 'package:blockchain_utils/crypto/quick_crypto.dart'; +import 'package:collection/collection.dart'; import 'input.dart'; import 'output.dart'; import 'script.dart'; @@ -19,17 +20,18 @@ import 'witness.dart'; /// [hasSegwit] Specifies a tx that includes segwit inputs /// [witnesses] The witness structure that corresponds to the inputs class BtcTransaction { - BtcTransaction( - {required List inputs, - required List outputs, - List witnesses = const [], - this.hasSegwit = false, - List? lock, - List? version}) - : locktime = List.unmodifiable( - lock ?? BitcoinOpCodeConst.DEFAULT_TX_LOCKTIME), - version = List.unmodifiable( - version ?? BitcoinOpCodeConst.DEFAULT_TX_VERSION), + BtcTransaction({ + required List inputs, + required List outputs, + List witnesses = const [], + this.hasSegwit = false, + this.canReplaceByFee = false, + this.mwebBytes, + 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); @@ -38,7 +40,10 @@ class BtcTransaction { final List locktime; final List version; final bool hasSegwit; + final bool canReplaceByFee; final List witnesses; + final List? mwebBytes; + final bool hasSilentPayment; BtcTransaction copyWith({ List? inputs, @@ -53,8 +58,10 @@ class BtcTransaction { outputs: outputs ?? this.outputs, witnesses: witnesses ?? this.witnesses, hasSegwit: hasSegwit ?? this.hasSegwit, + mwebBytes: mwebBytes, lock: lock ?? List.from(locktime), version: version ?? List.from(this.version), + hasSilentPayment: hasSilentPayment, ); } @@ -65,6 +72,7 @@ class BtcTransaction { inputs: tx.inputs.map((e) => e.copy()).toList(), outputs: tx.outputs.map((e) => e.copy()).toList(), witnesses: tx.witnesses.map((e) => e.copy()).toList(), + mwebBytes: tx.mwebBytes, lock: tx.locktime, version: tx.version); } @@ -76,37 +84,49 @@ class BtcTransaction { int cursor = 4; List? flag; bool hasSegwit = false; + bool hasMweb = false; if (rawtx[4] == 0) { flag = List.from(rawtx.sublist(5, 6)); - if (flag[0] == 1) { + if (flag[0] & 1 > 0) { hasSegwit = true; } + if (flag[0] & 8 > 0) { + hasMweb = true; + } cursor += 2; } final vi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); cursor += vi.item2; + bool canReplaceByFee = false; List inputs = []; for (int index = 0; index < vi.item1; index++) { - final inp = - TxInput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + final inp = TxInput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); - inputs.add(inp.item1); + final input = inp.item1; + inputs.add(input); cursor = inp.item2; + + if (canReplaceByFee == false) { + canReplaceByFee = + const ListEquality().equals(input.sequence, BitcoinOpCodeConst.REPLACE_BY_FEE_SEQUENCE); + } } List outputs = []; final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); cursor += viOut.item2; for (int index = 0; index < viOut.item1; index++) { - final inp = - TxOutput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + final inp = TxOutput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); outputs.add(inp.item1); cursor = inp.item2; } List 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 = []; @@ -114,8 +134,7 @@ class BtcTransaction { 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); + witness = rawtx.sublist(cursor + wtVi.item2, cursor + wtVi.item1 + wtVi.item2); } cursor += wtVi.item1 + wtVi.item2; witnessesTmp.add(BytesUtils.toHexString(witness)); @@ -124,14 +143,22 @@ class BtcTransaction { witnesses.add(TxWitnessInput(stack: witnessesTmp)); } } + List? mwebBytes; + if (hasMweb) { + mwebBytes = rawtx.sublist(cursor, rawtx.length - 4); + } + cursor = rawtx.length - 4; List lock = rawtx.sublist(cursor, cursor + 4); return BtcTransaction( - inputs: inputs, - outputs: outputs, - witnesses: witnesses, - hasSegwit: hasSegwit, - 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. @@ -153,8 +180,7 @@ class BtcTransaction { tx = tx.copyWith(outputs: []); for (int i = 0; i < tx.inputs.length; i++) { if (i != txInIndex) { - tx.inputs[i].sequence = - List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); + tx.inputs[i].sequence = List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); } } } else if ((sighash & 0x1f) == BitcoinOpCodeConst.SIGHASH_SINGLE) { @@ -172,8 +198,7 @@ class BtcTransaction { tx = tx.copyWith(outputs: [...outputs, tx.outputs[txInIndex]]); for (int i = 0; i < tx.inputs.length; i++) { if (i != txInIndex) { - tx.inputs[i].sequence = - List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); + tx.inputs[i].sequence = List.unmodifiable(BitcoinOpCodeConst.EMPTY_TX_SEQUENCE); } } } @@ -182,10 +207,8 @@ class BtcTransaction { } List txForSign = tx.toBytes(segwit: false); - txForSign = List.from([ - ...txForSign, - ...IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little) - ]); + txForSign = List.from( + [...txForSign, ...IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little)]); return QuickCrypto.sha256DoubleHash(txForSign); } @@ -193,8 +216,11 @@ class BtcTransaction { List toBytes({bool segwit = false}) { DynamicByteTracker data = DynamicByteTracker(); data.add(version); - if (segwit) { - data.add([0x00, 0x01]); + var flag = 0; + if (segwit) flag |= 1; + if (mwebBytes != null) flag |= 8; + if (flag > 0) { + data.add([0x00, flag]); } final txInCountBytes = IntUtils.encodeVarint(inputs.length); final txOutCountBytes = IntUtils.encodeVarint(outputs.length); @@ -214,6 +240,9 @@ class BtcTransaction { data.add(wit.toBytes()); } } + if (mwebBytes != null) { + data.add(mwebBytes!); + } data.add(locktime); return data.toBytes(); } @@ -235,15 +264,13 @@ class BtcTransaction { List hashSequence = List.filled(32, 0); List hashOutputs = List.filled(32, 0); int basicSigHashType = sighash & 0x1F; - bool anyoneCanPay = - (sighash & 0xF0) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; + bool anyoneCanPay = (sighash & 0xF0) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; bool 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()); + List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); hashPrevouts = List.from([ ...hashPrevouts, ...txidBytes, @@ -269,12 +296,10 @@ class BtcTransaction { } 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); + 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]); + hashOutputs = List.from([...packedAmount, ...lenScriptBytes, ...scriptBytes]); hashOutputs = QuickCrypto.sha256DoubleHash(hashOutputs); } @@ -284,26 +309,21 @@ class BtcTransaction { 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) - ])); + List txidBytes = List.from(BytesUtils.fromHexString(txIn.txId).reversed.toList()); + txForSigning.add(List.from( + [...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); + List 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)); + txForSigning.add(IntUtils.toBytes(sighash, length: 4, byteOrder: Endian.little)); return QuickCrypto.sha256DoubleHash(txForSigning.toBytes()); } @@ -328,8 +348,7 @@ class BtcTransaction { 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; + bool anyoneCanPay = (sighash & 0x80) == BitcoinOpCodeConst.SIGHASH_ANYONECANPAY; DynamicByteTracker txForSign = DynamicByteTracker(); txForSign.add([0]); txForSign.add([sighash]); @@ -342,8 +361,7 @@ class BtcTransaction { List hashOutputs = []; if (!anyoneCanPay) { for (final txin in newTx.inputs) { - List txidBytes = List.from( - BytesUtils.fromHexString(txin.txId).reversed.toList()); + List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); hashPrevouts = List.from([ ...hashPrevouts, ...txidBytes, @@ -354,8 +372,7 @@ class BtcTransaction { txForSign.add(hashPrevouts); for (final i in amounts) { - List bytes = - BigintUtils.toBytes(i, length: 8, order: Endian.little); + List bytes = BigintUtils.toBytes(i, length: 8, order: Endian.little); hashAmounts = List.from([...hashAmounts, ...bytes]); } @@ -369,8 +386,7 @@ class BtcTransaction { int scriptLen = h.length ~/ 2; List scriptBytes = BytesUtils.fromHexString(h); List lenBytes = List.from([scriptLen]); - hashScriptPubkeys = - List.from([...hashScriptPubkeys, ...lenBytes, ...scriptBytes]); + hashScriptPubkeys = List.from([...hashScriptPubkeys, ...lenBytes, ...scriptBytes]); } hashScriptPubkeys = QuickCrypto.sha256Hash(hashScriptPubkeys); txForSign.add(hashScriptPubkeys); @@ -383,17 +399,12 @@ class BtcTransaction { } if (!(sighashNone || sighashSingle)) { for (final txOut in newTx.outputs) { - List packedAmount = - BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); + 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 - ]); + hashOutputs = + List.from([...hashOutputs, ...packedAmount, ...lenScriptBytes, ...scriptBytes]); } hashOutputs = QuickCrypto.sha256Hash(hashOutputs); txForSign.add(hashOutputs); @@ -404,15 +415,11 @@ class BtcTransaction { 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) - ]); + List txidBytes = List.from(BytesUtils.fromHexString(txin.txId).reversed.toList()); + List result = List.from( + [...txidBytes, ...IntUtils.toBytes(txin.txIndex, length: 4, byteOrder: Endian.little)]); txForSign.add(result); - txForSign.add(BigintUtils.toBytes(amounts[txIndex], - length: 8, order: Endian.little)); + txForSign.add(BigintUtils.toBytes(amounts[txIndex], length: 8, order: Endian.little)); final sPubKey = scriptPubKeys[txIndex].toHex(); final sLength = sPubKey.length ~/ 2; txForSign.add([sLength]); @@ -427,18 +434,16 @@ class BtcTransaction { if (sighashSingle) { final txOut = newTx.outputs[txIndex]; - List packedAmount = - BigintUtils.toBytes(txOut.amount, length: 8, order: Endian.little); + 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 hashOut = List.from([...packedAmount, ...lenScriptBytes, ...sBytes]); txForSign.add(QuickCrypto.sha256Hash(hashOut)); } if (extFlags == 1) { - final leafVarBytes = List.from( - [leafVar, ...IntUtils.prependVarint(script?.toBytes() ?? [])]); + final leafVarBytes = + List.from([leafVar, ...IntUtils.prependVarint(script?.toBytes() ?? [])]); txForSign.add(taggedHash(leafVarBytes, "TapLeaf")); txForSign.add([0]); txForSign.add(List.filled(4, mask8)); diff --git a/lib/src/bitcoin/script/witness.dart b/lib/src/bitcoin/script/witness.dart index 86b0db5..88c7aca 100644 --- a/lib/src/bitcoin/script/witness.dart +++ b/lib/src/bitcoin/script/witness.dart @@ -1,12 +1,27 @@ +import 'dart:typed_data'; + import 'package:blockchain_utils/utils/utils.dart'; +class ScriptWitness { + List stack; + + ScriptWitness({List? stack}) : stack = stack ?? []; + + bool isNull() { + return stack.isEmpty; + } +} + /// A list of the witness items required to satisfy the locking conditions of a segwit input (aka witness stack). /// /// [stack] the witness items (hex str) list class TxWitnessInput { - TxWitnessInput({required List stack}) - : stack = List.unmodifiable(stack); + TxWitnessInput({required List stack, ScriptWitness? scriptWitness}) + : stack = List.unmodifiable(stack), + scriptWitness = scriptWitness ?? ScriptWitness(); + final List stack; + ScriptWitness scriptWitness; /// creates a copy of the object (classmethod) TxWitnessInput copy() { @@ -18,8 +33,7 @@ class TxWitnessInput { List stackBytes = []; for (String item in stack) { - List itemBytes = - IntUtils.prependVarint(BytesUtils.fromHexString(item)); + List itemBytes = IntUtils.prependVarint(BytesUtils.fromHexString(item)); stackBytes = [...stackBytes, ...itemBytes]; } diff --git a/lib/src/bitcoin/silent_payments/address.dart b/lib/src/bitcoin/silent_payments/address.dart new file mode 100644 index 0000000..e622851 --- /dev/null +++ b/lib/src/bitcoin/silent_payments/address.dart @@ -0,0 +1,202 @@ +// ignore_for_file: constant_identifier_names +// 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; + + SilentPaymentOwner({ + required super.version, + required super.B_scan, + required super.B_spend, + required this.b_scan, + required this.b_spend, + super.network, + }) : super(); + + factory SilentPaymentOwner.fromPrivateKeys({ + required ECPrivate b_scan, + required ECPrivate b_spend, + required BasedUtxoNetwork network, + int? version, + }) { + return SilentPaymentOwner( + b_scan: b_scan, + 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); + + 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); + } + + List generateLabel(int m) { + return taggedHash(BytesUtils.concatBytes([b_scan.toBytes(), serUint32(m)]), "BIP0352/Label"); + } + + SilentPaymentOwner toLabeledSilentPaymentAddress(int m) { + final B_m = B_spend.tweakAdd(BigintUtils.fromBytes(generateLabel(m))); + return SilentPaymentOwner( + b_scan: b_scan, + b_spend: b_spend, + B_scan: B_scan, + B_spend: B_m, + network: network, + version: version, + ); + } +} + +class SilentPaymentDestination extends SilentPaymentAddress { + SilentPaymentDestination({ + required super.version, + required ECPublic scanPubkey, + required ECPublic spendPubkey, + super.network, + required this.amount, + }) : super(B_scan: scanPubkey, B_spend: spendPubkey); + + int amount; + + factory SilentPaymentDestination.fromAddress(String address, int amount) { + final receiver = SilentPaymentAddress.fromAddress(address); + + return SilentPaymentDestination( + scanPubkey: receiver.B_scan, + spendPubkey: receiver.B_spend, + network: receiver.network, + version: receiver.version, + amount: amount, + ); + } +} + +class SilentPaymentAddress implements BitcoinBaseAddress { + static RegExp get regex => RegExp(r'(tsp|sp|sprt)1[0-9a-zA-Z]{113}'); + + 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"); + } + } + + factory SilentPaymentAddress.fromAddress(String address) { + final decoded = Bech32DecoderBase.decodeBech32( + address, + SegwitBech32Const.separator, + SegwitBech32Const.checksumStrLen, + (hrp, data) => Bech32Utils.verifyChecksum(hrp, data, Bech32Encodings.bech32m), + ); + final prefix = decoded.item1; + final words = decoded.item2; + + if (prefix != 'sp' && prefix != 'sprt' && prefix != 'tsp') { + throw Exception('Invalid prefix: $prefix'); + } + + final version = words[0]; + if (version != 0) throw ArgumentError('Invalid version'); + + final key = Bech32BaseUtils.convertFromBase32(words.sublist(1)); + + 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]) { + return toString(network: network); + } + + @override + String toString({BasedUtxoNetwork? network}) { + return Bech32EncoderBase.encodeBech32( + hrp, + [ + version, + ...Bech32BaseUtils.convertToBase32( + [...B_scan.toCompressedBytes(), ...B_spend.toCompressedBytes()]) + ], + SegwitBech32Const.separator, + (hrp, data) => Bech32Utils.computeChecksum(hrp, data, Bech32Encodings.bech32m), + ); + } + + @override + BitcoinAddressType get type => SilentPaymentsAddresType.p2sp; + + @override + Script toScriptPubKey() { + throw UnimplementedError(); + } + + @override + String pubKeyHash() { + throw UnimplementedError(); + } + + @override + String get addressProgram => ""; +} + +class Bech32U5 { + final int value; + + Bech32U5(this.value) { + if (value < 0 || value > 31) { + throw Exception('Value is outside the valid range.'); + } + } + + static Bech32U5 tryFromInt(int value) { + if (value < 0 || value > 31) { + throw Exception('Value is outside the valid range.'); + } + return Bech32U5(value); + } +} diff --git a/lib/src/bitcoin/silent_payments/payment.dart b/lib/src/bitcoin/silent_payments/payment.dart new file mode 100644 index 0000000..c04cf12 --- /dev/null +++ b/lib/src/bitcoin/silent_payments/payment.dart @@ -0,0 +1,242 @@ +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +class SilentPaymentOutput { + final P2trAddress address; + final int amount; + + SilentPaymentOutput(this.address, this.amount); +} + +class SilentPaymentScanningOutput { + final SilentPaymentOutput output; + final String tweak; + final String? label; + + SilentPaymentScanningOutput({required this.output, required this.tweak, this.label}); +} + +class ECPrivateInfo { + final ECPrivate privkey; + final bool isTaproot; + final bool tweak; + + ECPrivateInfo(this.privkey, this.isTaproot, {this.tweak = false}); +} + +class SilentPaymentBuilder { + final List vinOutpoints; + final List? pubkeys; + ECPublic? A_sum; + List? inputHash; + String? receiverTweak; + + SilentPaymentBuilder({ + required this.vinOutpoints, + this.pubkeys, + this.receiverTweak, + }) { + if (receiverTweak == null && pubkeys != null) { + _getAsum(); + _getInputHash(); + } + } + + void _getAsum() { + final head = pubkeys!.first; + final tail = pubkeys!.sublist(1); + + A_sum = + tail.fold(head, (acc, item) => ECPublic.fromBip32(acc.publicKey).pubkeyAdd(item)); + } + + void _getInputHash() { + final sortedOutpoints = >[]; + + for (final outpoint in vinOutpoints) { + sortedOutpoints.add(BytesUtils.concatBytes([ + BytesUtils.fromHexString(outpoint.txid).reversed.toList(), + BigintUtils.toBytes(BigInt.from(outpoint.index), length: 4, order: Endian.little) + ])); + } + + sortedOutpoints.sort(BytesUtils.compareBytes); + final lowestOutpoint = sortedOutpoints.first; + + inputHash = taggedHash( + BytesUtils.concatBytes([lowestOutpoint, A_sum!.toCompressedBytes()]), "BIP0352/Inputs"); + } + + Map> createOutputs( + List inputPrivKeyInfos, + List silentPaymentDestinations, + ) { + ECPrivate? a_sum; + + for (final info in inputPrivKeyInfos) { + var k = info.privkey; + final isTaproot = info.isTaproot; + + if (isTaproot) { + if (info.tweak) { + k = k.toTweakedTaprootKey(); + } + + final xOnlyPubkey = k.getPublic(); + final isOdd = xOnlyPubkey.publicKey.point.y % BigInt.two != BigInt.zero; + + if (isOdd) { + k = k.negate(); + } + } + + if (a_sum == null) { + a_sum = k; + } else { + a_sum = a_sum.tweakAdd(BigintUtils.fromBytes(k.toBytes())); + } + } + + A_sum = a_sum!.getPublic(); + _getInputHash(); + + Map>> silentPaymentGroups = {}; + + for (final silentPaymentDestination in silentPaymentDestinations) { + final B_scan = silentPaymentDestination.B_scan; + final scanPubkey = B_scan.toHex(); + + if (silentPaymentGroups.containsKey(scanPubkey)) { + // Current key already in silentPaymentGroups, simply add up the new destination + // with the already calculated ecdhSharedSecret + final group = silentPaymentGroups[scanPubkey]!; + final ecdhSharedSecret = group.keys.first; + final recipients = group.values.first; + + silentPaymentGroups[scanPubkey] = { + ecdhSharedSecret: [...recipients, silentPaymentDestination] + }; + } else { + final senderPartialSecret = a_sum.tweakMul(BigintUtils.fromBytes(inputHash!)).toBytes(); + final ecdhSharedSecret = + B_scan.tweakMul(BigintUtils.fromBytes(senderPartialSecret)).toHex(); + + silentPaymentGroups[scanPubkey] = { + ecdhSharedSecret: [silentPaymentDestination] + }; + } + } + + Map> result = {}; + for (final group in silentPaymentGroups.entries) { + final ecdhSharedSecret = group.value.keys.first; + final destinations = group.value.values.first; + + int k = 0; + for (final destination in destinations) { + final t_k = taggedHash( + BytesUtils.concatBytes([ + ECPublic.fromHex(ecdhSharedSecret).toCompressedBytes(), + BigintUtils.toBytes(BigInt.from(k), length: 4) + ]), + "BIP0352/SharedSecret"); + + final P_mn = destination.B_spend.tweakAdd(BigintUtils.fromBytes(t_k)); + final resOutput = + SilentPaymentOutput(P_mn.toTaprootAddress(tweak: false), destination.amount); + + if (result.containsKey(destination.toString())) { + result[destination.toString()]!.add(resOutput); + } else { + result[destination.toString()] = [resOutput]; + } + + k++; + } + } + + return result; + } + + Map scanOutputs( + ECPrivate b_scan, + ECPublic B_spend, + List outputsToCheck, { + Map? precomputedLabels, + }) { + final tweakDataForRecipient = receiverTweak != null + ? ECPublic.fromHex(receiverTweak!) + : A_sum!.tweakMul(BigintUtils.fromBytes(inputHash!)); + final ecdhSharedSecret = tweakDataForRecipient.tweakMul(b_scan.toBigInt()); + + final matches = {}; + var k = 0; + + do { + final t_k = taggedHash( + BytesUtils.concatBytes([ + ecdhSharedSecret.toCompressedBytes(), + BigintUtils.toBytes(BigInt.from(k), length: 4, order: Endian.big) + ]), + "BIP0352/SharedSecret"); + + final P_k = B_spend.tweakAdd(BigintUtils.fromBytes(t_k)); + final length = outputsToCheck.length; + + for (var i = 0; i < length; i++) { + final output = outputsToCheck[i].script.toBytes().sublist(2); + final outputPubkey = BytesUtils.toHexString(output); + final outputAmount = outputsToCheck[i].value.toInt(); + + if ((BytesUtils.compareBytes(output, P_k.toCompressedBytes().sublist(1)) == 0)) { + matches[outputPubkey] = SilentPaymentScanningOutput( + output: SilentPaymentOutput(P_k.toTaprootAddress(tweak: false), outputAmount), + tweak: BytesUtils.toHexString(t_k), + ); + outputsToCheck.removeAt(i); + k++; + break; + } + + if (precomputedLabels != null && precomputedLabels.isNotEmpty) { + var m_G_sub = ECPublic.fromBytes(output).pubkeyAdd(P_k.negate()); + var m_G = precomputedLabels[m_G_sub.toHex()]; + + if (m_G == null) { + m_G_sub = ECPublic.fromBytes(output).negate().pubkeyAdd(P_k.negate()); + m_G = precomputedLabels[m_G_sub.toHex()]; + } + + if (m_G != null) { + final P_km = P_k.tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(m_G))); + + matches[outputPubkey] = SilentPaymentScanningOutput( + output: SilentPaymentOutput(P_km.toTaprootAddress(tweak: false), outputAmount), + tweak: ECPrivate.fromBytes(t_k) + .tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(m_G))) + .toHex(), + label: m_G, + ); + + outputsToCheck.removeAt(i); + k++; + break; + } + } + + outputsToCheck.removeAt(i); + + if (i + 1 >= outputsToCheck.length) { + break; + } + } + } while (outputsToCheck.isNotEmpty); + + return matches; + } +} + +BitcoinScriptOutput getScriptFromOutput(String pubkey, int amount) { + return BitcoinScriptOutput( + script: Script(script: [BitcoinOpCodeConst.OP_1, pubkey]), value: BigInt.from(amount)); +} diff --git a/lib/src/bitcoin/silent_payments/silent_payments.dart b/lib/src/bitcoin/silent_payments/silent_payments.dart new file mode 100644 index 0000000..d839c6b --- /dev/null +++ b/lib/src/bitcoin/silent_payments/silent_payments.dart @@ -0,0 +1,23 @@ +// Library for Bitcoin Silent Payments handling in the bitcoin_base package. +// +// The library includes essential components such as: +// - Core address functionality. +// - encode/decode address support. +// - Utility functions for address manipulation. +// - Generate labeled addresses. +// - Scan transactions. +// - Generate payment outputs. +library bitcoin_base.silent_payments; + +import 'dart:typed_data'; + +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; +import 'package:bitcoin_base/src/provider/models/models.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:bitcoin_base/src/crypto/crypto.dart'; +import 'package:bitcoin_base/src/models/network.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; + +part 'address.dart'; +part 'payment.dart'; +part 'utils.dart'; diff --git a/lib/src/bitcoin/silent_payments/utils.dart b/lib/src/bitcoin/silent_payments/utils.dart new file mode 100644 index 0000000..1f68c07 --- /dev/null +++ b/lib/src/bitcoin/silent_payments/utils.dart @@ -0,0 +1,112 @@ +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; + +final NUMS_H = BigInt.parse("0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"); + +int deserCompactSize(ByteData f) { + final view = f.buffer; + int nbytes = view.lengthInBytes; + if (nbytes == 0) { + return 0; // end of stream + } + + int nit = f.getUint8(0); + if (nit == 253) { + nit = f.getUint16(1, Endian.little); + } else if (nit == 254) { + nit = f.getUint32(3, Endian.little); + } else if (nit == 255) { + nit = f.getUint64(7, Endian.little); + } + return nit; +} + +ByteData deserString(ByteData f) { + final nit = deserCompactSize(f); + int offset = 1; + return ByteData.sublistView(f.buffer.asUint8List().sublist(offset, nit + offset)); +} + +List deserStringVector(ByteData f) { + int offset = 0; + + final nit = deserCompactSize(f); + offset += 1; + + List result = []; + for (int i = 0; i < nit; i++) { + final t = deserString(ByteData.sublistView(f.buffer.asUint8List().sublist(offset))); + + result.add(t); + offset += t.lengthInBytes + 1; + } + return result; +} + +class VinInfo { + final Outpoint outpoint; + final List scriptSig; + final TxWitnessInput txinwitness; + final Script prevOutScript; + final ECPrivate? privkey; + + VinInfo({ + required this.outpoint, + required this.scriptSig, + required this.txinwitness, + required this.prevOutScript, + this.privkey, + }); +} + +ECPublic? getPubkeyFromInput(VinInfo vin) { + switch (vin.prevOutScript.getAddressType()) { + case P2pkhAddressType.p2pkh: + for (var i = vin.scriptSig.length; i > 0; i--) { + if (i - 33 >= 0) { + final pubkeyBytes = vin.scriptSig.sublist(i - 33, i); + final pubkeyHash = BytesUtils.toHexString(QuickCrypto.hash160(pubkeyBytes)); + if (pubkeyHash == + P2pkhAddress.fromScriptPubkey(script: vin.prevOutScript).addressProgram) { + return ECPublic.fromBytes(pubkeyBytes); + } + } + } + break; + case P2shAddressType.p2pkhInP2sh: + final redeemScript = vin.scriptSig.sublist(1); + if (Script.fromRaw(byteData: redeemScript).getAddressType() == SegwitAddresType.p2wpkh) { + return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); + } + break; + case SegwitAddresType.p2wpkh: + return ECPublic.fromBytes(vin.txinwitness.scriptWitness.stack.last.buffer.asUint8List()); + case SegwitAddresType.p2tr: + final witnessStack = vin.txinwitness.scriptWitness.stack; + if (witnessStack.isNotEmpty) { + if (witnessStack.length > 1 && witnessStack.last.buffer.asUint8List()[0] == 0x50) { + witnessStack.removeLast(); + } + + if (witnessStack.length > 1) { + final controlBlock = witnessStack.last.buffer.asUint8List(); + final internalKey = controlBlock.sublist(1, 33); + if (BytesUtils.compareBytes( + internalKey, BigintUtils.toBytes(NUMS_H, length: 32, order: Endian.big)) == + 0) { + return null; + } + } + return ECPublic.fromBytes(vin.prevOutScript.toBytes().sublist(2)); + } + break; + default: + return null; + } + + return null; +} + +List serUint32(int n) { + return BigintUtils.toBytes(BigInt.from(n), length: 4); +} diff --git a/lib/src/crypto/keypair/ec_private.dart b/lib/src/crypto/keypair/ec_private.dart index ee1b212..e8ace7a 100644 --- a/lib/src/crypto/keypair/ec_private.dart +++ b/lib/src/crypto/keypair/ec_private.dart @@ -1,5 +1,11 @@ +import 'dart:typed_data'; + 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. class ECPrivate { @@ -13,28 +19,23 @@ class ECPrivate { /// creates an object from raw 32 bytes factory ECPrivate.fromBytes(List prive) { - final key = Bip32PrivateKey.fromBytes(prive, Bip32KeyData(), - Bip32Const.mainNetKeyNetVersions, EllipticCurveTypes.secp256k1); + final key = Bip32PrivateKey.fromBytes( + prive, Bip32KeyData(), Bip32Const.mainNetKeyNetVersions, EllipticCurveTypes.secp256k1); return ECPrivate(key); } /// returns the corresponding ECPublic object - ECPublic getPublic() => - ECPublic.fromHex(BytesUtils.toHexString(prive.publicKey.compressed)); + ECPublic getPublic() => ECPublic.fromHex(BytesUtils.toHexString(prive.publicKey.compressed)); /// creates an object from a WIF of WIFC format (string) factory ECPrivate.fromWif(String wif, {required List? netVersion}) { - final decode = WifDecoder.decode(wif, - netVer: netVersion ?? BitcoinNetwork.mainnet.wifNetVer); + final decode = WifDecoder.decode(wif, netVer: netVersion ?? BitcoinNetwork.mainnet.wifNetVer); return ECPrivate.fromBytes(decode.item1); } /// returns as WIFC (compressed) or WIF format (string) String toWif({bool compressed = true, BitcoinNetwork? network}) { - List bytes = [ - ...(network ?? BitcoinNetwork.mainnet).wifNetVer, - ...toBytes() - ]; + List bytes = [...(network ?? BitcoinNetwork.mainnet).wifNetVer, ...toBytes()]; if (compressed) { bytes = [...bytes, 0x01]; } @@ -46,21 +47,44 @@ class ECPrivate { return prive.raw; } + BigInt toBigInt() { + return BigintUtils.fromBytes(prive.raw); + } + String toHex() { return BytesUtils.toHexString(prive.raw); } /// Returns a Bitcoin compact signature in hex - String signMessage(List message, - {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { - final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - final signature = btcSigner.signMessage(message, messagePrefix); - return BytesUtils.toHexString(signature); + String signMessage(List message, {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { + + final messageHash = + QuickCrypto.sha256Hash(BitcoinSignerUtils.magicMessage(message, messagePrefix)); + + final messageHashBytes = Uint8List.fromList(messageHash); + final privBytes = Uint8List.fromList(prive.raw); + final rs = ecc.sign(messageHashBytes, privBytes); + final rawSig = rs.toECSignature(); + + final pub = prive.publicKey; + final ECDomainParameters curve = ECCurve_secp256k1(); + final point = curve.curve.decodePoint(pub.point.toBytes()); + + final recId = SignUtils.findRecoveryId( + SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length), + rawSig, + Uint8List.fromList(pub.uncompressed), + ); + + final v = recId + 27 + (point!.isCompressed ? 4 : 0); + + final combined = Uint8List.fromList([v, ...rs]); + + return BytesUtils.toHexString(combined); } /// sign transaction digest and returns the signature. - String signInput(List txDigest, - {int sigHash = BitcoinOpCodeConst.SIGHASH_ALL}) { + String signInput(List txDigest, {int sigHash = BitcoinOpCodeConst.SIGHASH_ALL}) { final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); List signature = btcSigner.signTransaction(txDigest); signature = [...signature, sigHash]; @@ -79,20 +103,52 @@ 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."); - final tapScriptBytes = !tweak - ? [] - : tapScripts.map((e) => e.map((e) => e.toBytes()).toList()).toList(); + final tapScriptBytes = + !tweak ? [] : tapScripts.map((e) => e.map((e) => e.toBytes()).toList()).toList(); final btcSigner = BitcoinSigner.fromKeyBytes(toBytes()); - List signatur = btcSigner.signSchnorrTransaction(txDigest, - tapScripts: tapScriptBytes, tweak: tweak); + List signatur = + btcSigner.signSchnorrTransaction(txDigest, tapScripts: tapScriptBytes, tweak: tweak); if (sighash != BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL) { signatur = [...signatur, sighash]; } return BytesUtils.toHexString(signatur); } + ECPrivate toTweakedTaprootKey() { + final t = P2TRUtils.calculateTweek(getPublic().publicKey.point as ProjectiveECCPoint); + + return ECPrivate.fromBytes( + BitcoinSignerUtils.calculatePrivateTweek(toBytes(), BigintUtils.fromBytes(t))); + } + static ECPrivate random() { final secret = QuickCrypto.generateRandom(); return ECPrivate.fromBytes(secret); } + + ECPrivate tweakAdd(BigInt tweak) { + return ECPrivate.fromBytes(BigintUtils.toBytes( + (BigintUtils.fromBytes(prive.raw) + tweak) % Curves.generatorSecp256k1.order!, + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate tweakMul(BigInt tweak) { + return ECPrivate.fromBytes(BigintUtils.toBytes( + (BigintUtils.fromBytes(prive.raw) * tweak) % Curves.generatorSecp256k1.order!, + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate negate() { + // Negate the private key by subtracting from the order of the curve + return ECPrivate.fromBytes(BigintUtils.toBytes( + Curves.generatorSecp256k1.order! - BigintUtils.fromBytes(prive.raw), + length: getPublic().publicKey.point.curve.baselen, + )); + } + + ECPrivate clone() { + return ECPrivate.fromBytes(prive.raw); + } } diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index 03f85b2..18a5ec4 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -1,7 +1,9 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/bitcoin/script/script.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; +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'; class ECPublic { final Bip32PublicKey publicKey; @@ -9,16 +11,15 @@ class ECPublic { factory ECPublic.fromBip32(Bip32PublicKey publicKey) { if (publicKey.curveType != EllipticCurveTypes.secp256k1) { - throw const BitcoinBasePluginException( - "invalid public key curve for bitcoin"); + throw const BitcoinBasePluginException("invalid public key curve for bitcoin"); } return ECPublic._(publicKey); } /// Constructs an ECPublic key from a byte representation. factory ECPublic.fromBytes(List public) { - final publicKey = Bip32PublicKey.fromBytes(public, Bip32KeyData(), - Bip32Const.mainNetKeyNetVersions, EllipticCurveTypes.secp256k1); + final publicKey = Bip32PublicKey.fromBytes( + public, Bip32KeyData(), Bip32Const.mainNetKeyNetVersions, EllipticCurveTypes.secp256k1); return ECPublic._(publicKey); } @@ -50,17 +51,17 @@ class ECPublic { return BytesUtils.toHexString(QuickCrypto.hash160(bytes)); } - /// toAddress generates a P2PKH (Pay-to-Public-Key-Hash) address from the ECPublic key. + /// 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 toAddress({bool compressed = true}) { + P2pkhAddress toP2pkhAddress({bool compressed = true}) { final h16 = _toHash160(compressed: compressed); final toHex = BytesUtils.toHexString(h16); - return P2pkhAddress.fromHash160(addrHash: toHex); + return P2pkhAddress.fromHash160(h160: toHex); } - /// toSegwitAddress generates a P2WPKH (Pay-to-Witness-Public-Key-Hash) SegWit address + /// 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 toSegwitAddress({bool compressed = true}) { + P2wpkhAddress toP2wpkhAddress({bool compressed = true}) { final h16 = _toHash160(compressed: compressed); final toHex = BytesUtils.toHexString(h16); @@ -70,8 +71,7 @@ class ECPublic { /// 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}) { - final h = toHex(compressed: compressed); - return P2pkAddress(publicKey: h); + return P2pkAddress(publicKey: this); } /// toRedeemScript generates a redeem script from the ECPublic key. @@ -85,67 +85,56 @@ class ECPublic { /// 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 = toAddress(compressed: compressed); + final addr = toP2pkhAddress(compressed: compressed); final script = addr.toScriptPubKey(); if (useBCHP2sh32) { return P2shAddress.fromHash160( - addrHash: BytesUtils.toHexString( - QuickCrypto.sha256DoubleHash(script.toBytes())), + h160: BytesUtils.toHexString(QuickCrypto.sha256DoubleHash(script.toBytes())), type: P2shAddressType.p2pkhInP2sh32); } - return P2shAddress.fromScript( - script: script, type: P2shAddressType.p2pkhInP2sh); + return P2shAddress.fromRedeemScript(script: script, type: P2shAddressType.p2pkhInP2sh); } /// 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}) { + P2shAddress toP2pkInP2sh({bool compressed = true, bool useBCHP2sh32 = false}) { final script = toRedeemScript(compressed: compressed); if (useBCHP2sh32) { return P2shAddress.fromHash160( - addrHash: BytesUtils.toHexString( - QuickCrypto.sha256DoubleHash(script.toBytes())), + h160: BytesUtils.toHexString(QuickCrypto.sha256DoubleHash(script.toBytes())), type: P2shAddressType.p2pkInP2sh32); } - return P2shAddress.fromScript( - script: script, type: P2shAddressType.p2pkInP2sh); + return P2shAddress.fromRedeemScript(script: script, type: P2shAddressType.p2pkInP2sh); } /// ToTaprootAddress generates a P2TR(Taproot) address from the ECPublic key /// and an optional script. The 'script' parameter can be used to specify /// custom spending conditions. - P2trAddress toTaprootAddress({List>? scripts}) { - final pubKey = toTapRotHex(script: scripts); - return P2trAddress.fromProgram(program: pubKey); + P2trAddress toTaprootAddress({List>? scripts, bool tweak = true}) { + final pubKey = toTapRotHex(script: scripts, tweak: tweak); + return P2trAddress.fromProgram(program: pubKey, pubkey: ECPublic.fromHex(pubKey)); } /// 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 = toSegwitAddress(compressed: compressed); - return P2shAddress.fromScript( + final addr = toP2wpkhAddress(compressed: compressed); + 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 toP2wshScript({bool compressed = true}) { - return Script(script: [ - 'OP_1', - toHex(compressed: compressed), - "OP_1", - "OP_CHECKMULTISIG" - ]); + Script toP2wshRedeemScript({bool compressed = true}) { + return Script(script: ['OP_1', toHex(compressed: compressed), "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.fromScript( - script: toP2wshScript(compressed: compressed)); + return P2wshAddress.fromRedeemScript(script: toP2wshRedeemScript(compressed: compressed)); } /// toP2wshInP2sh generates a P2SH (Pay-to-Script-Hash) address @@ -153,10 +142,28 @@ class ECPublic { /// If 'compressed' is true, the key is in compressed format. P2shAddress toP2wshInP2sh({bool compressed = true}) { final p2sh = toP2wshAddress(compressed: compressed); - return P2shAddress.fromScript( + return P2shAddress.fromRedeemScript( script: p2sh.toScriptPubKey(), type: P2shAddressType.p2wshInP2sh); } + bool compareToAddress(BitcoinBaseAddress other, BasedUtxoNetwork network) { + late BitcoinBaseAddress address; + + if (other is P2pkAddress) { + address = toP2pkAddress(); + } else if (other is P2pkhAddress) { + address = toP2pkhAddress(); + } else if (other is P2wpkhAddress) { + address = toP2wpkhAddress(); + } else if (other is P2wshAddress) { + address = toP2wshAddress(); + } else if (other is P2trAddress) { + address = toTaprootAddress(); + } + + return address.toAddress(network) == other.toAddress(network); + } + /// toBytes returns the uncompressed byte representation of the ECPublic key. List toBytes({bool whitPrefix = true}) { if (!whitPrefix) { @@ -170,15 +177,20 @@ class ECPublic { return publicKey.compressed; } + EncodeType? getEncodeType() { + return publicKey.point.encodeType; + } + /// returns the x coordinate only as hex string after tweaking (needed for taproot) - String toTapRotHex({List>? script}) { - final scriptBytes = - script?.map((e) => e.map((e) => e.toBytes()).toList()).toList(); - final pubKey = P2TRUtils.tweakPublicKey( - publicKey.point as ProjectiveECCPoint, - script: scriptBytes); - return BytesUtils.toHexString( - BigintUtils.toBytes(pubKey.x, length: publicKey.point.curve.baselen)); + String toTapRotHex({List>? script, bool tweak = true}) { + var x = publicKey.point.x; + if (tweak) { + final scriptBytes = script?.map((e) => e.map((e) => e.toBytes()).toList()).toList(); + final pubKey = + P2TRUtils.tweakPublicKey(publicKey.point as ProjectiveECCPoint, script: scriptBytes); + x = pubKey.x; + } + return BytesUtils.toHexString(BigintUtils.toBytes(x, length: publicKey.point.curve.baselen)); } /// toXOnlyHex extracts and returns the x-coordinate (first 32 bytes) of the ECPublic key @@ -207,4 +219,38 @@ class ECPublic { return verifyKey.verifySchnorr(message, signature, tapleafScripts: tapleafScripts, isTweak: isTweak); } + + ECPublic tweakAdd(BigInt tweak) { + final point = publicKey.point as ProjectiveECCPoint; + // Compute the new public key after adding the tweak + final tweakedKey = point + (Curves.generatorSecp256k1 * tweak); + + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + // Perform the tweak multiplication + ECPublic tweakMul(BigInt tweak) { + final point = publicKey.point as ProjectiveECCPoint; + // Perform the tweak multiplication + final tweakedKey = point * tweak; + + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + ECPublic pubkeyAdd(ECPublic other) { + final tweakedKey = (publicKey.point as ProjectiveECCPoint) + other.publicKey.point; + return ECPublic.fromBytes(tweakedKey.toBytes()); + } + + ECPublic negate() { + // Negate the Y-coordinate by subtracting it from the field size (p). + final point = (publicKey.point as ProjectiveECCPoint); + final y = point.curve.p - point.y; + return ECPublic.fromBytes(BytesUtils.fromHexString( + "04${BytesUtils.toHexString(BigintUtils.toBytes(point.x, length: point.curve.baselen))}${BytesUtils.toHexString(BigintUtils.toBytes(y, length: point.curve.baselen))}")); + } + + ECPublic clone() { + return ECPublic.fromBytes(publicKey.uncompressed); + } } diff --git a/lib/src/crypto/keypair/sign_utils.dart b/lib/src/crypto/keypair/sign_utils.dart new file mode 100644 index 0000000..0b3f226 --- /dev/null +++ b/lib/src/crypto/keypair/sign_utils.dart @@ -0,0 +1,190 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/src/utils.dart'; +import 'package:pointycastle/ecc/ecc_fp.dart' as fp; + +final ECDomainParameters curve = ECCurve_secp256k1(); + +extension ECUtils on Uint8List { + ECSignature toECSignature() { + final sigLength = (this.length / 2).round(); + final r = BigInt.parse( + SignUtils.getHexString(this, offset: 0, length: sigLength), + radix: 16, + ); + final s = BigInt.parse( + SignUtils.getHexString(this, offset: sigLength, length: sigLength), + radix: 16, + ); + return ECSignature(r, s); + } + + bool isCompressedPoint() => curve.curve.decodePoint(this)!.isCompressed; +} + +class SignUtils { + /// Returns the recovery ID, a byte with value between 0 and 3, inclusive, that specifies which of 4 possible + /// curve points was used to sign a message. This value is also referred to as "v". + /// + /// @throws RuntimeException if no recovery ID can be found. + static int findRecoveryId(String hash, ECSignature sig, Uint8List pub) { + var recId = -1; + final Q = curve.curve.decodePoint(pub); + for (var i = 0; i < 4; i++) { + final k = recoverFromSignature(i, sig, hash); + if (k != null && k == Q) { + recId = i; + break; + } + } + if (recId == -1) { + throw Exception("Could not construct a recoverable key. This should never happen."); + } + return recId; + } + + static String getHexString( + List list, { + required int offset, + required int length, + }) { + final sublist = list.getRange(offset, offset + length); + return [for (var byte in sublist) byte.toRadixString(16).padLeft(2, '0').toUpperCase()].join(); + } + + ///

Given the components of a signature and a selector value, recover and return the public key + /// that generated the signature according to the algorithm in SEC1v2 section 4.1.6.

+ /// + ///

The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the correct one. Because + /// the key recovery operation yields multiple potential keys, the correct key must either be stored alongside the + /// signature, or you must be willing to try each recId in turn until you find one that outputs the key you are + /// expecting.

+ /// + ///

If this method returns null it means recovery was not possible and recId should be iterated.

+ /// + ///

Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, and if the + /// output is null OR a key that is not the one you expect, you try again with the next recId.

+ /// + /// @param recId Which possible key to recover. + /// @param sig the R and S components of the signature, wrapped. + /// @param message Hash of the data that was signed. + /// @param compressed Whether or not the original pubkey was compressed. + /// @return An ECKey containing only the public part, or null if recovery wasn't possible. + static ECPoint? recoverFromSignature(int recId, ECSignature sig, String message) { + // see https://www.secg.org/sec1-v2.pdf, section 4.1.6 + // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) + // 1.1 Let x = r + jn + final n = curve.n; // Curve order. + final i = BigInt.from(recId / 2); + final x = sig.r + (i * n); + // 1.2. Convert the integer x to an octet string X of length mlen using the conversion routine + // specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. + // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R using the + // conversion routine specified in Section 2.3.4. If this conversion routine outputs "invalid", then + // do another iteration of Step 1. + // + // More concisely, what these points mean is to use X as a compressed public key. + final prime = (curve.curve as fp.ECCurve).q!; + if (x.compareTo(prime) >= 0) { + // Cannot have point co-ordinates larger than this as everything takes place modulo Q. + return null; + } + // Compressed keys require you to know an extra bit of data about the y-coord as there are two possibilities. + // So it's encoded in the recId. + final R = _decompressKey(x, (recId & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers responsibility). + if (!(R * n)!.isInfinity) return null; + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + final e = BigInt.parse(message, radix: 16); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via iterating recId) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). In the above equation + // ** is point multiplication and + is point addition (the EC group operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For example the additive + // inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and -3 mod 11 = 8. + final eInv = (BigInt.zero - e) % n; + final rInv = sig.r.modInverse(n); + final srInv = (rInv * sig.s) % n; + final eInvrInv = (rInv * eInv) % n; + return sumOfTwoMultiplies(curve.G, eInvrInv, R, srInv)!; + } + + /// Decompress a compressed public key (x co-ord and low-bit of y-coord). + static ECPoint _decompressKey(BigInt xBN, bool yBit) { + final curveByteLength = ((curve.curve.fieldSize + 7) ~/ 8); + final compEnc = _x9IntegerToBytes(xBN, 1 + curveByteLength); + compEnc[0] = (yBit ? 0x03 : 0x02); + return curve.curve.decodePoint(compEnc)!; + } + +// Extracted from pointycastle/lib/ecc/ecc_fp.dart + static Uint8List _x9IntegerToBytes(BigInt? s, int qLength) { + var bytes = Uint8List.fromList(encodeBigInt(s)); + + if (qLength < bytes.length) { + return bytes.sublist(bytes.length - qLength); + } else if (qLength > bytes.length) { + return Uint8List(qLength)..setAll(qLength - bytes.length, bytes); + } + + return bytes; + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static ECPoint? sumOfTwoMultiplies(ECPoint P, BigInt a, ECPoint Q, BigInt b) { + var c = P.curve; + + if (c != Q.curve) { + throw ArgumentError('P and Q must be on same curve'); + } + + // Point multiplication for Koblitz curves (using WTNAF) beats Shamir's trick + // TODO: uncomment this when F2m available + /* + if( c is ECCurve.F2m ) { + ECCurve.F2m f2mCurve = (ECCurve.F2m)c; + if( f2mCurve.isKoblitz() ) { + return P.multiply(a).add(Q.multiply(b)); + } + } + */ + + return _implShamirsTrick(P, a, Q, b); + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static ECPoint? _implShamirsTrick(ECPoint P, BigInt k, ECPoint Q, BigInt l) { + var m = max(k.bitLength, l.bitLength); + + var Z = P + Q; + var R = P.curve.infinity; + + for (var i = m - 1; i >= 0; --i) { + R = R!.twice(); + + if (_testBit(k, i)) { + if (_testBit(l, i)) { + R = R! + Z; + } else { + R = R! + P; + } + } else { + if (_testBit(l, i)) { + R = R! + Q; + } + } + } + + return R; + } + + // Extracted from pointycastle/lib/signers/ecdsa_signer.dart + static bool _testBit(BigInt i, int n) => (i & (BigInt.one << n)) != BigInt.zero; +} diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index 89e32b3..adeaafc 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -27,9 +27,7 @@ abstract class BasedUtxoNetwork implements Enumerate { @override operator ==(other) { if (identical(other, this)) return true; - return other is BasedUtxoNetwork && - other.runtimeType == runtimeType && - value == other.value; + return other is BasedUtxoNetwork && other.runtimeType == runtimeType && value == other.value; } @override @@ -103,8 +101,7 @@ class BitcoinSVNetwork implements BasedUtxoNetwork { bool get isMainnet => this == BitcoinSVNetwork.mainnet; @override - List get supportedAddress => - [P2pkhAddressType.p2pkh, PubKeyAddressType.p2pk]; + List get supportedAddress => [P2pkhAddressType.p2pkh, PubKeyAddressType.p2pk]; @override List get coins { @@ -232,6 +229,7 @@ class LitecoinNetwork implements BasedUtxoNetwork { SegwitAddresType.p2wpkh, PubKeyAddressType.p2pk, SegwitAddresType.p2wsh, + SegwitAddresType.mweb, P2shAddressType.p2wshInP2sh, P2shAddressType.p2wpkhInP2sh, P2shAddressType.p2pkhInP2sh, @@ -243,23 +241,17 @@ class LitecoinNetwork implements BasedUtxoNetwork { if (isMainnet) { return [Bip44Coins.litecoin, Bip49Coins.litecoin, Bip84Coins.litecoin]; } - return [ - Bip44Coins.litecoinTestnet, - Bip49Coins.litecoinTestnet, - Bip84Coins.litecoinTestnet - ]; + return [Bip44Coins.litecoinTestnet, Bip49Coins.litecoinTestnet, Bip84Coins.litecoinTestnet]; } } /// 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 @@ -282,8 +274,8 @@ 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"); + String get p2wpkhHrp => + throw const BitcoinBasePluginException("DashNetwork network does not support P2WPKH/P2WSH"); /// Checks if the current network is the mainnet. @override @@ -409,8 +401,8 @@ class BitcoinCashNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses /// from the associated `CoinConf`. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( - "network does not support p2wpkh HRP"); + String get p2wpkhHrp => + throw const BitcoinBasePluginException("network does not support p2wpkh HRP"); String get networkHRP => conf.params.p2pkhStdHrp!; @@ -443,8 +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 diff --git a/lib/src/provider/api_provider.dart b/lib/src/provider/api_provider.dart index c043b56..98a8ce7 100644 --- a/lib/src/provider/api_provider.dart +++ b/lib/src/provider/api_provider.dart @@ -1,5 +1,4 @@ export 'models/models.dart'; -export 'transaction_builder/builder.dart'; -export 'service/services.dart'; -export 'api_provider/providers.dart'; +export 'providers/electrum.dart'; export 'electrum_methods/methods.dart'; +export 'service/services.dart'; diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart deleted file mode 100644 index 1dc5c81..0000000 --- a/lib/src/provider/api_provider/api_provider.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'dart:convert'; -import 'package:bitcoin_base/src/provider/models/models.dart'; -import 'package:bitcoin_base/src/provider/service/http/http_service.dart'; -import 'package:bitcoin_base/src/models/network.dart'; -import 'package:blockchain_utils/utils/string/string.dart'; - -class ApiProvider { - ApiProvider( - {required this.api, Map? header, required this.service}) - : _header = header ?? {"Content-Type": "application/json"}; - factory ApiProvider.fromMempool(BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.mempool(network); - return ApiProvider(api: api, header: header, service: service); - } - factory ApiProvider.fromBlocCypher( - BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.fromBlockCypher(network); - return ApiProvider(api: api, header: header, service: service); - } - final APIConfig api; - final ApiService service; - - final Map _header; - - Future _getRequest(String url) async { - final response = await service.get(url); - return response; - } - - Future _postReqiest(String url, Object? data) async { - final response = await service.post(url, body: data, headers: _header); - return response; - } - - Future> testmempool(List params) async { - final Map data = { - "jsonrpc": "2.0", - "method": "testmempoolaccept", - "id": DateTime.now().millisecondsSinceEpoch.toString(), - "params": params - }; - final response = await _postReqiest>( - "https://btc.getblock.io/786c97b8-f53f-427b-80f7-9af7bd5bdb84/testnet/", - json.encode(data)); - return response; - } - - Future> getAccountUtxo(UtxoAddressDetails owner, - {String Function(String)? tokenize}) async { - final apiUrl = api.getUtxoUrl(owner.address.toAddress(api.network)); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - final utxos = - (response as List).map((e) => MempolUtxo.fromJson(e)).toList(); - return utxos.toUtxoWithOwnerList(owner); - default: - final blockCypherUtxo = BlockCypherUtxo.fromJson(response); - return blockCypherUtxo.toUtxoWithOwner(owner); - } - } - - Future sendRawTransaction(String txDigest, - {String Function(String)? tokenize}) async { - final apiUrl = api.sendTransaction; - final url = tokenize?.call(apiUrl) ?? apiUrl; - - switch (api.apiType) { - case APIType.mempool: - final response = await _postReqiest(url, txDigest); - return response; - default: - final Map digestData = {"tx": txDigest}; - final response = await _postReqiest>( - url, json.encode(digestData)); - BlockCypherTransaction? tr; - if (response["tx"] != null) { - tr = BlockCypherTransaction.fromJson(response["tx"]); - } - - tr ??= BlockCypherTransaction.fromJson(response); - return tr.hash; - } - } - - Future getNetworkFeeRate( - {String Function(String)? tokenize}) async { - final apiUrl = api.getFeeApiUrl(); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest>(url); - switch (api.apiType) { - case APIType.mempool: - return BitcoinFeeRate.fromMempool(response); - default: - return BitcoinFeeRate.fromBlockCypher(response); - } - } - - Future getTransaction(String transactionId, - {String Function(String)? tokenize}) async { - final apiUrl = api.getTransactionUrl(transactionId); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest>(url); - switch (api.apiType) { - case APIType.mempool: - return MempoolTransaction.fromJson(response) as T; - default: - return BlockCypherTransaction.fromJson(response) as T; - } - } - - Future> getAccountTransactions(String address, - {String Function(String)? tokenize}) async { - final apiUrl = api.getTransactionsUrl(address); - final url = tokenize?.call(apiUrl) ?? apiUrl; - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - final transactions = (response as List) - .map((e) => MempoolTransaction.fromJson(e) as T) - .toList(); - return transactions; - default: - if (response is Map) { - if (response.containsKey("txs")) { - final transactions = (response["txs"] as List) - .map((e) => BlockCypherTransaction.fromJson(e) as T) - .toList(); - return transactions; - } - return []; - } - final transactions = (response as List) - .map((e) => BlockCypherTransaction.fromJson(e) as T) - .toList(); - return transactions; - } - } - - Future getBlockHeight(int height) async { - final url = api.getBlockHeight(height); - final response = await _getRequest(url); - switch (api.apiType) { - case APIType.mempool: - return response; - default: - final toJson = StringUtils.toJson>(response); - return toJson["hash"]; - } - } - - Future genesis() async { - return getBlockHeight(0); - } -} diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart deleted file mode 100644 index 9cd90ec..0000000 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; -import 'dart:async'; - -import 'package:blockchain_utils/exception/exceptions.dart'; - -class ElectrumApiProvider { - final BitcoinBaseElectrumRPCService rpc; - ElectrumApiProvider(this.rpc); - int _id = 0; - - /// Sends a request to the Electrum server using the specified [request] parameter. - /// - /// The [timeout] parameter, if provided, sets the maximum duration for the request. - Future request(ElectrumRequest request, - [Duration? timeout]) async { - final id = ++_id; - final params = request.toRequest(id); - final data = await rpc.call(params, timeout); - return request.onResonse(_findResult(data, params)); - } - - dynamic _findResult( - Map data, ElectrumRequestDetails request) { - if (data["error"] != null) { - final code = - int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; - final message = data["error"]?['message'] ?? ""; - throw RPCError( - errorCode: code, - message: message, - data: data["error"]?["data"], - request: data["request"] ?? request.params, - ); - } - - return data["result"]; - } -} diff --git a/lib/src/provider/api_provider/providers.dart b/lib/src/provider/api_provider/providers.dart deleted file mode 100644 index df92a29..0000000 --- a/lib/src/provider/api_provider/providers.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'api_provider.dart'; -export 'electrum_api_provider.dart'; diff --git a/lib/src/provider/electrum_methods/methods.dart b/lib/src/provider/electrum_methods/methods.dart index 052a047..39733df 100644 --- a/lib/src/provider/electrum_methods/methods.dart +++ b/lib/src/provider/electrum_methods/methods.dart @@ -27,3 +27,4 @@ export 'methods/relay_fee.dart'; export 'methods/scripthash_unsubscribe.dart'; export 'methods/server_peer_subscribe.dart'; export 'methods/status.dart'; +export 'methods/tweaks_subscribe.dart'; diff --git a/lib/src/provider/electrum_methods/methods/add_peer.dart b/lib/src/provider/electrum_methods/methods/add_peer.dart index fb23bba..c5f69da 100644 --- a/lib/src/provider/electrum_methods/methods/add_peer.dart +++ b/lib/src/provider/electrum_methods/methods/add_peer.dart @@ -1,10 +1,10 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// A newly-started server uses this call to get itself into other servers’ peers lists. /// It should not be used by wallet clients. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumAddPeer extends ElectrumRequest { - ElectrumAddPeer({required this.features}); +class ElectrumRequestAddPeer extends ElectrumRequest { + ElectrumRequestAddPeer({required this.features}); /// The same information that a call to the sender’s server.features() RPC call would return. final Map features; @@ -14,14 +14,14 @@ class ElectrumAddPeer extends ElectrumRequest { String get method => ElectrumRequestMethods.serverAddPeer.method; @override - List toJson() { + List toParams() { return [features]; } /// A boolean indicating whether the request was tentatively accepted /// The requesting server will appear in server.peers.subscribe() when further sanity checks complete successfully. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/block_headers.dart b/lib/src/provider/electrum_methods/methods/block_headers.dart index ef75150..ceff2fb 100644 --- a/lib/src/provider/electrum_methods/methods/block_headers.dart +++ b/lib/src/provider/electrum_methods/methods/block_headers.dart @@ -1,10 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Return a concatenated chunk of block headers from the main chain. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeaders +class ElectrumRequestBlockHeaders extends ElectrumRequest, Map> { - ElectrumBlockHeaders( + ElectrumRequestBlockHeaders( {required this.startHeight, required this.count, required this.cpHeight}); /// The height of the first header requested, a non-negative integer. @@ -21,13 +22,13 @@ class ElectrumBlockHeaders String get method => ElectrumRequestMethods.blockHeaders.method; @override - List toJson() { + List toParams() { return [startHeight, count, cpHeight]; } /// A dictionary @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/broad_cast.dart b/lib/src/provider/electrum_methods/methods/broad_cast.dart index fef43d0..b89cce1 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -1,26 +1,25 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Broadcast a transaction to the network. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBroadCastTransaction extends ElectrumRequest { - ElectrumBroadCastTransaction({required this.transactionRaw}); +class ElectrumRequestBroadCastTransaction extends ElectrumRequest { + ElectrumRequestBroadCastTransaction({required this.transactionRaw}); /// The raw transaction as a hexadecimal string. final String transactionRaw; /// blockchain.transaction.broadcast @override - String get method => ElectrumRequestMethods.broadCast.method; + String get method => ElectrumRequestMethods.broadcast.method; @override - List toJson() { + List toParams() { return [transactionRaw]; } /// The transaction hash as a hexadecimal string. @override - String onResonse(result) { + String? onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/donate_address.dart b/lib/src/provider/electrum_methods/methods/donate_address.dart index 4f59989..1195514 100644 --- a/lib/src/provider/electrum_methods/methods/donate_address.dart +++ b/lib/src/provider/electrum_methods/methods/donate_address.dart @@ -1,19 +1,19 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a server donation address. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumDonationAddress extends ElectrumRequest { +class ElectrumRequestDonationAddress extends ElectrumRequest { /// server.donation_address @override String get method => ElectrumRequestMethods.serverDontionAddress.method; @override - List toJson() { + List toParams() { return []; } @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index 1a1f858..6924d3c 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -2,27 +2,26 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumVersion extends ElectrumRequest, List> { - ElectrumVersion({required this.clientName, required this.protocolVersion}); +class ElectrumRequestVersion extends ElectrumRequest, List> { + ElectrumRequestVersion({required this.clientName, required this.protocolVersion}); /// A string identifying the connecting client software. final String clientName; - /// An array [protocol_min, protocol_max], each of which is a string. - final List protocolVersion; + final String protocolVersion; /// blockchain.version @override String get method => ElectrumRequestMethods.version.method; @override - List toJson() { + List toParams() { return [clientName, protocolVersion]; } /// identifying the server and the protocol version that will be used for future communication. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/estimate_fee.dart b/lib/src/provider/electrum_methods/methods/estimate_fee.dart index 9e3b2f5..103676a 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -3,8 +3,8 @@ import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumEstimateFee extends ElectrumRequest { - ElectrumEstimateFee({this.numberOfBlock = 2}); +class ElectrumRequestEstimateFee extends ElectrumRequest { + ElectrumRequestEstimateFee({this.numberOfBlock = 2}); /// The number of blocks to target for confirmation. final int numberOfBlock; @@ -14,13 +14,15 @@ class ElectrumEstimateFee extends ElectrumRequest { String get method => ElectrumRequestMethods.estimateFee.method; @override - List toJson() { + List toParams() { return [numberOfBlock]; } /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResonse(result) { - return BtcUtils.toSatoshi(result.toString()).abs(); + BigInt? onResponse(result) { + final fee = BtcUtils.toSatoshi(result.toString()); + if (fee.isNegative) return null; + return fee; } } diff --git a/lib/src/provider/electrum_methods/methods/get_balance.dart b/lib/src/provider/electrum_methods/methods/get_balance.dart index c9a49e0..c75504e 100644 --- a/lib/src/provider/electrum_methods/methods/get_balance.dart +++ b/lib/src/provider/electrum_methods/methods/get_balance.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the confirmed and unconfirmed balances of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetScriptHashBalance +class ElectrumRequestGetScriptHashBalance extends ElectrumRequest, Map> { - ElectrumGetScriptHashBalance({required this.scriptHash}); + ElectrumRequestGetScriptHashBalance({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,14 +14,49 @@ class ElectrumGetScriptHashBalance String get method => ElectrumRequestMethods.getBalance.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// A dictionary with keys confirmed and unconfirmed. /// The value of each is the appropriate balance in minimum coin units (satoshis). @override - Map onResonse(Map result) { + Map onResponse(Map result) { return result; } } + +class ElectrumBatchRequestGetScriptHashBalance + extends ElectrumBatchRequest, Map> { + ElectrumBatchRequestGetScriptHashBalance({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.getBalance.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: Map.from(data['result']), + ); + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart index 8c89c23..4371d93 100644 --- a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart +++ b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart @@ -1,16 +1,14 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a histogram of the fee rates paid by transactions in the memory pool, weighted by transaction size. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetFeeHistogram - extends ElectrumRequest>, List> { +class ElectrumRequestGetFeeHistogram extends ElectrumRequest>, List> { /// mempool.get_fee_histogram @override String get method => ElectrumRequestMethods.getFeeHistogram.method; @override - List toJson() { + List toParams() { return []; } @@ -19,7 +17,7 @@ class ElectrumGetFeeHistogram /// fee uses sat/vbyte as unit, and must be a non-negative integer or float. /// vsize uses vbyte as unit, and must be a non-negative integer. @override - List> onResonse(result) { - return result.map((e) => List.from(e)).toList(); + List> onResponse(result) { + return result.map((e) => (e as List).map((e) => (e as num).toDouble()).toList()).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_history.dart b/lib/src/provider/electrum_methods/methods/get_history.dart index 9f8efa2..bf1ea72 100644 --- a/lib/src/provider/electrum_methods/methods/get_history.dart +++ b/lib/src/provider/electrum_methods/methods/get_history.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the confirmed and unconfirmed history of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetHistory +class ElectrumRequestScriptHashGetHistory extends ElectrumRequest>, List> { - ElectrumScriptHashGetHistory({required this.scriptHash}); + ElectrumRequestScriptHashGetHistory({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,7 +14,7 @@ class ElectrumScriptHashGetHistory String get method => ElectrumRequestMethods.getHistory.method; @override - List toJson() { + List toParams() { return [scriptHash]; } @@ -23,7 +22,43 @@ class ElectrumScriptHashGetHistory /// with the output of blockchain.scripthash.get_mempool() appended to the list. /// Each confirmed transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } + +class ElectrumBatchRequestScriptHashGetHistory + extends ElectrumBatchRequest>, Map> { + ElectrumBatchRequestScriptHashGetHistory({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.getHistory.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult>> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + final result = data['result'] as List; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: result.map((e) => Map.from(e)).toList(), + ); + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_mempool.dart b/lib/src/provider/electrum_methods/methods/get_mempool.dart index fcd33a2..d416626 100644 --- a/lib/src/provider/electrum_methods/methods/get_mempool.dart +++ b/lib/src/provider/electrum_methods/methods/get_mempool.dart @@ -1,11 +1,10 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the unconfirmed transactions of a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashGetMempool +class ElectrumRequestScriptHashGetMempool extends ElectrumRequest>, List> { - ElectrumScriptHashGetMempool({required this.scriptHash}); + ElectrumRequestScriptHashGetMempool({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -15,13 +14,13 @@ class ElectrumScriptHashGetMempool String get method => ElectrumRequestMethods.getMempool.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// A list of mempool transactions in arbitrary order. Each mempool transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_merkle.dart b/lib/src/provider/electrum_methods/methods/get_merkle.dart index a39cd8a..6f84e1b 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -1,11 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the merkle branch to a confirmed transaction given its hash and height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetMerkle - extends ElectrumRequest, Map> { - ElectrumGetMerkle({required this.transactionHash, required this.height}); +class ElectrumRequestGetMerkle extends ElectrumRequest, Map> { + ElectrumRequestGetMerkle({required this.transactionHash, required this.height}); /// The transaction hash as a hexadecimal string. final String transactionHash; @@ -18,12 +16,12 @@ class ElectrumGetMerkle String get method => ElectrumRequestMethods.getMerkle.method; @override - List toJson() { + List toParams() { return [transactionHash, height]; } @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_transaction.dart b/lib/src/provider/electrum_methods/methods/get_transaction.dart index f18dd84..efaa4b9 100644 --- a/lib/src/provider/electrum_methods/methods/get_transaction.dart +++ b/lib/src/provider/electrum_methods/methods/get_transaction.dart @@ -1,24 +1,51 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; /// Return a raw transaction. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetTransaction extends ElectrumRequest { - ElectrumGetTransaction({required this.transactionHash, this.verbose = false}); +class ElectrumRequestGetTransactionHex extends ElectrumRequest { + ElectrumRequestGetTransactionHex({required this.transactionHash}); /// The transaction hash as a hexadecimal string. final String transactionHash; - /// Whether a verbose coin-specific response is required. - final bool verbose; + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, false]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + String onResponse(result) { + return result; + } +} + +class ElectrumBatchRequestGetTransactionHex + extends ElectrumBatchRequest> { + ElectrumBatchRequestGetTransactionHex({required this.transactionHashes}); + + /// The transaction hash as a hexadecimal string. + final List transactionHashes; /// blockchain.transaction.get @override String get method => ElectrumRequestMethods.getTransaction.method; @override - List toJson() { - return [transactionHash, verbose]; + List toParams() { + return [ + ...transactionHashes.map((e) => [e]) + ]; } /// If verbose is false: @@ -27,7 +54,105 @@ class ElectrumGetTransaction extends ElectrumRequest { /// If verbose is true: /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. @override - dynamic onResonse(result) { + ElectrumBatchRequestResult onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: data['result'] as String, + ); + } +} + +class ElectrumRequestGetTransactionVerbose + extends ElectrumRequest, Map> { + ElectrumRequestGetTransactionVerbose({required this.transactionHash}); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, true]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + Map onResponse(result) { return result; } } + +class ElectrumBatchRequestGetTransactionVerbose + extends ElectrumBatchRequest, Map> { + ElectrumBatchRequestGetTransactionVerbose({required this.transactionHashes}); + + /// The transaction hash as a hexadecimal string. + final List transactionHashes; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [ + ...transactionHashes.map((e) => [e, true]) + ]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + ElectrumBatchRequestResult> onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + return ElectrumBatchRequestResult( + request: request, + id: id, + result: data['result'] as Map, + ); + } +} + +/// Return a raw transaction. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumRequestGetRawTransaction extends ElectrumRequest { + ElectrumRequestGetRawTransaction(this.transactionHash); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; + + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toParams() { + return [transactionHash, false]; + } + + @override + BtcTransaction onResponse(String result) { + final txBytes = BytesUtils.fromHexString(result); + final tx = BtcTransaction.fromRaw(result); + assert(BytesUtils.bytesEqual(tx.toBytes(), txBytes), result); + return tx; + } +} diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index 2a957d1..ffcc08e 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -1,13 +1,11 @@ import 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return an ordered list of UTXOs sent to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashListUnspent +class ElectrumRequestScriptHashListUnspent extends ElectrumRequest, List> { - ElectrumScriptHashListUnspent( - {required this.scriptHash, this.includeTokens = false}); + ElectrumRequestScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -20,8 +18,8 @@ class ElectrumScriptHashListUnspent String get method => ElectrumRequestMethods.listunspent.method; @override - List toJson() { - return [scriptHash, if (includeTokens) "include_tokens"]; + List toParams() { + return [scriptHash, if (includeTokens) 'include_tokens']; } /// A list of unspent outputs in blockchain order. @@ -29,9 +27,8 @@ class ElectrumScriptHashListUnspent /// Mempool transactions paying to the address are included at the end of the list in an undefined order. /// Any output that is spent in the mempool does not appear. @override - List onResonse(result) { - final List utxos = - result.map((e) => ElectrumUtxo.fromJson(e)).toList(); + List onResponse(result) { + final utxos = result.map((e) => ElectrumUtxo.fromJson(e)).toList(); return utxos; } } diff --git a/lib/src/provider/electrum_methods/methods/get_value_proof.dart b/lib/src/provider/electrum_methods/methods/get_value_proof.dart index bebe814..5e0a8ab 100644 --- a/lib/src/provider/electrum_methods/methods/get_value_proof.dart +++ b/lib/src/provider/electrum_methods/methods/get_value_proof.dart @@ -2,9 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns a name resolution proof, suitable for low-latency (single round-trip) resolution. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetValueProof - extends ElectrumRequest, dynamic> { - ElectrumGetValueProof({required this.scriptHash, required this.cpHeight}); +class ElectrumRequestGetValueProof extends ElectrumRequest, dynamic> { + ElectrumRequestGetValueProof({required this.scriptHash, required this.cpHeight}); /// Script hash of the name being resolved. final String scriptHash; @@ -17,7 +16,7 @@ class ElectrumGetValueProof String get method => ElectrumRequestMethods.getValueProof.method; @override - List toJson() { + List toParams() { return [scriptHash, cpHeight]; } @@ -25,7 +24,7 @@ class ElectrumGetValueProof /// from the most recent update back to either the registration transaction or a /// checkpointed transaction (whichever is later). @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/header.dart b/lib/src/provider/electrum_methods/methods/header.dart index 97120b5..3000c3f 100644 --- a/lib/src/provider/electrum_methods/methods/header.dart +++ b/lib/src/provider/electrum_methods/methods/header.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return the block header at the given height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeader extends ElectrumRequest { - ElectrumBlockHeader({required this.startHeight, required this.cpHeight}); +class ElectrumRequestBlockHeader extends ElectrumRequest { + ElectrumRequestBlockHeader({required this.startHeight, required this.cpHeight}); final int startHeight; final int cpHeight; @@ -12,7 +12,7 @@ class ElectrumBlockHeader extends ElectrumRequest { String get method => ElectrumRequestMethods.blockHeader.method; @override - List toJson() { + List toParams() { return [startHeight, cpHeight]; } @@ -22,7 +22,7 @@ class ElectrumBlockHeader extends ElectrumRequest { /// This provides a proof that the given header is present in the blockchain; /// presumably the client has the merkle root hard-coded as a checkpoint. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart index 651c695..50c1760 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -1,21 +1,36 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +class ElectrumHeaderResponse { + final String hex; + final int height; + + ElectrumHeaderResponse(this.hex, this.height); + + factory ElectrumHeaderResponse.fromJson(Map json) { + return ElectrumHeaderResponse(json['hex'], json['height']); + } + + Map toJson() { + return {'hex': hex, 'height': height}; + } +} + /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumHeaderSubscribe - extends ElectrumRequest, Map> { +class ElectrumRequestHeaderSubscribe + extends ElectrumRequest> { /// blockchain.headers.subscribe @override String get method => ElectrumRequestMethods.headersSubscribe.method; @override - List toJson() { + List toParams() { return []; } /// The header of the current block chain tip. @override - Map onResonse(result) { - return result; + ElectrumHeaderResponse onResponse(result) { + return ElectrumHeaderResponse.fromJson(result); } } diff --git a/lib/src/provider/electrum_methods/methods/id_from_pos.dart b/lib/src/provider/electrum_methods/methods/id_from_pos.dart index 865f0b2..5739a48 100644 --- a/lib/src/provider/electrum_methods/methods/id_from_pos.dart +++ b/lib/src/provider/electrum_methods/methods/id_from_pos.dart @@ -1,11 +1,9 @@ -import 'package:bitcoin_base/src/provider/service/electrum/methods.dart'; -import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a transaction hash and optionally a merkle proof, given a block height and a position in the block. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumIdFromPos extends ElectrumRequest { - ElectrumIdFromPos( - {required this.height, required this.txPos, this.merkle = false}); +class ElectrumRequestIdFromPos extends ElectrumRequest { + ElectrumRequestIdFromPos({required this.height, required this.txPos, this.merkle = false}); /// The main chain block height, a non-negative integer. final int height; @@ -21,13 +19,13 @@ class ElectrumIdFromPos extends ElectrumRequest { String get method => ElectrumRequestMethods.idFromPos.method; @override - List toJson() { + List toParams() { return [height, txPos, merkle]; } /// If merkle is false, the transaction hash as a hexadecimal string. If true, a dictionary @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart index f3169d3..00004da 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart @@ -4,23 +4,22 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Whenever a masternode comes online or a client is syncing, /// they will send this message which describes the masternode entry and how to validate messages from it. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeAnnounceBroadcast extends ElectrumRequest { - ElectrumMasternodeAnnounceBroadcast({required this.signmnb}); +class ElectrumRequestMasternodeAnnounceBroadcast extends ElectrumRequest { + ElectrumRequestMasternodeAnnounceBroadcast({required this.signmnb}); final String signmnb; /// masternode.announce.broadcast @override - String get method => - ElectrumRequestMethods.masternodeAnnounceBroadcast.method; + String get method => ElectrumRequestMethods.masternodeAnnounceBroadcast.method; @override - List toJson() { + List toParams() { return [signmnb]; } /// true if the message was broadcasted successfully otherwise false. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_list.dart b/lib/src/provider/electrum_methods/methods/masternode_list.dart index 437dc3b..62b2ac7 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_list.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_list.dart @@ -2,9 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns the list of masternodes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeList - extends ElectrumRequest, List> { - ElectrumMasternodeList({required this.payees}); +class ElectrumRequestMasternodeList extends ElectrumRequest, List> { + ElectrumRequestMasternodeList({required this.payees}); /// An array of masternode payee addresses. final List payees; @@ -14,13 +13,13 @@ class ElectrumMasternodeList String get method => ElectrumRequestMethods.masternodeList.method; @override - List toJson() { + List toParams() { return [payees]; } /// An array with the masternodes information. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart index 2734415..f5bebd2 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns the status of masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeSubscribe extends ElectrumRequest { - ElectrumMasternodeSubscribe({required this.collateral}); +class ElectrumRequestMasternodeSubscribe extends ElectrumRequest { + ElectrumRequestMasternodeSubscribe({required this.collateral}); /// The txId and the index of the collateral. Example ("8c59133e714797650cf69043d05e409bbf45670eed7c4e4a386e52c46f1b5e24-0") final String collateral; @@ -13,7 +13,7 @@ class ElectrumMasternodeSubscribe extends ElectrumRequest { String get method => ElectrumRequestMethods.masternodeSubscribe.method; @override - List toJson() { + List toParams() { return [collateral]; } @@ -22,7 +22,7 @@ class ElectrumMasternodeSubscribe extends ElectrumRequest { /// the internet connection, the offline time and even the collateral /// amount, so this subscription notice these changes to the user. @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/ping.dart b/lib/src/provider/electrum_methods/methods/ping.dart index 4d41704..f767b31 100644 --- a/lib/src/provider/electrum_methods/methods/ping.dart +++ b/lib/src/provider/electrum_methods/methods/ping.dart @@ -2,17 +2,17 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Ping the server to ensure it is responding, and to keep the session alive. The server may disconnect clients that have sent no requests for roughly 10 minutes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumPing extends ElectrumRequest { +class ElectrumRequestPing extends ElectrumRequest { @override String get method => ElectrumRequestMethods.ping.method; @override - List toJson() { + List toParams() { return []; } @override - dynamic onResonse(result) { + dynamic onResponse(result) { return null; } } diff --git a/lib/src/provider/electrum_methods/methods/protx_diff.dart b/lib/src/provider/electrum_methods/methods/protx_diff.dart index cca5d7d..c7d99d5 100644 --- a/lib/src/provider/electrum_methods/methods/protx_diff.dart +++ b/lib/src/provider/electrum_methods/methods/protx_diff.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + /// Returns a diff between two deterministic masternode lists. The result also contains proof data.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXDiff extends ElectrumRequest, dynamic> { - ElectrumProtXDiff({required this.baseHeight, required this.height}); +class ElectrumRequestProtXDiff + extends ElectrumRequest, dynamic> { + ElectrumRequestProtXDiff({required this.baseHeight, required this.height}); /// The starting block height final int baseHeight; @@ -16,13 +18,13 @@ class ElectrumProtXDiff extends ElectrumRequest, dynamic> { String get method => ElectrumRequestMethods.protxDiff.method; @override - List toJson() { + List toParams() { return [baseHeight, height]; } /// A dictionary with deterministic masternode lists diff plus proof data. @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/protx_info.dart b/lib/src/provider/electrum_methods/methods/protx_info.dart index 7685699..a5dcac2 100644 --- a/lib/src/provider/electrum_methods/methods/protx_info.dart +++ b/lib/src/provider/electrum_methods/methods/protx_info.dart @@ -2,8 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns detailed information about a deterministic masternode. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumProtXInfo extends ElectrumRequest, dynamic> { - ElectrumProtXInfo({required this.protxHash}); +class ElectrumRequestProtXInfo extends ElectrumRequest, dynamic> { + ElectrumRequestProtXInfo({required this.protxHash}); /// The hash of the initial ProRegTx final String protxHash; @@ -13,13 +13,13 @@ class ElectrumProtXInfo extends ElectrumRequest, dynamic> { String get method => ElectrumRequestMethods.protxInfo.method; @override - List toJson() { + List toParams() { return [protxHash]; } /// A dictionary with detailed deterministic masternode data @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/relay_fee.dart b/lib/src/provider/electrum_methods/methods/relay_fee.dart index 267dccf..e641901 100644 --- a/lib/src/provider/electrum_methods/methods/relay_fee.dart +++ b/lib/src/provider/electrum_methods/methods/relay_fee.dart @@ -1,21 +1,22 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + import 'package:bitcoin_base/src/utils/btc_utils.dart'; /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumRelayFee extends ElectrumRequest { +class ElectrumRequestRelayFee extends ElectrumRequest { /// blockchain.relayfee @override String get method => ElectrumRequestMethods.relayFee.method; @override - List toJson() { + List toParams() { return []; } /// relay fee in Bigint(satoshi) @override - BigInt onResonse(result) { + BigInt onResponse(result) { return BtcUtils.toSatoshi(result.toString()); } } diff --git a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart index 4990b6d..51c4349 100644 --- a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart +++ b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart @@ -1,9 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Unsubscribe from a script hash, preventing future notifications if its status changes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashUnSubscribe extends ElectrumRequest { - ElectrumScriptHashUnSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashUnSubscribe extends ElectrumRequest { + ElectrumRequestScriptHashUnSubscribe({required this.scriptHash}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -13,7 +13,7 @@ class ElectrumScriptHashUnSubscribe extends ElectrumRequest { String get method => ElectrumRequestMethods.scriptHashUnSubscribe.method; @override - List toJson() { + List toParams() { return [scriptHash]; } @@ -21,7 +21,7 @@ class ElectrumScriptHashUnSubscribe extends ElectrumRequest { /// otherwise False. Note that False might be returned even /// for something subscribed to earlier, because the server can drop subscriptions in rare circumstances. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_banner.dart b/lib/src/provider/electrum_methods/methods/server_banner.dart index 92e3ab4..d41eed8 100644 --- a/lib/src/provider/electrum_methods/methods/server_banner.dart +++ b/lib/src/provider/electrum_methods/methods/server_banner.dart @@ -2,17 +2,17 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a banner to be shown in the Electrum console. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerBanner extends ElectrumRequest { +class ElectrumRequestServerBanner extends ElectrumRequest { @override String get method => ElectrumRequestMethods.serverBanner.method; @override - List toJson() { + List toParams() { return []; } @override - String onResonse(result) { + String onResponse(result) { return result.toString(); } } diff --git a/lib/src/provider/electrum_methods/methods/server_features.dart b/lib/src/provider/electrum_methods/methods/server_features.dart index 7e9c939..5b36d72 100644 --- a/lib/src/provider/electrum_methods/methods/server_features.dart +++ b/lib/src/provider/electrum_methods/methods/server_features.dart @@ -2,20 +2,20 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a list of features and services supported by the server. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerFeatures extends ElectrumRequest { +class ElectrumRequestServerFeatures extends ElectrumRequest { /// server.features @override String get method => ElectrumRequestMethods.serverFeatures.method; @override - List toJson() { + List toParams() { return []; } /// A dictionary of keys and values. Each key represents a feature or service of the server, /// and the value gives additional information. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart index d1a3db5..f4a77bf 100644 --- a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart @@ -1,21 +1,20 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a list of peer servers. Despite the name this is not a subscription and the server must send no notifications.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerPeersSubscribe - extends ElectrumRequest, List> { +class ElectrumRequestServerPeersSubscribe extends ElectrumRequest, List> { /// server.peers.subscribe @override String get method => ElectrumRequestMethods.serverPeersSubscribe.method; @override - List toJson() { + List toParams() { return []; } /// An array of peer servers, each returned as a 3-element array @override - List onResonse(result) { + List onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/status.dart b/lib/src/provider/electrum_methods/methods/status.dart index f12a0fa..95e09fa 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/status.dart @@ -1,10 +1,9 @@ -import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashSubscribe - extends ElectrumRequest, dynamic> { - ElectrumScriptHashSubscribe({required this.scriptHash}); +class ElectrumRequestScriptHashSubscribe extends ElectrumRequest { + ElectrumRequestScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -14,13 +13,45 @@ class ElectrumScriptHashSubscribe String get method => ElectrumRequestMethods.scriptHashSubscribe.method; @override - List toJson() { + List toParams() { return [scriptHash]; } /// The status of the script hash. @override - Map onResonse(result) { - return Map.from(result); + String? onResponse(result) { + return result; + } +} + +class ElectrumBatchRequestScriptHashSubscribe + extends ElectrumBatchRequest> { + ElectrumBatchRequestScriptHashSubscribe({required this.scriptHashes}); + + /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) + final List scriptHashes; + + /// blockchain.scripthash.get_history + @override + String get method => ElectrumRequestMethods.scriptHashSubscribe.method; + + @override + List toParams() { + return [ + ...scriptHashes.map((e) => [e]) + ]; + } + + /// A list of confirmed transactions in blockchain order, + /// with the output of blockchain.scripthash.get_mempool() appended to the list. + /// Each confirmed transaction is a dictionary + @override + ElectrumBatchRequestResult onResponse( + Map data, + ElectrumBatchRequestDetails request, + ) { + final id = data['id'] as int; + final result = data['result'] as String?; + return ElectrumBatchRequestResult(request: request, id: id, result: result); } } diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart new file mode 100644 index 0000000..4a940f7 --- /dev/null +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -0,0 +1,120 @@ +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + +class TweakOutputData { + final int vout; + final int amount; + final dynamic spendingInput; + + TweakOutputData({ + required this.vout, + required this.amount, + this.spendingInput, + }); +} + +class TweakData { + final String tweak; + final Map outputPubkeys; + + TweakData({required this.tweak, required this.outputPubkeys}); +} + +class ElectrumTweaksSubscribeResponse { + final String? message; + final int block; + final Map blockTweaks; + + ElectrumTweaksSubscribeResponse({ + required this.block, + required this.blockTweaks, + this.message, + }); + + static ElectrumTweaksSubscribeResponse? fromJson(Map json) { + if (json.isEmpty) { + return null; + } + + if (json.containsKey('params')) { + final params = json['params'] as List; + final message = params.first["message"]; + + if (message != null) { + return null; + } + } + + late int block; + final blockTweaks = {}; + + try { + for (final key in json.keys) { + block = int.parse(key); + final txs = json[key] as Map; + + for (final txid in txs.keys) { + final tweakResponseData = txs[txid] as Map; + + final tweakHex = tweakResponseData["tweak"].toString(); + final outputPubkeys = (tweakResponseData["output_pubkeys"] as Map); + + final tweakOutputData = {}; + + for (final vout in outputPubkeys.keys) { + final outputData = outputPubkeys[vout]; + tweakOutputData[outputData[0]] = TweakOutputData( + vout: int.parse(vout.toString()), + amount: outputData[1], + spendingInput: outputData.length > 2 ? outputData[2] : null, + ); + } + + final tweakData = TweakData(tweak: tweakHex, outputPubkeys: tweakOutputData); + blockTweaks[txid] = tweakData; + } + } + } catch (_) { + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: 0, + blockTweaks: {}, + ); + } + + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: block, + blockTweaks: blockTweaks, + ); + } +} + +/// Subscribe to receive block headers when a new block is found. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumTweaksSubscribe + extends ElectrumRequest> { + /// blockchain.tweaks.subscribe + ElectrumTweaksSubscribe({ + required this.height, + required this.count, + required this.historicalMode, + }); + + final int height; + final int count; + final bool historicalMode; + + @override + String get method => ElectrumRequestMethods.tweaksSubscribe.method; + + @override + List toParams() { + return [height, count, historicalMode]; + } + + /// The header of the current block chain tip. + @override + ElectrumTweaksSubscribeResponse? onResponse(result) { + return ElectrumTweaksSubscribeResponse.fromJson(result); + } +} diff --git a/lib/src/provider/models/block_cypher/block_cypher_models.dart b/lib/src/provider/models/block_cypher/block_cypher_models.dart index 5d4761b..09f994c 100644 --- a/lib/src/provider/models/block_cypher/block_cypher_models.dart +++ b/lib/src/provider/models/block_cypher/block_cypher_models.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; class TxRef implements UTXO { final String txHash; @@ -32,7 +33,7 @@ class TxRef implements UTXO { blockHeight: json['block_height'], txInputN: json['tx_input_n'], txOutputN: json['tx_output_n'], - value: BigInt.from(json['value']), + value: BigintUtils.parse(json['value']), refBalance: json['ref_balance'], spent: json['spent'], confirmations: json['confirmations'], @@ -40,6 +41,20 @@ class TxRef implements UTXO { script: json['script'], ); } + @override + Map toJson() { + return { + "tx_hash": txHash, + "block_height": blockHeight, + "tx_input_n": txInputN, + "value": value.toString(), + "tx_output_n": txOutputN, + "spent": spent, + "confirmations": confirmations, + "confirmed": confirmed.toString(), + "script": script + }; + } @override BitcoinUtxo toUtxo(BitcoinAddressType addressType) { @@ -99,7 +114,7 @@ class BlockCypherUtxo { } List toUtxoWithOwner(UtxoAddressDetails owner) { - List utxos = txRefs.map((ref) { + final utxos = txRefs.map((ref) { return UtxoWithAddress( utxo: ref.toUtxo(owner.address.type), ownerDetails: owner, diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 9369398..01b77ce 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -1,5 +1,5 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; +import 'package:bitcoin_base/src/models/network.dart'; import 'package:bitcoin_base/src/provider/constant/constant.dart'; enum APIType { mempool, blockCypher } @@ -12,7 +12,10 @@ class APIConfig { final String sendTransaction; final String blockHeight; final APIType apiType; + final String rawTransaction; final BasedUtxoNetwork network; + final String? block; + final String? blockTimestamp; factory APIConfig.selectApi(APIType apiType, BasedUtxoNetwork network) { switch (apiType) { @@ -24,8 +27,8 @@ class APIConfig { } String getUtxoUrl(String address) { - String baseUrl = url; - return baseUrl.replaceAll("###", address); + final baseUrl = url; + return baseUrl.replaceAll('###', address); } String getFeeApiUrl() { @@ -33,18 +36,41 @@ class APIConfig { } String getTransactionUrl(String transactionId) { - String baseUrl = transaction; - return baseUrl.replaceAll("###", transactionId); + final baseUrl = transaction; + return baseUrl.replaceAll('###', transactionId); + } + + String getRawTransactionUrl(String transactionId) { + final baseUrl = rawTransaction; + return baseUrl.replaceAll('###', transactionId); + } + + String getBlockUrl(String blockHash) { + if (block == null) { + throw const BitcoinBasePluginException("block url is not available"); + } + + String baseUrl = block!; + return baseUrl.replaceAll("###", blockHash); + } + + String getBlockTimestampUrl(int timestamp) { + if (blockTimestamp == null) { + throw const BitcoinBasePluginException("block timestamp url is not available"); + } + + String baseUrl = blockTimestamp!; + return baseUrl.replaceAll("###", timestamp.toString()); } String getTransactionsUrl(String address) { - String baseUrl = transactions; - return baseUrl.replaceAll("###", address); + final baseUrl = transactions; + return baseUrl.replaceAll('###', address); } String getBlockHeight(int blockHaight) { - String baseUrl = blockHeight; - return baseUrl.replaceAll("###", "$blockHaight"); + final baseUrl = blockHeight; + return baseUrl.replaceAll('###', '$blockHaight'); } factory APIConfig.fromBlockCypher(BasedUtxoNetwork network) { @@ -67,14 +93,14 @@ class APIConfig { break; default: throw BitcoinBasePluginException( - "blockcypher does not support ${network.conf.coinName.name}, u must use your own provider"); + 'blockcypher does not support ${network.conf.coinName.name}, u must use your own provider'); } return APIConfig( - url: - "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", + url: "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", feeRate: baseUrl, transaction: "$baseUrl/txs/###", + rawTransaction: '$baseUrl/txs/###', sendTransaction: "$baseUrl/txs/push", apiType: APIType.blockCypher, transactions: "$baseUrl/addrs/###/full?limit=200", @@ -82,29 +108,33 @@ class APIConfig { blockHeight: "$baseUrl/blocks/###"); } - factory APIConfig.mempool(BasedUtxoNetwork network) { - String baseUrl; - switch (network) { - case BitcoinNetwork.mainnet: - baseUrl = BtcApiConst.mempoolMainBaseURL; - break; - case BitcoinNetwork.testnet: - baseUrl = BtcApiConst.mempoolBaseURL; - break; - default: - throw BitcoinBasePluginException( - "mempool does not support ${network.conf.coinName.name}"); + factory APIConfig.mempool(BasedUtxoNetwork network, [String? baseUrl]) { + if (baseUrl == null) { + switch (network) { + case BitcoinNetwork.mainnet: + baseUrl = BtcApiConst.mempoolMainBaseURL; + break; + case BitcoinNetwork.testnet: + baseUrl = BtcApiConst.mempoolBaseURL; + break; + default: + throw BitcoinBasePluginException( + "mempool does not support ${network.conf.coinName.name}"); + } } return APIConfig( url: "$baseUrl/address/###/utxo", - feeRate: "$baseUrl/v1/fees/recommended", + feeRate: "$baseUrl/fees/recommended", transaction: "$baseUrl/tx/###", + rawTransaction: '$baseUrl/tx/###/hex', sendTransaction: "$baseUrl/tx", apiType: APIType.mempool, transactions: "$baseUrl/address/###/txs", network: network, - blockHeight: "$baseUrl/block-height/###"); + blockHeight: "$baseUrl/block-height/###", + block: "$baseUrl/block/###", + blockTimestamp: "$baseUrl/mining/blocks/timestamp/###"); } APIConfig( @@ -115,5 +145,8 @@ class APIConfig { required this.sendTransaction, required this.apiType, required this.network, - required this.blockHeight}); + required this.blockHeight, + required this.rawTransaction, + this.block, + this.blockTimestamp}); } diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart index 9fd9abc..9b3825f 100644 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ b/lib/src/provider/models/electrum/electrum_utxo.dart @@ -1,19 +1,17 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/cash_token/cash_token.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; +import 'package:blockchain_utils/utils/numbers/utils/int_utils.dart'; class ElectrumUtxo implements UTXO { factory ElectrumUtxo.fromJson(Map json) { - CashToken? token; - if (json.containsKey("token_data")) { - token = CashToken.fromJson(json["token_data"]); - } return ElectrumUtxo._( - height: json["height"], - txId: json["tx_hash"], - vout: json["tx_pos"], - value: BigInt.parse((json["value"].toString())), - token: token); + height: IntUtils.parse(json['height']), + txId: json['tx_hash'], + vout: IntUtils.parse(json['tx_pos']), + value: BigintUtils.parse(json['value']), + token: json["token_data"] == null ? null : CashToken.fromJson(json['token_data'])); } const ElectrumUtxo._( {required this.height, @@ -30,11 +28,27 @@ class ElectrumUtxo implements UTXO { @override BitcoinUtxo toUtxo(BitcoinAddressType addressType) { return BitcoinUtxo( - txHash: txId, - value: value, - vout: vout, - scriptType: addressType, - blockHeight: height, - token: token); + txHash: txId, + value: value, + vout: vout, + scriptType: addressType, + blockHeight: height, + token: token, + ); + } + + static List fromJsonList(List json) { + return json.map((e) => ElectrumUtxo.fromJson(e)).toList(); + } + + @override + Map toJson() { + return { + "token_data": '', + "height": height, + "tx_hash": txId, + "tx_pos": vout, + "value": value.toString() + }; } } diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 09c6312..910a825 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -2,30 +2,45 @@ import 'package:bitcoin_base/src/exception/exception.dart'; enum BitcoinFeeRateType { low, medium, high } +class BitcoinFee { + BitcoinFee({int? satoshis, BigInt? bytes}) + : satoshis = satoshis ?? _parseKbFees(bytes!), + bytes = bytes ?? _parseMempoolFees(satoshis!); + + final int satoshis; + final BigInt bytes; + + @override + String toString() { + return 'satoshis: $satoshis, bytes: $bytes'; + } +} + class BitcoinFeeRate { - BitcoinFeeRate( - {required this.high, - required this.medium, - required this.low, - this.economyFee, - this.hourFee}); + BitcoinFeeRate({ + required this.high, + required this.medium, + required this.low, + this.economyFee, + this.minimumFee, + }); /// High fee rate in satoshis per kilobyte - final BigInt high; + final BitcoinFee high; /// Medium fee rate in satoshis per kilobyte - final BigInt medium; + final BitcoinFee medium; /// low fee rate in satoshis per kilobyte - final BigInt low; + final BitcoinFee low; - /// only mnenpool api - final BigInt? economyFee; + /// only mempool api + final BitcoinFee? economyFee; - /// only mnenpool api - final BigInt? hourFee; + /// only mempool api + final BitcoinFee? minimumFee; - BigInt _feeRatrete(BitcoinFeeRateType feeRateType) { + BitcoinFee _feeRate(BitcoinFeeRateType feeRateType) { switch (feeRateType) { case BitcoinFeeRateType.low: return low; @@ -36,6 +51,10 @@ class BitcoinFeeRate { } } + int toSat(BigInt feeRate) { + return _parseKbFees(feeRate); + } + /// GetEstimate calculates the estimated fee in satoshis for a given transaction size /// and fee rate (in satoshis per kilobyte) using the formula: // @@ -48,16 +67,15 @@ class BitcoinFeeRate { /// Returns: /// - BigInt: A BigInt containing the estimated fee in satoshis. BigInt getEstimate(int trSize, - {BigInt? customFeeRatePerKb, - BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { - BigInt feeRate = customFeeRatePerKb ?? _feeRatrete(feeRateType); + {BigInt? customFeeRatePerKb, BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { + BigInt feeRate = customFeeRatePerKb ?? _feeRate(feeRateType).bytes; final trSizeBigInt = BigInt.from(trSize); return (trSizeBigInt * feeRate) ~/ BigInt.from(1000); } @override String toString() { - return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee hourFee: $hourFee'; + return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee minimumFee: $minimumFee'; } /// NewBitcoinFeeRateFromMempool creates a BitcoinFeeRate structure from JSON data retrieved @@ -65,14 +83,11 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromMempool(Map json) { return BitcoinFeeRate( - high: _parseMempoolFees(json['fastestFee']), - medium: _parseMempoolFees(json['halfHourFee']), - low: _parseMempoolFees(json['minimumFee']), - economyFee: json['economyFee'] == null - ? null - : _parseMempoolFees(json['economyFee']), - hourFee: - json['hourFee'] == null ? null : _parseMempoolFees(json['hourFee']), + high: BitcoinFee(satoshis: json['fastestFee']), + medium: BitcoinFee(satoshis: json['halfHourFee']), + low: BitcoinFee(satoshis: json['hourFee']), + economyFee: json['economyFee'] == null ? null : BitcoinFee(satoshis: json['economyFee']), + minimumFee: json['minimumFee'] == null ? null : BitcoinFee(satoshis: json['minimumFee']), ); } @@ -81,9 +96,10 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromBlockCypher(Map json) { return BitcoinFeeRate( - high: BigInt.from((json['high_fee_per_kb'] as int)), - medium: BigInt.from((json['medium_fee_per_kb'] as int)), - low: BigInt.from((json['low_fee_per_kb'] as int))); + high: BitcoinFee(bytes: BigInt.from((json['high_fee_per_kb'] as int))), + medium: BitcoinFee(bytes: BigInt.from((json['medium_fee_per_kb'] as int))), + low: BitcoinFee(bytes: BigInt.from((json['low_fee_per_kb'] as int))), + ); } } @@ -103,3 +119,12 @@ BigInt _parseMempoolFees(dynamic data) { "cannot parse mempool fees excepted double, string got ${data.runtimeType}"); } } + +/// ParseMempoolFees takes a data dynamic and converts it to a BigInt representing +/// mempool fees in satoshis per kilobyte (sat/KB). The function performs the conversion +/// based on the type of the input data, which can be either a double (floating-point +/// fee rate) or an int (integer fee rate in satoshis per byte). +int _parseKbFees(BigInt fee) { + const kb = 1024; + return (fee.toInt() / kb).round(); +} diff --git a/lib/src/provider/models/mempool/mempol_models.dart b/lib/src/provider/models/mempool/mempol_models.dart index b1ea5b5..d891541 100644 --- a/lib/src/provider/models/mempool/mempol_models.dart +++ b/lib/src/provider/models/mempool/mempol_models.dart @@ -1,5 +1,6 @@ import 'package:bitcoin_base/src/bitcoin/address/address.dart'; import 'package:bitcoin_base/src/provider/models/utxo_details.dart'; +import 'package:blockchain_utils/utils/numbers/utils/bigint_utils.dart'; class MempoolPrevOut { final String scriptPubKey; @@ -139,10 +140,10 @@ class MempoolTransaction { txID: json['txid'], version: json['version'], locktime: json['locktime'], - vin: - List.from(json['vin'].map((x) => MempoolVin.fromJson(x))), + vin: List.from( + (json['vin'] as List).map((x) => MempoolVin.fromJson(x))), vout: List.from( - json['vout'].map((x) => MempoolVout.fromJson(x))), + (json['vout'] as List).map((x) => MempoolVout.fromJson(x))), size: json['size'], weight: json['weight'], fee: json['fee'], @@ -169,7 +170,7 @@ class MempolUtxo implements UTXO { txid: json['txid'], vout: json['vout'], status: MempoolStatus.fromJson(json['status']), - value: BigInt.parse(json['value'].toString()), + value: BigintUtils.parse(json['value']), ); } @@ -182,11 +183,16 @@ class MempolUtxo implements UTXO { scriptType: addressType, blockHeight: 1); } + + @override + Map toJson() { + return {"txid": txid, "vout": vout, "status": status, "value": value}; + } } extension MempoolUtxoExtentions on List { List toUtxoWithOwnerList(UtxoAddressDetails owner) { - List utxos = map((e) => UtxoWithAddress( + final utxos = map((e) => UtxoWithAddress( utxo: BitcoinUtxo( txHash: e.txid, value: e.value, diff --git a/lib/src/provider/models/multisig_script.dart b/lib/src/provider/models/multisig_script.dart index 657f912..0fb091a 100644 --- a/lib/src/provider/models/multisig_script.dart +++ b/lib/src/provider/models/multisig_script.dart @@ -54,12 +54,12 @@ class MultiSignatureAddress { throw BitcoinBasePluginException( "${network.conf.coinName.name} Bitcoin forks that do not support Segwit. use toP2shAddress"); } - return P2wshAddress.fromScript(script: multiSigScript); + return P2wshAddress.fromScriptPubkey(script: multiSigScript); } BitcoinBaseAddress toP2wshInP2shAddress({required BasedUtxoNetwork network}) { final p2wsh = toP2wshAddress(network: network); - return P2shAddress.fromScript( + return P2shAddress.fromScriptPubkey( script: p2wsh.toScriptPubKey(), type: P2shAddressType.p2wshInP2sh); } @@ -72,11 +72,11 @@ class MultiSignatureAddress { if (addressType.hashLength == 32) { return P2shAddress.fromHash160( - addrHash: BytesUtils.toHexString( + h160: BytesUtils.toHexString( QuickCrypto.sha256DoubleHash(multiSigScript.toBytes())), type: addressType); } - return P2shAddress.fromScript(script: multiSigScript, type: addressType); + return P2shAddress.fromScriptPubkey(script: multiSigScript, type: addressType); } BitcoinBaseAddress fromType({ diff --git a/lib/src/provider/models/utxo_details.dart b/lib/src/provider/models/utxo_details.dart index a6ecb98..f6299e3 100644 --- a/lib/src/provider/models/utxo_details.dart +++ b/lib/src/provider/models/utxo_details.dart @@ -48,8 +48,7 @@ class UtxoWithAddress { ECPublic public() { if (isMultiSig()) { - throw const BitcoinBasePluginException( - "Cannot access public key in multi-signature address"); + throw const BitcoinBasePluginException("Cannot access public key in multi-signature address"); } if (ownerDetails._publicKey == null) { throw const BitcoinBasePluginException( @@ -95,15 +94,25 @@ class BitcoinOutput implements BitcoinSpendableBaseOutput { /// Value is a pointer to a BigInt representing the amount of bitcoins sent to the recipient. @override final BigInt value; + + bool isSilentPayment; + bool isChange; + // final CashToken? token; BitcoinOutput({ required this.address, required this.value, + this.isSilentPayment = false, + this.isChange = false, }); @override - TxOutput get toOutput => - TxOutput(amount: value, scriptPubKey: address.toScriptPubKey()); + TxOutput get toOutput => TxOutput( + amount: value, + scriptPubKey: address.toScriptPubKey(), + isSilentPayment: isSilentPayment, + isChange: isChange, + ); } /// Represents a custom script-based Bitcoin output, implementing BitcoinBaseOutput. @@ -120,8 +129,7 @@ class BitcoinScriptOutput implements BitcoinBaseOutput { /// Convert the custom script output to a standard TxOutput. @override - TxOutput get toOutput => - TxOutput(amount: value, scriptPubKey: script, cashToken: null); + TxOutput get toOutput => TxOutput(amount: value, scriptPubKey: script, cashToken: null); } /// BitcoinTokenOutput represents details about a Bitcoin cash transaction with cash token output, including @@ -143,8 +151,8 @@ class BitcoinTokenOutput implements BitcoinSpendableBaseOutput { /// Convert the custom script output to a standard TxOutput. @override - TxOutput get toOutput => TxOutput( - amount: value, scriptPubKey: address.toScriptPubKey(), cashToken: token); + TxOutput get toOutput => + TxOutput(amount: value, scriptPubKey: address.toScriptPubKey(), cashToken: token); } /// Represents a burnable output, specifically related to [BitcoinTokenOutput] for burning Cash Tokens. @@ -201,6 +209,7 @@ class BitcoinUtxo { required this.scriptType, this.blockHeight, this.token, + this.isSilentPayment, }); /// check if utxos is p2tr @@ -208,6 +217,8 @@ class BitcoinUtxo { return scriptType == SegwitAddresType.p2tr; } + bool? isSilentPayment; + /// check if utxos is segwit bool isSegwit() { return scriptType.isSegwit || isP2shSegwit(); @@ -215,13 +226,12 @@ class BitcoinUtxo { /// checl if utxos is p2sh neasted segwit bool isP2shSegwit() { - return scriptType == P2shAddressType.p2wpkhInP2sh || - scriptType == P2shAddressType.p2wshInP2sh; + return scriptType == P2shAddressType.p2wpkhInP2sh || scriptType == P2shAddressType.p2wshInP2sh; } /// convert utxos to transaction input with specify sequence like ReplaceByeFee (4Bytes) TxInput toInput([List? sequence]) { - return TxInput(txId: txHash, txIndex: vout, sequance: sequence); + return TxInput(txId: txHash, txIndex: vout, sequence: sequence); } @override diff --git a/lib/src/provider/providers/electrum.dart b/lib/src/provider/providers/electrum.dart new file mode 100644 index 0000000..e964b04 --- /dev/null +++ b/lib/src/provider/providers/electrum.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:rxdart/rxdart.dart'; + +class BatchSubscription { + final BehaviorSubject subscription; + final ElectrumBatchRequestDetails params; + + BatchSubscription(this.subscription, this.params); +} + +typedef ListenerCallback = StreamSubscription Function( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, +}); + +class ElectrumProvider { + final BitcoinBaseElectrumRPCService rpc; + ElectrumProvider._(this.rpc); + int _id = 0; + int get id => _id; + Timer? _aliveTimer; + + static Future connect(Future rpc) async { + final provider = ElectrumProvider._(await rpc); + provider.keepAlive(); + return provider; + } + + Future>> batchRequest( + ElectrumBatchRequest request, [ + Duration? timeout, + ]) async { + final id = ++_id; + final params = request.toRequest(id) as ElectrumBatchRequestDetails; + _id = request.finalId; + + final results = await rpc.batchCall(params, timeout); + return results.map((r) => request.onResponse(r, params)).toList(); + } + + /// Sends a request to the Electrum server using the specified [request] parameter. + /// + /// The [timeout] parameter, if provided, sets the maximum duration for the request. + Future request(ElectrumRequest request, [Duration? timeout]) async { + final id = ++_id; + final params = request.toRequest(id); + final result = await rpc.call(params, timeout); + return request.onResponse(result); + } + + Future>?> batchSubscribe( + ElectrumBatchRequest request, [ + Duration? timeout, + ]) async { + final id = ++_id; + final params = request.toRequest(id) as ElectrumBatchRequestDetails; + _id = request.finalId; + final subscriptions = rpc.batchSubscribe(params); + + if (subscriptions == null) return null; + + return subscriptions.map((s) => BatchSubscription(s.subscription, params)).toList(); + } + + // Preserving generic type T in subscribe method + BehaviorSubject? subscribe(ElectrumRequest request) { + final id = ++_id; + final params = request.toRequest(id); + final subscription = rpc.subscribe(params); + + if (subscription == null) return null; + + return subscription.subscription; + } + + Future> getFeeRates() async { + try { + final topDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 1)); + final middleDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 5)); + final bottomDoubleString = await request(ElectrumRequestEstimateFee(numberOfBlock: 10)); + + final top = (topDoubleString!.toInt() / 1000).round(); + final middle = (middleDoubleString!.toInt() / 1000).round(); + final bottom = (bottomDoubleString!.toInt() / 1000).round(); + + return [bottom, middle, top]; + } catch (_) { + return []; + } + } + + void keepAlive() { + _aliveTimer?.cancel(); + _aliveTimer = Timer.periodic(const Duration(seconds: 6), (_) async => ping()); + } + + void ping() async { + try { + return await request(ElectrumRequestPing()); + } catch (_) {} + } +} diff --git a/lib/src/provider/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index c8b56be..f8bbc94 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -1,3 +1,8 @@ export 'methods.dart'; export 'params.dart'; export 'service.dart'; +export 'electrum_version.dart'; +export 'request_completer.dart'; +export 'electrum_ssl_service.dart'; +export 'electrum_tcp_service.dart'; +// export 'electrum_websocket_service.dart'; diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart new file mode 100644 index 0000000..4070099 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -0,0 +1,405 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumSSLService implements BitcoinBaseElectrumRPCService { + ElectrumSSLService._( + this.url, + SecureSocket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + SecureSocket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + try { + await _socket?.close(); + } catch (e) { + print("ElectrumSSLService: close(): $e"); + } + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + _setConnectionStatus(ConnectionStatus.disconnected); + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await SecureSocket.connect( + uri.host, + uri.port, + onBadCertificate: (_) => true, + ).timeout(connectionTimeOut); + + return ElectrumSSLService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + + unterminatedString = ''; + } + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + unterminatedString = ''; + } + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + + if (error != null) { + final message = error["message"]; + + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + + if (id == null && method != null) { + _tasks.forEach((key, value) { + if (value.request.method == method) { + id = key; + } + }); + } + } + + final result = _findResult(response, _tasks[id]?.request); + _finish(id!, result); + } + + void _onMessage(List event) { + try { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } catch (_) {} + } + + dynamic _findResult(dynamic data, BaseElectrumRequestDetails? request) { + final error = data["error"]; + int? id; + try { + id = request?.params["id"]; + } catch (_) {} + final requestParams = data["request"] ?? request?.params; + + if (error != null) { + if (error is String) { + if (id != null) { + _errors[id] = RPCError( + data: error, + errorCode: 0, + message: error, + request: requestParams, + ); + } + } else { + final code = int.tryParse(((error['code']?.toString()) ?? "0")) ?? 0; + final message = error['message'] ?? ""; + + if (id != null) { + _errors[id] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + } + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } + + if (message.toLowerCase().contains("batch limit")) { + for (final k in _tasks.keys) { + final task = _tasks[k]; + + if (task?.isBatchRequest ?? false) { + _errors[k] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + + if (task!.isSubscription) { + task.subject?.addError(_errors[k]!); + } else { + task.completer?.completeError(_errors[k]!); + } + } + } + } + } + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + if (!task.isSubscription) { + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + if (task.isBatchRequest) { + task.completer!.complete({"id": id, "result": result}); + } else { + task.completer!.complete(result); + } + } + + _tasks.remove(id); + } else { + if (task.isBatchRequest) { + task.subject?.add({"id": id, "result": result}); + } else { + task.subject?.add(result); + } + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + List> _registerBatchSubscription( + ElectrumBatchRequestDetails mainParams, + ) { + return mainParams.params.map((params) { + final subscription = AsyncBehaviorSubject(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + subject: subscription.subscription, + request: mainParams, + isSubscription: true, + isBatchRequest: true, + ); + + return subscription; + }).toList(); + } + + @override + List>? batchSubscribe(ElectrumBatchRequestDetails params) { + try { + final subscriptions = _registerBatchSubscription(params); + add(params.toTCPParams()); + + return subscriptions; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + List> _registerBatchTask(ElectrumBatchRequestDetails mainParams) { + return mainParams.params.map((params) { + final completer = AsyncRequestCompleter(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + completer: completer.completer, + request: mainParams, + isSubscription: false, + isBatchRequest: true, + ); + + return completer; + }).toList(); + } + + @override + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]) async { + try { + final completers = _registerBatchTask(params); + add(params.toTCPParams()); + final result = await Future.wait(completers.map((e) => e.completer.future)) + .timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + for (final id in params.paramsById.keys) { + _tasks.remove(id); + } + } + } + + RPCError? getError(int id) => _errors[id]; +} diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart new file mode 100644 index 0000000..e8dda66 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -0,0 +1,397 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/service.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { + ElectrumTCPService._( + this.url, + Socket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + Socket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + try { + await _socket?.close(); + } catch (e) { + print("ElectrumSSLService: close(): $e"); + } + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + _setConnectionStatus(ConnectionStatus.disconnected); + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await Socket.connect(uri.host, uri.port).timeout(connectionTimeOut); + + return ElectrumTCPService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + + unterminatedString = ''; + } + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + try { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } catch (_) { + final response = json.decode(unterminatedString) as List; + for (final element in response) { + _handleResponse(element as Map); + } + unterminatedString = ''; + } + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + if (response["error"] != null) { + final message = error["message"]; + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + + _tasks.forEach((key, value) { + if (value.request.method == method) { + id = key; + } + }); + } + + final result = _findResult(response, _tasks[id]!.request); + _finish(id!, result); + } + + void _onMessage(List event) { + try { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } catch (_) {} + } + + dynamic _findResult(dynamic data, BaseElectrumRequestDetails? request) { + final error = data["error"]; + int? id; + try { + id = request?.params["id"]; + } catch (_) {} + final requestParams = data["request"] ?? request?.params; + + if (error != null) { + if (error is String) { + if (id != null) { + _errors[id] = RPCError( + data: error, + errorCode: 0, + message: error, + request: requestParams, + ); + } + } else { + final code = int.tryParse(((error['code']?.toString()) ?? "0")) ?? 0; + final message = error['message'] ?? ""; + + if (id != null) { + _errors[id] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + } + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } + + if (message.toLowerCase().contains("batch limit")) { + for (final k in _tasks.keys) { + final task = _tasks[k]; + + if (task?.isBatchRequest ?? false) { + _errors[k] = RPCError( + errorCode: code, + message: message, + data: error["data"], + request: requestParams, + ); + + if (task!.isSubscription) { + task.subject?.addError(_errors[k]!); + } else { + task.completer?.completeError(_errors[k]!); + } + } + } + } + } + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + if (!task.isSubscription) { + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + if (task.isBatchRequest) { + task.completer!.complete({"id": id, "result": result}); + } else { + task.completer!.complete(result); + } + } + + _tasks.remove(id); + } else { + if (task.isBatchRequest) { + task.subject?.add({"id": id, "result": result}); + } else { + task.subject?.add(result); + } + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + List> _registerBatchSubscription( + ElectrumBatchRequestDetails mainParams, + ) { + return mainParams.params.map((params) { + final subscription = AsyncBehaviorSubject(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + subject: subscription.subscription, + request: mainParams, + isSubscription: true, + isBatchRequest: true, + ); + + return subscription; + }).toList(); + } + + @override + List>? batchSubscribe(ElectrumBatchRequestDetails params) { + try { + final subscriptions = _registerBatchSubscription(params); + add(params.toTCPParams()); + + return subscriptions; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + List> _registerBatchTask(ElectrumBatchRequestDetails mainParams) { + return mainParams.params.map((params) { + final completer = AsyncRequestCompleter(params); + + final id = params["id"] as int; + _tasks[id] = SocketTask( + completer: completer.completer, + request: mainParams, + isSubscription: false, + ); + + return completer; + }).toList(); + } + + @override + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]) async { + try { + final completers = _registerBatchTask(params); + add(params.toTCPParams()); + final result = await Future.wait(completers.map((e) => e.completer.future)) + .timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + for (final id in params.paramsById.keys) { + _tasks.remove(id); + } + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} diff --git a/lib/src/provider/service/electrum/electrum_version.dart b/lib/src/provider/service/electrum/electrum_version.dart new file mode 100644 index 0000000..ef485c7 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_version.dart @@ -0,0 +1,49 @@ +import 'package:bitcoin_base/src/exception/exception.dart'; + +class ElectrumVersion { + final int major; + final int minor; + final int patch; + + const ElectrumVersion(this.major, this.minor, this.patch); + + factory ElectrumVersion.fromStr(String version) { + final parts = version.split('.'); + if (parts.length != 3) { + throw BitcoinBasePluginException('Invalid version string: $version'); + } + + return ElectrumVersion(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); + } + + int compareTo(ElectrumVersion other) { + if (major > other.major) { + return 1; + } else if (major < other.major) { + return -1; + } + + if (major == other.major) { + if (minor > other.minor) { + return 1; + } else if (minor < other.minor) { + return -1; + } + } + + if (minor == other.minor) { + if (patch > other.patch) { + return 1; + } else if (patch < other.patch) { + return -1; + } + } + + return 0; + } + + @override + String toString() { + return '$major.$minor.$patch'; + } +} diff --git a/lib/src/provider/service/electrum/electrum_websocket_service.dart b/lib/src/provider/service/electrum/electrum_websocket_service.dart new file mode 100644 index 0000000..1a460a6 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_websocket_service.dart @@ -0,0 +1,85 @@ +// import 'dart:async'; +// import 'dart:convert'; +// import 'package:bitcoin_base/bitcoin_base.dart'; +// import 'package:example/services_examples/cross_platform_websocket/core.dart'; + +// class ElectrumWebSocketService implements BitcoinBaseElectrumRPCService { +// ElectrumWebSocketService._( +// this.url, +// WebSocketCore channel, { +// this.defaultRequestTimeOut = const Duration(seconds: 30), +// }) : _socket = channel { +// _subscription = +// channel.stream.cast().listen(_onMessage, onError: _onClose, onDone: _onDone); +// } +// WebSocketCore? _socket; +// StreamSubscription? _subscription; +// final Duration defaultRequestTimeOut; + +// Map requests = {}; +// bool _isDisconnected = false; + +// bool get isConnected => !_isDisconnected; + +// @override +// final String url; + +// void add(List params) { +// if (_isDisconnected) { +// throw StateError("socket has been disconnected"); +// } +// _socket?.sink(params); +// } + +// void _onClose(Object? error) { +// _isDisconnected = true; + +// _socket?.close(); +// _socket = null; +// _subscription?.cancel().catchError((e) {}); +// _subscription = null; +// } + +// void _onDone() { +// _onClose(null); +// } + +// @override +// void disconnect() { +// _onClose(null); +// } + +// static Future connect( +// String url, { +// Iterable? protocols, +// Duration defaultRequestTimeOut = const Duration(seconds: 30), +// final Duration connectionTimeOut = const Duration(seconds: 30), +// }) async { +// final channel = await WebSocketCore.connect(url, protocols: protocols?.toList()); + +// return ElectrumWebSocketService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); +// } + +// void _onMessage(String event) { +// final Map decode = json.decode(event); +// if (decode.containsKey("id")) { +// final int id = int.parse(decode["id"]!.toString()); +// final request = requests.remove(id); +// request?.completer.complete(decode); +// } +// } + +// @override +// Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { +// final AsyncRequestCompleter compeleter = AsyncRequestCompleter(params.params); + +// try { +// requests[params.id] = compeleter; +// add(params.toWebSocketParams()); +// final result = await compeleter.completer.future.timeout(timeout ?? defaultRequestTimeOut); +// return result; +// } finally { +// requests.remove(params.id); +// } +// } +// } diff --git a/lib/src/provider/service/electrum/methods.dart b/lib/src/provider/service/electrum/methods.dart index 027fd44..bd4121a 100644 --- a/lib/src/provider/service/electrum/methods.dart +++ b/lib/src/provider/service/electrum/methods.dart @@ -12,24 +12,24 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("server.donation_address"); /// A newly-started server uses this call to get itself into other servers’ peers lists. It should not be used by wallet clients. - static const ElectrumRequestMethods serverAddPeer = - ElectrumRequestMethods._("server.add_peer"); + 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 = @@ -56,44 +56,46 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("blockchain.block.headers"); /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. - static const ElectrumRequestMethods estimateFee = - ElectrumRequestMethods._("blockchain.estimatefee"); + static const String estimateFeeMethod = "blockchain.estimatefee"; + static const ElectrumRequestMethods estimateFee = ElectrumRequestMethods._(estimateFeeMethod); /// Return the confirmed and unconfirmed history of a script hash. - static const ElectrumRequestMethods getHistory = - ElectrumRequestMethods._("blockchain.scripthash.get_history"); + static const String getHistoryMethod = "blockchain.scripthash.get_history"; + static const ElectrumRequestMethods getHistory = ElectrumRequestMethods._(getHistoryMethod); /// Return the unconfirmed transactions of a script hash. static const ElectrumRequestMethods getMempool = ElectrumRequestMethods._("blockchain.scripthash.get_mempool"); /// Broadcast a transaction to the network. - static const ElectrumRequestMethods broadCast = - ElectrumRequestMethods._("blockchain.transaction.broadcast"); + static const String broadcastMethod = "blockchain.transaction.broadcast"; + static const ElectrumRequestMethods broadcast = ElectrumRequestMethods._(broadcastMethod); /// Return a banner to be shown in the Electrum console. - static const ElectrumRequestMethods serverBanner = - ElectrumRequestMethods._("server.banner"); + static const ElectrumRequestMethods serverBanner = ElectrumRequestMethods._("server.banner"); /// Return a list of features and services supported by the server. - static const ElectrumRequestMethods serverFeatures = - ElectrumRequestMethods._("server.features"); + static const ElectrumRequestMethods serverFeatures = ElectrumRequestMethods._("server.features"); /// 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. - static const ElectrumRequestMethods ping = - ElectrumRequestMethods._("server.ping"); + static const ElectrumRequestMethods ping = ElectrumRequestMethods._("server.ping"); /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. - static const ElectrumRequestMethods version = - ElectrumRequestMethods._("server.version"); + static const String versionMethod = "server.version"; + static const ElectrumRequestMethods version = ElectrumRequestMethods._(versionMethod); /// Subscribe to receive block headers when a new block is found. + static const String headersSubscribeMethod = "blockchain.headers.subscribe"; static const ElectrumRequestMethods headersSubscribe = - ElectrumRequestMethods._("blockchain.headers.subscribe"); + ElectrumRequestMethods._(headersSubscribeMethod); + + /// Subscribe to receive block headers when a new block is found. + static const String tweaksSubscribeMethod = "blockchain.tweaks.subscribe"; + static const ElectrumRequestMethods tweaksSubscribe = + ElectrumRequestMethods._(tweaksSubscribeMethod); /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. - static const ElectrumRequestMethods relayFee = - ElectrumRequestMethods._("blockchain.relayfee"); + static const ElectrumRequestMethods relayFee = ElectrumRequestMethods._("blockchain.relayfee"); /// Pass through the masternode announce message to be broadcast by the daemon. static const ElectrumRequestMethods masternodeAnnounceBroadcast = @@ -104,20 +106,18 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("masternode.subscribe"); /// Returns the list of masternodes. - static const ElectrumRequestMethods masternodeList = - ElectrumRequestMethods._("masternode.list"); + static const ElectrumRequestMethods masternodeList = ElectrumRequestMethods._("masternode.list"); /// Returns a diff between two deterministic masternode lists. The result also contains proof data. - static const ElectrumRequestMethods protxDiff = - ElectrumRequestMethods._("protx.diff"); + static const ElectrumRequestMethods protxDiff = ElectrumRequestMethods._("protx.diff"); /// Returns detailed information about a deterministic masternode. - static const ElectrumRequestMethods protxInfo = - ElectrumRequestMethods._("protx.info"); + static const ElectrumRequestMethods protxInfo = ElectrumRequestMethods._("protx.info"); /// Returns a name resolution proof, suitable for low-latency (single round-trip) resolution. static const ElectrumRequestMethods getValueProof = ElectrumRequestMethods._("blockchain.name.get_value_proof"); + @override String toString() { return method; diff --git a/lib/src/provider/service/electrum/params.dart b/lib/src/provider/service/electrum/params.dart index cb245c2..58f608c 100644 --- a/lib/src/provider/service/electrum/params.dart +++ b/lib/src/provider/service/electrum/params.dart @@ -1,13 +1,26 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// Abstract class representing parameters for Electrum requests. -abstract class ElectrumRequestParams { +abstract class BaseElectrumRequestParams { abstract final String method; - List toJson(); +} + +abstract class ElectrumRequestParams implements BaseElectrumRequestParams { + List toParams(); } /// Represents details of an Electrum request, including id, method, and parameters. -class ElectrumRequestDetails { +abstract class BaseElectrumRequestDetails { + const BaseElectrumRequestDetails({required this.method, required this.params}); + + final String method; + final dynamic params; + + List toTCPParams(); + List toWebSocketParams(); +} + +class ElectrumRequestDetails implements BaseElectrumRequestDetails { const ElectrumRequestDetails({ required this.id, required this.method, @@ -15,40 +28,127 @@ class ElectrumRequestDetails { }); final int id; - + @override final String method; + @override final Map params; + @override List toTCPParams() { final param = "${StringUtils.fromJson(params)}\n"; return StringUtils.encode(param); } + @override List toWebSocketParams() { return StringUtils.encode(StringUtils.fromJson(params)); } } /// Abstract class representing an Electrum request with generic result and response types. -abstract class ElectrumRequest - implements ElectrumRequestParams { +abstract class BaseElectrumRequest implements BaseElectrumRequestParams { String? get validate => null; - RESULT onResonse(RESPONSE result) { + BaseElectrumRequestDetails toRequest(int requestId); +} + +abstract class ElectrumRequest extends BaseElectrumRequest + implements ElectrumRequestParams { + RESULT onResponse(RESPONSE result) { return result as RESULT; } + @override ElectrumRequestDetails toRequest(int requestId) { - List inJson = toJson(); - inJson.removeWhere((v) => v == null); - final params = { + final params = toParams(); + params.removeWhere((v) => v == null); + final json = { "jsonrpc": "2.0", "method": method, - "params": inJson, + "params": params, "id": requestId, }; - return ElectrumRequestDetails( - id: requestId, params: params, method: method); + return ElectrumRequestDetails(id: requestId, params: json, method: method); + } +} + +abstract class ElectrumBatchRequestParams implements BaseElectrumRequestParams { + List> toParams(); +} + +class ElectrumBatchRequestDetails implements BaseElectrumRequestDetails { + const ElectrumBatchRequestDetails({ + required this.paramsById, + required this.method, + required this.params, + }); + + final Map> paramsById; + + @override + final String method; + + @override + final List> params; + + @override + List toTCPParams() { + final param = "${StringUtils.fromJson(params)}\n"; + return StringUtils.encode(param); + } + + @override + List toWebSocketParams() { + return StringUtils.encode(StringUtils.fromJson(params)); + } +} + +class ElectrumBatchRequestResult { + final ElectrumBatchRequestDetails request; + final RESULT result; + final int id; + + List? get paramForRequest => request.paramsById[id]; + + ElectrumBatchRequestResult({ + required this.request, + required this.id, + required this.result, + }); +} + +abstract class ElectrumBatchRequest extends BaseElectrumRequest + implements ElectrumBatchRequestParams { + ElectrumBatchRequestResult onResponse( + RESPONSE result, + ElectrumBatchRequestDetails request, + ) { + throw UnimplementedError(); + } + + int finalId = 0; + + @override + BaseElectrumRequestDetails toRequest(int requestId) { + List> params = toParams(); + final paramsById = >{}; + + final json = params.map((e) { + final json = { + "jsonrpc": "2.0", + "method": method, + "params": e, + "id": requestId, + }; + paramsById[requestId] = e; + + requestId++; + return json; + }).toList(); + + finalId = requestId; + + return ElectrumBatchRequestDetails(paramsById: paramsById, params: json, method: method); } } diff --git a/lib/src/provider/service/electrum/request_completer.dart b/lib/src/provider/service/electrum/request_completer.dart new file mode 100644 index 0000000..d238dbc --- /dev/null +++ b/lib/src/provider/service/electrum/request_completer.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +class AsyncRequestCompleter { + AsyncRequestCompleter(this.params); + final Completer completer = Completer(); + final Map params; +} + +class AsyncBehaviorSubject { + AsyncBehaviorSubject(this.params); + final BehaviorSubject subscription = BehaviorSubject(); + final Map params; +} diff --git a/lib/src/provider/service/electrum/service.dart b/lib/src/provider/service/electrum/service.dart index 889b194..826a398 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -1,13 +1,65 @@ +import 'dart:convert'; + import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +enum ConnectionStatus { connected, disconnected, connecting, failed } + +class SocketTask { + SocketTask({ + required this.isSubscription, + required this.request, + this.isBatchRequest = false, + this.completer, + this.subject, + }); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; + final bool isBatchRequest; + final BaseElectrumRequestDetails request; +} + +/// Abstract class for providing JSON-RPC service functionality. +abstract class BitcoinBaseElectrumRPCService { + BitcoinBaseElectrumRPCService(); -/// A mixin for providing JSON-RPC service functionality. -mixin BitcoinBaseElectrumRPCService { /// Represents the URL endpoint for JSON-RPC calls. String get url; - /// Makes an HTTP GET request to the Tron network with the specified [params]. + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params); + + List>? batchSubscribe(ElectrumBatchRequestDetails params); + + /// Makes an HTTP GET request with the specified [params]. /// /// The optional [timeout] parameter sets the maximum duration for the request. - Future> call(ElectrumRequestDetails params, - [Duration? timeout]); + Future call(ElectrumRequestDetails params, [Duration? timeout]); + + Future> batchCall(ElectrumBatchRequestDetails params, [Duration? timeout]); + + bool get isConnected; + void disconnect(); + void reconnect(); + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) { + throw UnimplementedError(); + } +} + +bool isJSONStringCorrect(String source) { + try { + json.decode(source); + return true; + } catch (_) { + return false; + } } diff --git a/lib/src/provider/service/http/http_service.dart b/lib/src/provider/service/http/http_service.dart index 99631b8..be0e94a 100644 --- a/lib/src/provider/service/http/http_service.dart +++ b/lib/src/provider/service/http/http_service.dart @@ -13,6 +13,5 @@ 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}); } diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/provider/transaction_builder/forked_transaction_builder.dart index 862b0f0..6559a39 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/provider/transaction_builder/forked_transaction_builder.dart @@ -1,5 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_base/src/exception/exception.dart'; +import 'package:bitcoin_base/src/provider/transaction_builder/core.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; /// A transaction builder specifically designed for the Bitcoin Cash (BCH) and Bitcoin SV (BSV) networks. @@ -9,7 +9,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 (Bitcoin Cash or Bitcoin SV). /// - [utxosInfo]: List of UtxoWithAddress objects providing information about Unspent Transaction Outputs (UTXOs). @@ -21,7 +21,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// /// Note: The constructor automatically validates the builder by calling the [_validateBuilder] method. class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { - final List outPuts; + final List outputs; final BigInt fee; final BasedUtxoNetwork network; final List utxosInfo; @@ -31,7 +31,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { final BitcoinOrdering inputOrdering; final BitcoinOrdering outputOrdering; ForkedTransactionBuilder( - {required this.outPuts, + {required this.outputs, required this.fee, required this.network, required List utxos, @@ -52,7 +52,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { for (final i in utxosInfo) { i.ownerDetails.address.toAddress(network); } - for (final i in outPuts) { + for (final i in outputs) { if (i is BitcoinOutput) { i.address.toAddress(network); } @@ -74,7 +74,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { utxos: utxos, /// We select transaction outputs - outPuts: outputs, + outputs: outputs, /// Transaction fee /// Ensure that you have accurately calculated the amounts. @@ -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; }); @@ -144,7 +144,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { case P2pkhAddressType.p2pkhwt: case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32wt: - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress().toScriptPubKey(); default: throw BitcoinBasePluginException( "${utxo.utxo.scriptType} does not sudpport on ${network.conf.coinName.name}"); @@ -177,8 +177,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { script: scriptPubKeys, amount: utox.utxo.value, token: utox.utxo.token, - sighash: - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); + sighash: BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); } /// buildP2wshOrP2shScriptSig constructs and returns a script signature (represented as a List of strings) @@ -191,8 +190,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { // /// Returns: /// - List: A List of strings representing the script signature for the P2WSH or P2SH input. - List _buildMiltisigUnlockingScript( - List signedDigest, UtxoWithAddress utx) { + List _buildMiltisigUnlockingScript(List signedDigest, UtxoWithAddress utx) { /// The constructed script signature consists of the signed digest elements followed by /// the script details of the multi-signature address. return ['', ...signedDigest, utx.multiSigAddress.multiSigScript.toHex()]; @@ -216,7 +214,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32: case P2shAddressType.p2pkhInP2sh32wt: - final script = senderPub.toAddress().toScriptPubKey(); + final script = senderPub.toP2pkhAddress().toScriptPubKey(); return [signedDigest, senderPub.toHex(), script.toHex()]; case P2shAddressType.p2pkInP2sh: case P2shAddressType.p2pkInP2shwt: @@ -249,40 +247,37 @@ 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() { - List outputs = outPuts + List builtOutputs = outputs .where((element) => element is! BitcoinBurnableOutput) .map((e) => e.toOutput) .toList(); if (memo != null) { - outputs - .add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); + builtOutputs.add(TxOutput(amount: BigInt.zero, scriptPubKey: _opReturn(memo!))); } if (outputOrdering == BitcoinOrdering.shuffle) { - outputs = outputs..shuffle(); + builtOutputs = builtOutputs..shuffle(); } else if (outputOrdering == BitcoinOrdering.bip69) { - outputs = outputs + builtOutputs = builtOutputs ..sort( (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; }, ); } - return List.unmodifiable(outputs); + return List.unmodifiable(builtOutputs); } /* @@ -329,11 +324,7 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumOutputAmounts}) { if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { throw BitcoinBasePluginException('Sum value of utxo not spending', - details: { - "inputAmount": sumUtxoAmount, - "fee": fee, - "outputAmount": sumOutputAmounts - }); + details: {"inputAmount": sumUtxoAmount, "fee": fee, "outputAmount": sumOutputAmounts}); } if (!isFakeTransaction) { /// sum of token amounts @@ -344,22 +335,16 @@ be retrieved by anyone who examines the blockchain's history. for (final i in sumOfTokenUtxos.entries) { if (sumTokenOutputAmouts[i.key] != i.value) { BigInt amount = sumTokenOutputAmouts[i.key] ?? BigInt.zero; - amount += outPuts + amount += outputs .whereType() .where((element) => element.categoryID == i.key) - .fold( - BigInt.zero, - (previousValue, element) => - previousValue + (element.value ?? BigInt.zero)); + .fold(BigInt.zero, + (previousValue, element) => previousValue + (element.value ?? BigInt.zero)); if (amount != i.value) { throw BitcoinBasePluginException( 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', - details: { - "token": i.key, - "inputValue": i.value, - "outputValue": amount - }); + details: {"token": i.key, "inputValue": i.value, "outputValue": amount}); } } } @@ -368,16 +353,11 @@ be retrieved by anyone who examines the blockchain's history. final token = i.utxo.token!; if (token.hasAmount) continue; if (!token.hasNFT) continue; - final hasOneoutput = outPuts.whereType().any( - (element) => - element.utxoHash == i.utxo.txHash && - element.token.category == token.category); + final hasOneoutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.token.category == token.category); if (hasOneoutput) continue; - final hasBurnableOutput = outPuts - .whereType() - .any((element) => - element.utxoHash == i.utxo.txHash && - element.categoryID == token.category); + final hasBurnableOutput = outputs.whereType().any((element) => + element.utxoHash == i.utxo.txHash && element.categoryID == token.category); if (hasBurnableOutput) continue; throw BitcoinBasePluginException( 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', @@ -401,12 +381,8 @@ be retrieved by anyone who examines the blockchain's history. final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -420,8 +396,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } continue; } @@ -460,11 +435,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -474,24 +447,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -503,12 +471,10 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } @@ -521,8 +487,7 @@ be retrieved by anyone who examines the blockchain's history. } @override - Future buildTransactionAsync( - BitcoinSignerCallBackAsync sign) async { + Future buildTransactionAsync(BitcoinSignerCallBackAsync sign) async { /// build inputs final sortedInputs = _buildInputs(); @@ -550,11 +515,9 @@ be retrieved by anyone who examines the blockchain's history. sumOutputAmounts: sumOutputAmounts); /// create new transaction with inputs and outputs and isSegwit transaction or not - final transaction = - BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); + final transaction = BtcTransaction(inputs: inputs, outputs: outputs, hasSegwit: false); - const int sighash = - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; + const int sighash = BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; /// Well, now let's do what we want for each input for (int i = 0; i < inputs.length; i++) { @@ -564,24 +527,19 @@ be retrieved by anyone who examines the blockchain's history. final script = _buildInputScriptPubKeys(indexUtxo); /// We generate transaction digest for current input - final digest = - _generateTransactionDigest(script, i, indexUtxo, transaction); + final digest = _generateTransactionDigest(script, i, indexUtxo, transaction); /// handle multisig address if (indexUtxo.isMultiSig()) { final multiSigAddress = indexUtxo.multiSigAddress; int sumMultiSigWeight = 0; final mutlsiSigSignatures = []; - for (int ownerIndex = 0; - ownerIndex < multiSigAddress.signers.length; - ownerIndex++) { + for (int ownerIndex = 0; ownerIndex < multiSigAddress.signers.length; ownerIndex++) { /// now we need sign the transaction digest - final sig = await sign(digest, indexUtxo, - multiSigAddress.signers[ownerIndex].publicKey, sighash); + final sig = + await sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); if (sig.isEmpty) continue; - for (int weight = 0; - weight < multiSigAddress.signers[ownerIndex].weight; - weight++) { + for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; weight++) { if (mutlsiSigSignatures.length >= multiSigAddress.threshold) { break; } @@ -593,18 +551,15 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( - "some multisig signature does not exist"); + throw const BitcoinBasePluginException("some multisig signature does not exist"); } - _addScripts( - input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); + _addScripts(input: inputs[i], signatures: mutlsiSigSignatures, utxo: indexUtxo); continue; } /// now we need sign the transaction digest - final sig = - await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); + final sig = await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/provider/transaction_builder/transaction_builder.dart index a20130d..aace073 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/provider/transaction_builder/transaction_builder.dart @@ -22,7 +22,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// /// Note: The constructor automatically validates the builder by calling the [_validateBuilder] method. class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { - final List outPuts; + final List outputs; final BigInt fee; final BasedUtxoNetwork network; final List utxosInfo; @@ -31,8 +31,12 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final bool isFakeTransaction; final BitcoinOrdering inputOrdering; final BitcoinOrdering outputOrdering; + final List? inputPrivKeyInfos; + final List? vinOutpoints; + bool _hasSilentPayment = false; + BitcoinTransactionBuilder({ - required this.outPuts, + required this.outputs, required this.fee, required this.network, required List utxos, @@ -41,6 +45,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { this.memo, this.enableRBF = false, this.isFakeTransaction = false, + this.inputPrivKeyInfos, + this.vinOutpoints, }) : utxosInfo = utxos { _validateBuilder(); } @@ -52,17 +58,16 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { "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(); + 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 BitcoinBasePluginException("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. i.ownerDetails.address.toAddress(network); } - for (final i in outPuts) { + for (final i in outputs) { if (i is BitcoinOutput) { /// Verify each output for its association with this network's address. Raise an exception if the address is incorrect. i.address.toAddress(network); @@ -73,18 +78,21 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// This method is used to create a dummy transaction, /// allowing us to obtain the size of the original transaction /// before conducting the actual transaction. This helps us estimate the transaction cost - static int estimateTransactionSize( - {required List utxos, - required List outputs, - required BasedUtxoNetwork network, - String? memo, - bool enableRBF = false}) { + static int estimateTransactionSize({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + bool enableRBF = false, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) { final transactionBuilder = BitcoinTransactionBuilder( /// Now, we provide the UTXOs we want to spend. utxos: utxos, /// We select transaction outputs - outPuts: outputs, + outputs: outputs, /* Transaction fee Ensure that you have accurately calculated the amounts. @@ -110,6 +118,9 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// We consider the transaction to be fake so that it doesn't check the amounts /// and doesn't generate errors when determining the transaction size. isFakeTransaction: true, + + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, ); /// 64 byte schnorr signature length @@ -120,8 +131,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { const String fakeECDSASignatureBytes = "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; - final transaction = transactionBuilder - .buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { + final transaction = + transactionBuilder.buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { if (utxo.utxo.isP2tr()) { return fakeSchnorSignaturBytes; } else { @@ -131,8 +142,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// Now we need the size of the transaction. If the transaction is a SegWit transaction, /// we use the getVSize method; otherwise, we use the getSize method to obtain the transaction size - final size = - transaction.hasSegwit ? transaction.getVSize() : transaction.getSize(); + final size = transaction.hasSegwit ? transaction.getVSize() : transaction.getSize(); return size; } @@ -175,16 +185,12 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { switch (utxo.utxo.scriptType) { case P2shAddressType.p2wshInP2sh: if (isTaproot) { - return multiSigAAddr - .toP2wshInP2shAddress(network: network) - .toScriptPubKey(); + return multiSigAAddr.toP2wshInP2shAddress(network: network).toScriptPubKey(); } return script; case SegwitAddresType.p2wsh: if (isTaproot) { - return multiSigAAddr - .toP2wshAddress(network: network) - .toScriptPubKey(); + return multiSigAAddr.toP2wshAddress(network: network).toScriptPubKey(); } return script; case P2shAddressType.p2pkhInP2sh: @@ -193,8 +199,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { } return script; default: - throw BitcoinBasePluginException( - "unsuported multi-sig type ${utxo.utxo.scriptType}"); + throw BitcoinBasePluginException("unsuported multi-sig type ${utxo.utxo.scriptType}"); } } @@ -206,31 +211,35 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { if (isTaproot) { return senderPub.toP2wshAddress().toScriptPubKey(); } - return senderPub.toP2wshScript(); + return senderPub.toP2wshRedeemScript(); case P2pkhAddressType.p2pkh: - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress().toScriptPubKey(); case SegwitAddresType.p2wpkh: if (isTaproot) { - return senderPub.toSegwitAddress().toScriptPubKey(); + return senderPub.toP2wpkhAddress().toScriptPubKey(); } - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress().toScriptPubKey(); case SegwitAddresType.p2tr: - return senderPub.toTaprootAddress().toScriptPubKey(); + return senderPub + .toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true) + .toScriptPubKey(); + case SegwitAddresType.mweb: + return Script(script: []); case P2shAddressType.p2pkhInP2sh: if (isTaproot) { return senderPub.toP2pkhInP2sh().toScriptPubKey(); } - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress().toScriptPubKey(); case P2shAddressType.p2wpkhInP2sh: if (isTaproot) { return senderPub.toP2wpkhInP2sh().toScriptPubKey(); } - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toP2pkhAddress().toScriptPubKey(); case P2shAddressType.p2wshInP2sh: if (isTaproot) { return senderPub.toP2wshInP2sh().toScriptPubKey(); } - return senderPub.toP2wshScript(); + return senderPub.toP2wshRedeemScript(); case P2shAddressType.p2pkInP2sh: if (isTaproot) { return senderPub.toP2pkInP2sh().toScriptPubKey(); @@ -255,13 +264,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { // /// 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