From 7a82c6922f2506aeb36a0fb1eb22f5feeb58cb56 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:44:57 +0330 Subject: [PATCH 01/62] v4.8.0 Update dependencies --- CHANGELOG.md | 4 +++ example/pubspec.lock | 30 +++++++++---------- example/pubspec.yaml | 2 +- lib/src/bitcoin/address/core.dart | 2 +- lib/src/bitcoin/address/legacy_address.dart | 6 ++-- lib/src/bitcoin/address/network_address.dart | 2 +- lib/src/bitcoin/address/segwit_address.dart | 4 +-- .../bitcoin/address/utils/address_utils.dart | 10 +++---- lib/src/bitcoin/script/input.dart | 2 +- lib/src/bitcoin/script/op_code/tools.dart | 4 +-- lib/src/bitcoin/script/sequence.dart | 6 ++-- lib/src/bitcoin/script/transaction.dart | 2 +- lib/src/cash_token/cash_token.dart | 18 +++++------ lib/src/crypto/keypair/ec_public.dart | 2 +- lib/src/exception/exception.dart | 10 +++---- lib/src/models/network.dart | 8 ++--- .../api_provider/electrum_api_provider.dart | 17 +++++------ lib/src/provider/models/config.dart | 4 +-- .../provider/models/fee_rate/fee_rate.dart | 2 +- lib/src/provider/models/multisig_script.dart | 12 ++++---- lib/src/provider/models/utxo_details.dart | 6 ++-- .../forked_transaction_builder.dart | 20 ++++++------- .../transaction_builder.dart | 26 ++++++++-------- pubspec.yaml | 6 ++-- 24 files changed, 103 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c89343..dc41c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.8.0 + +* Update dependencies + ## 4.7.0 * Update dependencies diff --git a/example/pubspec.lock b/example/pubspec.lock index d7800dd..9cabb7b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,15 +15,15 @@ packages: path: ".." relative: true source: path - version: "4.7.0" + version: "4.8.0" blockchain_utils: dependency: "direct main" description: name: blockchain_utils - sha256: aebc3a32b927b34f638817c4bfdb85f86a97e6ad35f0cd962660b0c6e8d5c56b + sha256: ebb6c336ba0120de0982c50d8bc597cb494a530bd22bd462895bb5cebde405af url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.4.0" boolean_selector: dependency: transitive description: @@ -110,18 +110,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -150,18 +150,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: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" web: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ca4fb33..2fad8b2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: path: ../ # blockchain_utils: # path: ../../blockchain_utils - blockchain_utils: ^3.3.0 + blockchain_utils: ^3.4.0 http: ^1.2.0 dev_dependencies: diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index 1211396..51a04e8 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -9,7 +9,7 @@ abstract class BitcoinAddressType implements Enumerate { /// Factory method to create a BitcoinAddressType enum value from a name or value. static BitcoinAddressType fromValue(String value) { return values.firstWhere((element) => element.value == value, - orElse: () => throw BitcoinBasePluginException( + orElse: () => throw DartBitcoinPluginException( 'Invalid BitcoinAddressType: $value')); } diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index c3952cf..30b1445 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -14,7 +14,7 @@ abstract class LegacyAddress implements BitcoinBaseAddress { final decode = _BitcoinAddressUtils.decodeLagacyAddressWithNetworkAndType( address: address, type: type, network: network); if (decode == null) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "Invalid ${network.conf.coinName} address"); } _addressProgram = decode; @@ -63,7 +63,7 @@ class P2shAddress extends LegacyAddress { @override String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "network does not support ${type.value} address"); } return super.toAddress(network); @@ -111,7 +111,7 @@ 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"); + throw const DartBitcoinPluginException("Invalid secp256k1 public key"); } publicHex = publicKey; } diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index e1c4307..69950da 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -107,7 +107,7 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { address, network, validateNetworkHRP: validateNetworkPrefix); if (decodeAddress == null) { - throw BitcoinBasePluginException("Invalid ${network.value} address."); + throw DartBitcoinPluginException("Invalid ${network.value} address."); } return BitcoinCashAddress._(decodeAddress, address); } diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index a68574f..039395d 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -6,7 +6,7 @@ abstract class SegwitAddress implements BitcoinBaseAddress { required BasedUtxoNetwork network, required this.segwitVersion}) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "network does not support ${type.value} address"); } addressProgram = _BitcoinAddressUtils.toSegwitProgramWithVersionAndNetwork( @@ -30,7 +30,7 @@ abstract class SegwitAddress implements BitcoinBaseAddress { @override String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "network does not support ${type.value} address"); } return _BitcoinAddressUtils.segwitToAddress( diff --git a/lib/src/bitcoin/address/utils/address_utils.dart b/lib/src/bitcoin/address/utils/address_utils.dart index f6d2baa..af88a82 100644 --- a/lib/src/bitcoin/address/utils/address_utils.dart +++ b/lib/src/bitcoin/address/utils/address_utils.dart @@ -89,7 +89,7 @@ class _BitcoinAddressUtils { final convert = SegwitBech32Decoder.decode(network.p2wpkhHrp, address); final witnessVersion = convert.item1; if (witnessVersion != version) { - throw const BitcoinBasePluginException("Invalid segwit version"); + throw const DartBitcoinPluginException("Invalid segwit version"); } return BytesUtils.toHexString(convert.item2); } @@ -137,7 +137,7 @@ class _BitcoinAddressUtils { if (network.supportedAddress.contains(address.type)) { return address; } - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "${network.value} does not support ${address.type.value} address"); } @@ -158,7 +158,7 @@ class _BitcoinAddressUtils { } baseAddress ??= toLegacy(address, network); if (baseAddress == null) { - throw const BitcoinBasePluginException("Invalid Bitcoin address"); + throw const DartBitcoinPluginException("Invalid Bitcoin address"); } return validateAddress(baseAddress, network); } @@ -180,7 +180,7 @@ class _BitcoinAddressUtils { } // ignore: empty_catches } catch (e) {} - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid Bitcoin address program length (program length should be 32 or 20 bytes)"); } @@ -276,7 +276,7 @@ class _BitcoinAddressUtils { required BitcoinAddressType type, required BasedUtxoNetwork network}) { if (!network.supportedAddress.contains(type)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "${network.value} does not support ${type.value} address type"); } if (network is BitcoinCashNetwork) { diff --git a/lib/src/bitcoin/script/input.dart b/lib/src/bitcoin/script/input.dart index b55b68a..c9d0aee 100644 --- a/lib/src/bitcoin/script/input.dart +++ b/lib/src/bitcoin/script/input.dart @@ -63,7 +63,7 @@ class TxInput { List inpHash = txInputRaw.sublist(cursor, cursor + 32).reversed.toList(); if (inpHash.isEmpty) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Input transaction hash not found. Probably malformed raw transaction"); } List outputN = diff --git a/lib/src/bitcoin/script/op_code/tools.dart b/lib/src/bitcoin/script/op_code/tools.dart index 9bb44e5..8d17869 100644 --- a/lib/src/bitcoin/script/op_code/tools.dart +++ b/lib/src/bitcoin/script/op_code/tools.dart @@ -19,14 +19,14 @@ List opPushData(String hexData) { writeUint32LE(lengthBytes.length, lengthBytes); return List.from([0x4e, ...lengthBytes, ...dataBytes]); } else { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Data too large. Cannot push into script"); } } List pushInteger(int integer) { if (integer < 0) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'Integer is currently required to be positive.'); } diff --git a/lib/src/bitcoin/script/sequence.dart b/lib/src/bitcoin/script/sequence.dart index f202eff..920212f 100644 --- a/lib/src/bitcoin/script/sequence.dart +++ b/lib/src/bitcoin/script/sequence.dart @@ -13,7 +13,7 @@ class Sequence { {required this.seqType, required this.value, this.isTypeBlock = true}) { if (seqType == BitcoinOpCodeConst.TYPE_RELATIVE_TIMELOCK && (value < 1 || value > mask16)) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'Sequence should be between 1 and 65535'); } } @@ -39,13 +39,13 @@ class Sequence { return IntUtils.toBytes(seq, length: 4, byteOrder: Endian.little); } - throw const BitcoinBasePluginException("Invalid seqType"); + throw const DartBitcoinPluginException("Invalid seqType"); } /// Returns the appropriate integer for a script; e.g. for relative timelocks int forScript() { if (seqType == BitcoinOpCodeConst.TYPE_REPLACE_BY_FEE) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "RBF is not to be included in a script."); } int scriptIntiger = value; diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index da9b409..380067f 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -159,7 +159,7 @@ class BtcTransaction { } } else if ((sighash & 0x1f) == BitcoinOpCodeConst.SIGHASH_SINGLE) { if (txInIndex >= tx.outputs.length) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Transaction index is greater than the available outputs"); } diff --git a/lib/src/cash_token/cash_token.dart b/lib/src/cash_token/cash_token.dart index 53f852c..e322257 100644 --- a/lib/src/cash_token/cash_token.dart +++ b/lib/src/cash_token/cash_token.dart @@ -63,7 +63,7 @@ class CashTokenCapability { final int intCapability = _getCapability(bitfield); return values.firstWhere((element) => element.value == intCapability); } on StateError { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid CashToken NFT Capability"); } } @@ -73,7 +73,7 @@ class CashTokenCapability { try { return values.firstWhere((element) => element.name == name); } on StateError { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid CashToken NFT Capability Name"); } } @@ -273,30 +273,30 @@ class CashToken { List? commitment, required int bitfield}) { if (!CashTokenUtils.isValidBitfield(bitfield)) { - throw const BitcoinBasePluginException("Invalid bitfield"); + throw const DartBitcoinPluginException("Invalid bitfield"); } if (CashTokenUtils.hasAmount(bitfield) && amount == null) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid cash token: the bitfield indicates an amount, but the amount is null."); } if (amount != null) { if (amount < BigInt.zero || amount > CashTokenUtils.maxTokenAmount) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid amount. Amount must be between zero and 99."); } } if (!StringUtils.isHexBytes(category)) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid category hexadecimal bytes."); } final toBytes = BytesUtils.fromHexString(category); if (toBytes.length != CashTokenUtils.idBytesLength) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid category. The category should consist of 32 bytes."); } if (CashTokenUtils.hasCommitmentLength(bitfield) && (commitment == null || commitment.isEmpty)) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Invalid cash token: the bitfield indicates an commitment, but the commitment is null or empty."); } return CashToken.noValidate( @@ -336,7 +336,7 @@ class CashToken { amount < BigInt.zero || amount > CashTokenUtils.maxTokenAmount || CashTokenUtils.hasCommitmentLength(bitfield) && commitment.isEmpty) { - throw const BitcoinBasePluginException('Invalid cash token'); + throw const DartBitcoinPluginException('Invalid cash token'); } return Tuple( CashToken( diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index 03f85b2..b33e29f 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -9,7 +9,7 @@ class ECPublic { factory ECPublic.fromBip32(Bip32PublicKey publicKey) { if (publicKey.curveType != EllipticCurveTypes.secp256k1) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "invalid public key curve for bitcoin"); } return ECPublic._(publicKey); diff --git a/lib/src/exception/exception.dart b/lib/src/exception/exception.dart index f06d838..09652ba 100644 --- a/lib/src/exception/exception.dart +++ b/lib/src/exception/exception.dart @@ -1,9 +1,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; -class BitcoinBasePluginException extends BlockchainUtilsException { - @override - final String message; - @override - final Map? details; - const BitcoinBasePluginException(this.message, {this.details}); +class DartBitcoinPluginException extends BlockchainUtilsException { + const DartBitcoinPluginException(String message, + {Map? details}) + : super(message, details: details); } diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index 89e32b3..67c1277 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -282,7 +282,7 @@ class DashNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( + String get p2wpkhHrp => throw const DartBitcoinPluginException( "DashNetwork network does not support P2WPKH/P2WSH"); /// Checks if the current network is the mainnet. @@ -341,7 +341,7 @@ class DogecoinNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( + String get p2wpkhHrp => throw const DartBitcoinPluginException( "DogecoinNetwork network does not support P2WPKH/P2WSH"); /// Checks if the current network is the mainnet. @@ -409,7 +409,7 @@ 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( + String get p2wpkhHrp => throw const DartBitcoinPluginException( "network does not support p2wpkh HRP"); String get networkHRP => conf.params.p2pkhStdHrp!; @@ -470,7 +470,7 @@ class PepeNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const BitcoinBasePluginException( + String get p2wpkhHrp => throw const DartBitcoinPluginException( "DogecoinNetwork network does not support P2WPKH/P2WSH"); /// Checks if the current network is the mainnet. diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index 9cd90ec..835cc2d 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -21,16 +21,15 @@ class ElectrumApiProvider { 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'] ?? ""; + final error = data["error"]; + if (error != null) { + final code = int.tryParse(error["code"]?.toString() ?? ""); + final message = error['message'] ?? ""; throw RPCError( - errorCode: code, - message: message, - data: data["error"]?["data"], - request: data["request"] ?? request.params, - ); + errorCode: code, + message: message, + request: data["request"] ?? request.params, + details: error); } return data["result"]; diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 9369398..44ebefb 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -66,7 +66,7 @@ class APIConfig { baseUrl = BtcApiConst.blockCypherLitecoinBaseUri; break; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "blockcypher does not support ${network.conf.coinName.name}, u must use your own provider"); } @@ -92,7 +92,7 @@ class APIConfig { baseUrl = BtcApiConst.mempoolBaseURL; break; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "mempool does not support ${network.conf.coinName.name}"); } diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 09c6312..8064aaa 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -99,7 +99,7 @@ BigInt _parseMempoolFees(dynamic data) { } else if (data is int) { return BigInt.from((data * kb)); } else { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "cannot parse mempool fees excepted double, string got ${data.runtimeType}"); } } diff --git a/lib/src/provider/models/multisig_script.dart b/lib/src/provider/models/multisig_script.dart index 657f912..7a8aed6 100644 --- a/lib/src/provider/models/multisig_script.dart +++ b/lib/src/provider/models/multisig_script.dart @@ -51,7 +51,7 @@ class MultiSignatureAddress { BitcoinBaseAddress toP2wshAddress({required BasedUtxoNetwork network}) { if (network is! LitecoinNetwork && network is! BitcoinNetwork) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "${network.conf.coinName.name} Bitcoin forks that do not support Segwit. use toP2shAddress"); } return P2wshAddress.fromScript(script: multiSigScript); @@ -66,7 +66,7 @@ class MultiSignatureAddress { BitcoinBaseAddress toP2shAddress( [P2shAddressType addressType = P2shAddressType.p2pkhInP2sh]) { if (!legacySupportP2shTypes.contains(addressType)) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "invalid p2sh type please use one of them ${legacySupportP2shTypes.map((e) => "$e").join(", ")}"); } @@ -94,7 +94,7 @@ class MultiSignatureAddress { case P2shAddressType.p2pkhInP2sh32wt: return toP2shAddress(addressType as P2shAddressType); default: - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "invalid multisig address type. use of of them [BitcoinAddressType.p2wsh, BitcoinAddressType.p2wshInP2sh, BitcoinAddressType.p2pkhInP2sh]"); } } @@ -116,15 +116,15 @@ class MultiSignatureAddress { final sumWeight = signers.fold(0, (sum, signer) => sum + signer.weight); if (threshold > 16 || threshold < 1) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The threshold should be between 1 and 16'); } if (sumWeight > 16) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The total weight of the owners should not exceed 16'); } if (sumWeight < threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( 'The total weight of the signatories should reach the threshold'); } final multiSigScript = ['OP_$threshold']; diff --git a/lib/src/provider/models/utxo_details.dart b/lib/src/provider/models/utxo_details.dart index a6ecb98..4917e98 100644 --- a/lib/src/provider/models/utxo_details.dart +++ b/lib/src/provider/models/utxo_details.dart @@ -48,11 +48,11 @@ class UtxoWithAddress { ECPublic public() { if (isMultiSig()) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Cannot access public key in multi-signature address"); } if (ownerDetails._publicKey == null) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Cannot access public key in watch only address; use UtxoAddressDetails constractor instead `UtxoAddressDetails.watchOnly`"); } return ECPublic.fromHex(ownerDetails._publicKey!); @@ -64,7 +64,7 @@ class UtxoWithAddress { MultiSignatureAddress get multiSigAddress => isMultiSig() ? ownerDetails._multiSigAddress! - : throw const BitcoinBasePluginException( + : throw const DartBitcoinPluginException( "The address is not associated with a multi-signature setup"); } diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/provider/transaction_builder/forked_transaction_builder.dart index 862b0f0..542c208 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/provider/transaction_builder/forked_transaction_builder.dart @@ -46,7 +46,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { void _validateBuilder() { if (network is! BitcoinCashNetwork && network is! BitcoinSVNetwork) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "invalid network. use ForkedTransactionBuilder for BitcoinCashNetwork and BSVNetwork otherwise use BitcoinTransactionBuilder"); } for (final i in utxosInfo) { @@ -125,7 +125,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { case P2shAddressType.p2pkhInP2sh32wt: return script; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "unsuported multi-sig type ${utxo.utxo.scriptType} for ${network.conf.coinName.name}"); } } @@ -146,7 +146,7 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { case P2shAddressType.p2pkhInP2sh32wt: return senderPub.toAddress().toScriptPubKey(); default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "${utxo.utxo.scriptType} does not sudpport on ${network.conf.coinName.name}"); } } @@ -225,7 +225,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi final script = senderPub.toRedeemScript(); return [signedDigest, script.toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( 'Cannot send from this type of address ${utx.utxo.scriptType}'); } } @@ -328,7 +328,7 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumUtxoAmount, required BigInt sumOutputAmounts}) { if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { - throw BitcoinBasePluginException('Sum value of utxo not spending', + throw DartBitcoinPluginException('Sum value of utxo not spending', details: { "inputAmount": sumUtxoAmount, "fee": fee, @@ -353,7 +353,7 @@ be retrieved by anyone who examines the blockchain's history. previousValue + (element.value ?? BigInt.zero)); if (amount != i.value) { - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', details: { "token": i.key, @@ -379,7 +379,7 @@ be retrieved by anyone who examines the blockchain's history. element.utxoHash == i.utxo.txHash && element.categoryID == token.category); if (hasBurnableOutput) continue; - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', details: {"category id": token.category}); } @@ -420,7 +420,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } continue; @@ -503,7 +503,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } @@ -593,7 +593,7 @@ be retrieved by anyone who examines the blockchain's history. } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/provider/transaction_builder/transaction_builder.dart index a20130d..07587c6 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/provider/transaction_builder/transaction_builder.dart @@ -48,14 +48,14 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// validate network and address suport before create transaction void _validateBuilder() { if (network is BitcoinCashNetwork || network is BitcoinSVNetwork) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "invalid network for BitcoinCashNetwork and BSVNetwork use ForkedTransactionBuilder"); } final token = utxosInfo.any((element) => element.utxo.token != null); final tokenInput = outPuts.whereType(); final burn = outPuts.whereType(); if (token || tokenInput.isNotEmpty || burn.isNotEmpty) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "Cash Token only work on Bitcoin cash network"); } for (final i in utxosInfo) { @@ -193,7 +193,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { } return script; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "unsuported multi-sig type ${utxo.utxo.scriptType}"); } } @@ -237,7 +237,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { } return senderPub.toRedeemScript(); } - throw const BitcoinBasePluginException("invalid bitcoin address type"); + throw const DartBitcoinPluginException("invalid bitcoin address type"); } /// generateTransactionDigest generates and returns a transaction digest for a given input in the context of a Bitcoin @@ -314,7 +314,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final p2wsh = P2wshAddress.fromScript(script: script); return [p2wsh.toScriptPubKey().toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "Invalid p2sh nested segwit type ${utxo.utxo.scriptType.value}"); } } @@ -327,7 +327,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final script = senderPub.toSegwitAddress().toScriptPubKey(); return [script.toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "Invalid p2sh nested segwit type ${utxo.utxo.scriptType.value}"); } } @@ -353,7 +353,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi case P2shAddressType.p2wpkhInP2sh: return [signedDigest, senderPub.toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "invalid segwit address type ${utx.utxo.scriptType.value}"); } } else { @@ -369,7 +369,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi final script = senderPub.toRedeemScript(); return [signedDigest, script.toHex()]; default: - throw BitcoinBasePluginException( + throw DartBitcoinPluginException( "invalid address type ${utx.utxo.scriptType.value}"); } } @@ -494,7 +494,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi /// We will check whether you have spent the correct amounts or not if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { - throw const BitcoinBasePluginException('Sum value of utxo not spending'); + throw const DartBitcoinPluginException('Sum value of utxo not spending'); } /// create new transaction with inputs and outputs and isSegwit transaction or not @@ -552,7 +552,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } _addUnlockScriptScript( @@ -615,7 +615,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } continue; @@ -656,7 +656,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi /// We will check whether you have spent the correct amounts or not if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { - throw const BitcoinBasePluginException('Sum value of utxo not spending'); + throw const DartBitcoinPluginException('Sum value of utxo not spending'); } /// create new transaction with inputs and outputs and isSegwit transaction or not @@ -714,7 +714,7 @@ that demonstrate the right to spend the bitcoins associated with the correspondi } } if (sumMultiSigWeight != multiSigAddress.threshold) { - throw const BitcoinBasePluginException( + throw const DartBitcoinPluginException( "some multisig signature does not exist"); } _addUnlockScriptScript( diff --git a/pubspec.yaml b/pubspec.yaml index b86dce4..f1c3442 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bitcoin_base description: A versatile library for Bitcoin, Dogecoin, Litecoin, Dash, BSV, and BCH. Supports P2PKH, P2SH, P2WPKH, P2WSH, P2TR, with advanced creation, signing, and spending capabilities. -version: 4.7.0 +version: 4.8.0 homepage: "https://github.com/mrtnetwork/bitcoin_base" repository: "https://github.com/mrtnetwork/bitcoin_base" Author: mrhaydari.t@gmail.com @@ -16,14 +16,14 @@ environment: dependencies: - blockchain_utils: ^3.3.0 + blockchain_utils: ^3.4.0 # blockchain_utils: # path: ../blockchain_utils dev_dependencies: - test: ^1.25.5 + test: ^1.25.8 lints: ^4.0.0 flutter_lints: ^4.0.0 From d42a8be8d2197eee029bf94a27d87fed6f52cb63 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:02:35 +0330 Subject: [PATCH 02/62] Update network_address.dart --- lib/src/bitcoin/address/network_address.dart | 115 +++++++++---------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index 69950da..b15aa3c 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -2,104 +2,105 @@ part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; /// An abstract class representing a forked address for a specific network. abstract class BitcoinNetworkAddress { - const BitcoinNetworkAddress(); + const BitcoinNetworkAddress._( + {required this.address, + required this.network, + required this.baseAddress}); /// The underlying Bitcoin base address. - abstract final BitcoinBaseAddress baseAddress; + final BitcoinBaseAddress baseAddress; /// Converts the address to a string representation for the specified network [T]. - String toAddress([T? network]) { - return network == null ? address : baseAddress.toAddress(network); + String toAddress([T? updateNetwork]) { + return updateNetwork == null + ? address + : baseAddress.toAddress(updateNetwork); } /// The type of the Bitcoin address. BitcoinAddressType get type => baseAddress.type; /// The string representation of the address. - abstract final String address; + final String address; + + final T network; } /// A concrete implementation of [BitcoinNetworkAddress] for Bitcoin network. class BitcoinAddress extends BitcoinNetworkAddress { - const BitcoinAddress._(this.baseAddress, this.address); + const BitcoinAddress._( + BitcoinBaseAddress baseAddress, String address, BitcoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinAddress(String address, {BitcoinNetwork network = BitcoinNetwork.mainnet}) { return BitcoinAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory BitcoinAddress.fromBaseAddress(BitcoinBaseAddress address, - {DashNetwork network = DashNetwork.mainnet}) { + {BitcoinNetwork network = BitcoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinAddress._( + baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Doge network. class DogeAddress extends BitcoinNetworkAddress { - const DogeAddress._(this.baseAddress, this.address); + const DogeAddress._( + BitcoinBaseAddress baseAddress, String address, DogecoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory DogeAddress(String address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { return DogeAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory DogeAddress.fromBaseAddress(BitcoinBaseAddress address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DogeAddress._(baseAddress, baseAddress.toAddress(network)); + return DogeAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Pepecoin network. class PepeAddress extends BitcoinNetworkAddress { - const PepeAddress._(this.baseAddress, this.address); + const PepeAddress._( + BitcoinBaseAddress baseAddress, String address, PepeNetwork network) + : super._(address: address, network: network, baseAddress: baseAddress); factory PepeAddress(String address, {PepeNetwork network = PepeNetwork.mainnet}) { return PepeAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory PepeAddress.fromBaseAddress(BitcoinBaseAddress address, {PepeNetwork network = PepeNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return PepeAddress._(baseAddress, baseAddress.toAddress(network)); + return PepeAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Litecoin network. class LitecoinAddress extends BitcoinNetworkAddress { - LitecoinAddress._(this.baseAddress, this.address); + const LitecoinAddress._( + BitcoinBaseAddress baseAddress, String address, LitecoinNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory LitecoinAddress(String address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { return LitecoinAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory LitecoinAddress.fromBaseAddress(BitcoinBaseAddress address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return LitecoinAddress._(baseAddress, baseAddress.toAddress(network)); + return LitecoinAddress._( + baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for Bitcoin cash network. class BitcoinCashAddress extends BitcoinNetworkAddress { - const BitcoinCashAddress._(this.baseAddress, this.address); + const BitcoinCashAddress._(BitcoinBaseAddress baseAddress, String address, + BitcoinCashNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinCashAddress(String address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet, bool validateNetworkPrefix = false}) { @@ -109,61 +110,55 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { if (decodeAddress == null) { throw DartBitcoinPluginException("Invalid ${network.value} address."); } - return BitcoinCashAddress._(decodeAddress, address); + return BitcoinCashAddress._(decodeAddress, address, network); } factory BitcoinCashAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinCashAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinCashAddress._( + baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; @override - String toAddress([BitcoinCashNetwork? network, String? prefix]) { + String toAddress([BitcoinCashNetwork? updateNetwork, String? prefix]) { if (prefix != null) { return BchAddrConverter.convert(address, prefix, null); } - return super.toAddress(network); + return super.toAddress(updateNetwork); } } /// A concrete implementation of [BitcoinNetworkAddress] for Dash network. class DashAddress extends BitcoinNetworkAddress { - const DashAddress._(this.baseAddress, this.address); + const DashAddress._( + BitcoinBaseAddress baseAddress, String address, DashNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory DashAddress(String address, {DashNetwork network = DashNetwork.mainnet}) { return DashAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory DashAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DashAddress._(baseAddress, baseAddress.toAddress(network)); + return DashAddress._(baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } /// A concrete implementation of [BitcoinNetworkAddress] for bitcoinSV network. -class BitcoinSVAddress extends BitcoinNetworkAddress { - const BitcoinSVAddress._(this.baseAddress, this.address); +class BitcoinSVAddress extends BitcoinNetworkAddress { + const BitcoinSVAddress._( + BitcoinBaseAddress baseAddress, String address, BitcoinSVNetwork network) + : super._(address: address, baseAddress: baseAddress, network: network); factory BitcoinSVAddress(String address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { return BitcoinSVAddress._( - _BitcoinAddressUtils.decodeAddress(address, network), address); + _BitcoinAddressUtils.decodeAddress(address, network), address, network); } factory BitcoinSVAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinSVAddress._(baseAddress, baseAddress.toAddress(network)); + return BitcoinSVAddress._( + baseAddress, baseAddress.toAddress(network), network); } - @override - final BitcoinBaseAddress baseAddress; - @override - final String address; } From 9bfdd2fba2a4cd4d87ea53069ad35a13f239efd0 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:18:49 +0330 Subject: [PATCH 03/62] fix ElectrumEstimateFee --- .../transfer_from_7_account_to_6_accout_example.dart | 5 +++++ example/lib/global/transfer_to_8_account_example.dart | 5 +++++ .../provider/electrum_methods/methods/estimate_fee.dart | 8 +++++--- 3 files changed, 15 insertions(+), 3 deletions(-) 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..295d906 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 @@ -123,6 +123,11 @@ void main() async { /// get network fee esmtimate (fee per kilobyte) final networkEstimate = await provider.request(ElectrumEstimateFee()); + /// the daemon does not have enough information to make an estimate + if (networkEstimate == null) { + return; + } + /// Convert kilobytes to bytes, multiply by the transaction size, and the result yields the transaction fees. final fee = BigInt.from(transactionSize) * (networkEstimate ~/ BigInt.from(1000)); diff --git a/example/lib/global/transfer_to_8_account_example.dart b/example/lib/global/transfer_to_8_account_example.dart index 90f21f7..6830feb 100644 --- a/example/lib/global/transfer_to_8_account_example.dart +++ b/example/lib/global/transfer_to_8_account_example.dart @@ -102,6 +102,11 @@ void main() async { /// get network fee esmtimate (kb/s) final networkEstimate = await provider.request(ElectrumEstimateFee()); + /// the daemon does not have enough information to make an estimate + if (networkEstimate == null) { + return; + } + /// kb to bytes and mul with transaction size and now we have fee final fee = BigInt.from(estimateSize) * (networkEstimate ~/ BigInt.from(1000)); diff --git a/lib/src/provider/electrum_methods/methods/estimate_fee.dart b/lib/src/provider/electrum_methods/methods/estimate_fee.dart index 9e3b2f5..859dfd8 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -3,7 +3,7 @@ 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 { +class ElectrumEstimateFee extends ElectrumRequest { ElectrumEstimateFee({this.numberOfBlock = 2}); /// The number of blocks to target for confirmation. @@ -20,7 +20,9 @@ class ElectrumEstimateFee extends ElectrumRequest { /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResonse(result) { - return BtcUtils.toSatoshi(result.toString()).abs(); + BigInt? onResonse(result) { + final fee = BtcUtils.toSatoshi(result.toString()); + if (fee.isNegative) return null; + return fee; } } From a2edeceb2cc39e3d056440fbc4a1b41e178d3fe0 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:17:48 +0330 Subject: [PATCH 04/62] v4.9.0 Correct Bitcoin address network configuration. Resolve issue with Electrum fee estimation results. --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc41c5e..1e6a3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 4.9.0 + +* Correct Bitcoin address network configuration. +* Resolve issue with Electrum fee estimation results. + + ## 4.8.0 * Update dependencies diff --git a/pubspec.yaml b/pubspec.yaml index f1c3442..359c7ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bitcoin_base description: A versatile library for Bitcoin, Dogecoin, Litecoin, Dash, BSV, and BCH. Supports P2PKH, P2SH, P2WPKH, P2WSH, P2TR, with advanced creation, signing, and spending capabilities. -version: 4.8.0 +version: 4.9.0 homepage: "https://github.com/mrtnetwork/bitcoin_base" repository: "https://github.com/mrtnetwork/bitcoin_base" Author: mrhaydari.t@gmail.com From 5c844aee9ab3b3f53cbe45b19655928892b59d7f Mon Sep 17 00:00:00 2001 From: Sandro Simas Date: Mon, 14 Oct 2024 20:02:00 -0300 Subject: [PATCH 05/62] Adding Omni XEP support --- lib/src/models/network.dart | 54 ++++++++++++++++++++++++++++++++++++- pubspec.yaml | 10 ++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index 67c1277..ac324ea 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -48,7 +48,8 @@ abstract class BasedUtxoNetwork implements Enumerate { BitcoinCashNetwork.testnet, BitcoinSVNetwork.mainnet, BitcoinSVNetwork.testnet, - PepeNetwork.mainnet + PepeNetwork.mainnet, + OmniXepNetwork.mainnet, ]; static BasedUtxoNetwork fromName(String name) { @@ -493,3 +494,54 @@ class PepeNetwork implements BasedUtxoNetwork { return [Bip44Coins.pepecoinTestnet, Bip49Coins.pepecoinTestnet]; } } + +/// Class representing a Dogecoin network, implementing the `BasedUtxoNetwork` abstract class. +class OmniXepNetwork implements BasedUtxoNetwork { + /// Mainnet configuration with associated `CoinConf`. + static const OmniXepNetwork mainnet = + OmniXepNetwork._("omniXepMainnet", CoinsConf.omniXepMainNet); + + /// Overrides the `conf` property from `BasedUtxoNetwork` with the associated `CoinConf`. + @override + final CoinConf conf; + + /// Constructor for creating a Dogecoin network with a specific configuration. + const OmniXepNetwork._(this.value, this.conf); + + @override + final String value; + + /// Retrieves the Wallet Import Format (WIF) version bytes from the associated `CoinConf`. + @override + List get wifNetVer => conf.params.wifNetVer!; + + /// Retrieves the Pay-to-Public-Key-Hash (P2PKH) version bytes from the associated `CoinConf`. + @override + List get p2pkhNetVer => conf.params.p2pkhNetVer!; + + /// Retrieves the Pay-to-Script-Hash (P2SH) version bytes from the associated `CoinConf`. + @override + List get p2shNetVer => conf.params.p2shNetVer!; + + /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. + @override + String get p2wpkhHrp => throw const DartBitcoinPluginException( + "Omni XEP network does not support P2WPKH/P2WSH"); + + /// Checks if the current network is the mainnet. + @override + bool get isMainnet => true; + + @override + final List supportedAddress = const [ + P2shAddressType.p2wpkhInP2sh, + ]; + + @override + List get coins { + if (isMainnet) { + return [Bip49Coins.omniXep]; + } + return []; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 359c7ef..0538952 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,15 +11,17 @@ topics: - bitcoincash - dash +publish_to: 'none' + environment: sdk: '>=2.15.0 <4.0.0' dependencies: - blockchain_utils: ^3.4.0 - - # blockchain_utils: - # path: ../blockchain_utils + # blockchain_utils: ^3.4.0 + + blockchain_utils: + path: ../blockchain_utils dev_dependencies: From 3b7db06c7c3d1312be462fec3d2550b1e617ee65 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:20:30 +0330 Subject: [PATCH 06/62] 4.9.1 Resolved issue with transaction deserialization (Issue #9) --- CHANGELOG.md | 4 ++++ example/pubspec.lock | 16 ++++++++-------- lib/src/bitcoin/script/script.dart | 3 +-- pubspec.yaml | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e6a3cf..a086184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.9.1 + +* Resolved issue with transaction deserialization (Issue #9) + ## 4.9.0 * Correct Bitcoin address network configuration. diff --git a/example/pubspec.lock b/example/pubspec.lock index 9cabb7b..14f2df9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,7 +15,7 @@ packages: path: ".." relative: true source: path - version: "4.8.0" + version: "4.9.0" blockchain_utils: dependency: "direct main" description: @@ -60,10 +60,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -94,10 +94,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_parser: dependency: transitive description: @@ -251,10 +251,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" 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/lib/src/bitcoin/script/script.dart b/lib/src/bitcoin/script/script.dart index 7c46945..5228776 100644 --- a/lib/src/bitcoin/script/script.dart +++ b/lib/src/bitcoin/script/script.dart @@ -46,8 +46,7 @@ class Script { 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)); int dataSize = viAndSize.item1; int size = viAndSize.item2; final lastIndex = (index + size + dataSize) > scriptBytes.length diff --git a/pubspec.yaml b/pubspec.yaml index 359c7ef..7f21203 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bitcoin_base description: A versatile library for Bitcoin, Dogecoin, Litecoin, Dash, BSV, and BCH. Supports P2PKH, P2SH, P2WPKH, P2WSH, P2TR, with advanced creation, signing, and spending capabilities. -version: 4.9.0 +version: 4.9.1 homepage: "https://github.com/mrtnetwork/bitcoin_base" repository: "https://github.com/mrtnetwork/bitcoin_base" Author: mrhaydari.t@gmail.com @@ -23,7 +23,7 @@ dependencies: dev_dependencies: - test: ^1.25.8 + test: ^1.25.5 lints: ^4.0.0 flutter_lints: ^4.0.0 From 81082e26c668a8e1561e3f8da2d214dc15306448 Mon Sep 17 00:00:00 2001 From: Sandro Simas Date: Fri, 18 Oct 2024 16:11:24 -0300 Subject: [PATCH 07/62] Small improvement --- lib/src/models/network.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index ac324ea..3280519 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -525,8 +525,7 @@ class OmniXepNetwork implements BasedUtxoNetwork { /// Retrieves the Human-Readable Part (HRP) for Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses. @override - String get p2wpkhHrp => throw const DartBitcoinPluginException( - "Omni XEP network does not support P2WPKH/P2WSH"); + String get p2wpkhHrp => conf.params.p2wpkhHrp!; /// Checks if the current network is the mainnet. @override @@ -534,6 +533,7 @@ class OmniXepNetwork implements BasedUtxoNetwork { @override final List supportedAddress = const [ + SegwitAddresType.p2wpkh, P2shAddressType.p2wpkhInP2sh, ]; From 7929af9c44e66e9542aa48915eb08014b8390aae Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:52:10 +0330 Subject: [PATCH 08/62] fix empty segwit deserialization --- lib/src/bitcoin/script/transaction.dart | 41 +++++++++++++++---------- test/p2wsh_test.dart | 6 ++++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 380067f..8ad7206 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -83,7 +83,7 @@ class BtcTransaction { } cursor += 2; } - final vi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); + final vi = IntUtils.decodeVarint(rawtx.sublist(cursor)); cursor += vi.item2; List inputs = []; @@ -96,7 +96,7 @@ class BtcTransaction { } List outputs = []; - final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); + final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor)); cursor += viOut.item2; for (int index = 0; index < viOut.item1; index++) { final inp = @@ -106,25 +106,32 @@ class BtcTransaction { } List witnesses = []; if (hasSegwit) { - for (int n = 0; n < inputs.length; n++) { - final wVi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); - cursor += wVi.item2; - List witnessesTmp = []; - for (int n = 0; n < wVi.item1; n++) { - List witness = []; - final wtVi = IntUtils.decodeVarint(rawtx.sublist(cursor, cursor + 9)); - if (wtVi.item1 != 0) { - witness = rawtx.sublist( - cursor + wtVi.item2, cursor + wtVi.item1 + wtVi.item2); + if (cursor + 4 < rawtx.length) { + // in this case the tx contains wintness data. + for (int n = 0; n < inputs.length; n++) { + final wVi = IntUtils.decodeVarint(rawtx.sublist(cursor)); + cursor += wVi.item2; + List witnessesTmp = []; + for (int n = 0; n < wVi.item1; n++) { + List witness = []; + final wtVi = IntUtils.decodeVarint(rawtx.sublist(cursor)); + if (wtVi.item1 != 0) { + witness = rawtx.sublist( + cursor + wtVi.item2, cursor + wtVi.item1 + wtVi.item2); + } + cursor += wtVi.item1 + wtVi.item2; + witnessesTmp.add(BytesUtils.toHexString(witness)); } - cursor += wtVi.item1 + wtVi.item2; - witnessesTmp.add(BytesUtils.toHexString(witness)); - } - witnesses.add(TxWitnessInput(stack: witnessesTmp)); + witnesses.add(TxWitnessInput(stack: witnessesTmp)); + } } } - List lock = rawtx.sublist(cursor, cursor + 4); + List? lock; + if ((rawtx.length - cursor) >= 4) { + lock = rawtx.sublist(cursor, cursor + 4); + } + print("lock $lock"); return BtcTransaction( inputs: inputs, outputs: outputs, diff --git a/test/p2wsh_test.dart b/test/p2wsh_test.dart index 1b78659..deeed33 100644 --- a/test/p2wsh_test.dart +++ b/test/p2wsh_test.dart @@ -2,6 +2,12 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:test/test.dart'; void main() { + final sx = + "020000000001019361bb41042ba1b84bfd0adf549fd71d0100d523c96f235c0303a820ebe97b6f00000000000100000002f3e0010000000000225120eeef21c790e883138874de40105c2900e95d03b5f2964f6164afde5dfacec01d33e4e902000000001600142d8db3142cf97e996977fed9aec74021395c036900000000"; + final tx = BtcTransaction.fromRaw(sx); + print(tx.inputs.length); + print(tx.serialize() == sx); + return; group("P2WSH", () { final sk1 = ECPrivate.fromWif( "cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo", From 940518a1ec9d2a04de0dc40f45dfd1adac719739 Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:52:32 +0330 Subject: [PATCH 09/62] remove junk code --- lib/src/bitcoin/script/transaction.dart | 1 - test/p2wsh_test.dart | 6 ------ 2 files changed, 7 deletions(-) diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 8ad7206..28e8333 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -131,7 +131,6 @@ class BtcTransaction { if ((rawtx.length - cursor) >= 4) { lock = rawtx.sublist(cursor, cursor + 4); } - print("lock $lock"); return BtcTransaction( inputs: inputs, outputs: outputs, diff --git a/test/p2wsh_test.dart b/test/p2wsh_test.dart index deeed33..1b78659 100644 --- a/test/p2wsh_test.dart +++ b/test/p2wsh_test.dart @@ -2,12 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:test/test.dart'; void main() { - final sx = - "020000000001019361bb41042ba1b84bfd0adf549fd71d0100d523c96f235c0303a820ebe97b6f00000000000100000002f3e0010000000000225120eeef21c790e883138874de40105c2900e95d03b5f2964f6164afde5dfacec01d33e4e902000000001600142d8db3142cf97e996977fed9aec74021395c036900000000"; - final tx = BtcTransaction.fromRaw(sx); - print(tx.inputs.length); - print(tx.serialize() == sx); - return; group("P2WSH", () { final sk1 = ECPrivate.fromWif( "cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo", From bedd81e8f5e457465a8581e112489861bac3bc43 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:10:01 -0300 Subject: [PATCH 10/62] feat: worker changes --- .../global/old_examples/bitcoin_example.dart | 100 +++---- .../spending_single_type.dart | 109 +++----- .../multi_sig_transactions.dart | 39 +-- .../transaction_builder_example.dart | 34 +-- .../electrum/electrum_ssl_service.dart | 107 +++++--- .../electrum/electrum_tcp_service.dart | 97 +++++-- .../electrum/electrum_websocket_service.dart | 39 ++- .../electrum/request_completer.dart | 7 - example/pubspec.lock | 90 +++++- example/pubspec.yaml | 4 +- lib/bitcoin_base.dart | 4 + lib/src/bitcoin/address/address.dart | 2 +- lib/src/bitcoin/address/core.dart | 5 +- lib/src/bitcoin/address/derivations.dart | 216 +++++++++++++++ lib/src/bitcoin/address/legacy_address.dart | 99 ++++--- lib/src/bitcoin/address/network_address.dart | 16 +- lib/src/bitcoin/address/segwit_address.dart | 87 ++++-- lib/src/bitcoin/address/util.dart | 71 +++++ lib/src/bitcoin/amount/amount.dart | 5 + lib/src/bitcoin/amount/utils.dart | 33 +++ lib/src/bitcoin/script/transaction.dart | 21 +- lib/src/bitcoin/silent_payments/address.dart | 70 +++-- lib/src/crypto/keypair/ec_private.dart | 14 +- lib/src/crypto/keypair/ec_public.dart | 4 + .../provider/api_provider/api_provider.dart | 13 +- .../api_provider/electrum_api_provider.dart | 93 +++++-- .../provider/electrum_methods/methods.dart | 1 + .../electrum_methods/methods/add_peer.dart | 2 +- .../methods/block_headers.dart | 8 +- .../electrum_methods/methods/broad_cast.dart | 4 +- .../methods/donate_address.dart | 2 +- .../methods/electrum_version.dart | 2 +- .../methods/estimate_fee.dart | 2 +- .../electrum_methods/methods/get_balance.dart | 2 +- .../methods/get_fee_histogram.dart | 5 +- .../electrum_methods/methods/get_history.dart | 2 +- .../electrum_methods/methods/get_mempool.dart | 2 +- .../electrum_methods/methods/get_merkle.dart | 5 +- .../methods/get_transaction.dart | 36 ++- .../electrum_methods/methods/get_unspet.dart | 11 +- .../methods/get_value_proof.dart | 2 +- .../electrum_methods/methods/header.dart | 2 +- .../methods/headers_subscribe.dart | 21 +- .../electrum_methods/methods/id_from_pos.dart | 2 +- .../masternode_announce_broadcast.dart | 2 +- .../methods/masternode_list.dart | 5 +- .../methods/masternode_subscribe.dart | 2 +- .../electrum_methods/methods/ping.dart | 2 +- .../electrum_methods/methods/protx_diff.dart | 2 +- .../electrum_methods/methods/protx_info.dart | 2 +- .../electrum_methods/methods/relay_fee.dart | 2 +- .../methods/scripthash_unsubscribe.dart | 2 +- .../methods/server_banner.dart | 2 +- .../methods/server_features.dart | 2 +- .../methods/server_peer_subscribe.dart | 5 +- .../electrum_methods/methods/status.dart | 7 +- .../methods/tweaks_subscribe.dart | 98 +++++++ lib/src/provider/models/config.dart | 28 +- .../models/electrum/electrum_utxo.dart | 26 +- .../provider/models/fee_rate/fee_rate.dart | 79 ++++-- .../provider/service/electrum/electrum.dart | 4 + .../electrum/electrum_ssl_service.dart | 258 ++++++++++++++++++ .../electrum/electrum_tcp_service.dart | 258 ++++++++++++++++++ .../electrum/electrum_websocket_service.dart | 85 ++++++ .../provider/service/electrum/methods.dart | 27 +- lib/src/provider/service/electrum/params.dart | 2 +- .../service/electrum/request_completer.dart | 14 + .../provider/service/electrum/service.dart | 46 +++- .../provider/service/http/http_service.dart | 70 +++++ pubspec.yaml | 7 +- test/encode_decode_transaction_test.dart | 15 +- 71 files changed, 1995 insertions(+), 547 deletions(-) delete mode 100644 example/lib/services_examples/electrum/request_completer.dart create mode 100644 lib/src/bitcoin/address/derivations.dart create mode 100644 lib/src/bitcoin/amount/amount.dart create mode 100644 lib/src/bitcoin/amount/utils.dart create mode 100644 lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart create mode 100644 lib/src/provider/service/electrum/electrum_ssl_service.dart create mode 100644 lib/src/provider/service/electrum/electrum_tcp_service.dart create mode 100644 lib/src/provider/service/electrum/electrum_websocket_service.dart create mode 100644 lib/src/provider/service/electrum/request_completer.dart diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index 3ce647d..a24aaf2 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -2,7 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; /// Calculates the change value based on the sum of all provided values. /// @@ -19,8 +18,7 @@ import 'package:example/services_examples/explorer_service/explorer_service.dart /// Returns: /// - The change value. BigInt _changeValue(BigInt sum, List all) { - final sumAll = all.fold( - BigInt.zero, (previousValue, element) => previousValue + element); + final sumAll = all.fold(BigInt.zero, (previousValue, element) => previousValue + element); final remind = sum - sumAll; if (remind < BigInt.zero) { @@ -61,27 +59,25 @@ void _spendFromP2pkhTo10DifferentType() async { final examplePublicKey2 = examplePrivateKey.getPublic(); /// Define transaction outputs - final out1 = P2pkhAddress.fromAddress( - address: "msxiCJXD2WB43wK2PpTUvoqQLF7ZP98qqM", network: network); + final out1 = + P2pkhAddress.fromAddress(address: "msxiCJXD2WB43wK2PpTUvoqQLF7ZP98qqM", network: network); final out2 = P2trAddress.fromAddress( - address: "tb1plq65drqavf93wf63d8g7d8ypuzaargd5h9d35u05ktrcwxq4a6ss0gpvrt", - network: network); + address: "tb1plq65drqavf93wf63d8g7d8ypuzaargd5h9d35u05ktrcwxq4a6ss0gpvrt", network: network); final out3 = P2wpkhAddress.fromAddress( address: "tb1q3zqgu9j368wgk8u5f9vtmkdwq8geetdxry690d", network: network); - final out4 = P2pkAddress.fromPubkey(pubkey: examplePublicKey.publicKey.toHex()); - final out5 = P2shAddress.fromAddress( - address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); - final out6 = P2shAddress.fromAddress( - address: "2NDAUpeUB1kGAQET8SojF8seXNrk3uudtCb", network: network); - final out7 = P2shAddress.fromAddress( - address: "2NE9CYdxju2iEAfR4FMdKPUcZbnKcfCiLhM", network: network); - final out8 = P2shAddress.fromAddress( - address: "2MwGRf8wNJsaYKdigqPwikPpg9JAT2faaPB", network: network); + final out4 = P2pkAddress.fromPubkey(pubkey: ECPublic.fromBip32(examplePublicKey.publicKey)); + final out5 = + P2shAddress.fromAddress(address: "2N5hVdETdJMwLDxxttfqeWgMuny6K4SYGSc", network: network); + final out6 = + P2shAddress.fromAddress(address: "2NDAUpeUB1kGAQET8SojF8seXNrk3uudtCb", network: network); + final out7 = + P2shAddress.fromAddress(address: "2NE9CYdxju2iEAfR4FMdKPUcZbnKcfCiLhM", network: network); + final out8 = + P2shAddress.fromAddress(address: "2MwGRf8wNJsaYKdigqPwikPpg9JAT2faaPB", network: network); final out9 = P2wshAddress.fromAddress( - address: "tb1qes3upam2nv3rc6s38tqgk0cqh6dlycvk6cjydyvpx9zlumh4h4lsjq26p8", - network: network); - final out10 = P2shAddress.fromAddress( - address: "2N2aRKjTQ3uzgUSLWFQAUDvKLnKCiBfCSAh", network: network); + address: "tb1qes3upam2nv3rc6s38tqgk0cqh6dlycvk6cjydyvpx9zlumh4h4lsjq26p8", network: network); + final out10 = + P2shAddress.fromAddress(address: "2N2aRKjTQ3uzgUSLWFQAUDvKLnKCiBfCSAh", network: network); /// Calculate the change value for the transaction final change = _changeValue( @@ -130,8 +126,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// Create a UTXO using a BitcoinUtxo with specific details utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction - txHash: - "b06f4ed0b49a5092a9ea206553ddc5fc469be694d0d28c95598c653e66cdeb5e", + txHash: "b06f4ed0b49a5092a9ea206553ddc5fc469be694d0d28c95598c653e66cdeb5e", /// Value represents the amount of the UTXO in satoshis. value: BigInt.from(250000), @@ -145,19 +140,16 @@ void _spendFromP2pkhTo10DifferentType() async { /// Include owner details with the public key and address associated with the UTXO ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toP2pkhAddress())), + publicKey: examplePublicKey2.toHex(), address: examplePublicKey2.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "6ff0bdb2966f62f5e202c924e1cab1368b0258833e48986cc0a70fbca624ba93", + txHash: "6ff0bdb2966f62f5e202c924e1cab1368b0258833e48986cc0a70fbca624ba93", value: BigInt.from(812830), vout: 0, scriptType: examplePublicKey2.toP2pkhAddress().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey2.toHex(), - address: examplePublicKey2.toP2pkhAddress())), + publicKey: examplePublicKey2.toHex(), address: examplePublicKey2.toP2pkhAddress())), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder @@ -249,8 +241,8 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// outputs /// make sure pass network to address for validate, before sending create transaction - final out1 = P2pkhAddress.fromAddress( - address: "n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR", network: network); + final out1 = + P2pkhAddress.fromAddress(address: "n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR", network: network); final builder = BitcoinTransactionBuilder( @@ -272,8 +264,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { UtxoWithAddress( utxo: BitcoinUtxo( /// Transaction hash uniquely identifies the referenced transaction - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", /// Value represents the amount of the UTXO in satoshis. value: BtcUtils.toSatoshi("0.001"), @@ -291,8 +282,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toP2pkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 1, scriptType: childKey1PublicKey.toTaprootAddress().type, @@ -302,8 +292,7 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toTaprootAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 2, scriptType: childKey1PublicKey.toP2wpkhAddress().type, @@ -313,53 +302,44 @@ void _spendFrom10DifferentTypeToP2pkh() async { address: childKey1PublicKey.toP2wpkhAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 3, scriptType: examplePublicKey.toP2pkAddress().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkAddress())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 4, scriptType: examplePublicKey.toP2pkInP2sh().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 5, scriptType: examplePublicKey.toP2pkhInP2sh().type, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2pkhInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2pkhInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 6, scriptType: examplePublicKey.toP2wpkhInP2sh().type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails( - publicKey: examplePublicKey.toHex(), - address: examplePublicKey.toP2wpkhInP2sh())), + publicKey: examplePublicKey.toHex(), address: examplePublicKey.toP2wpkhInP2sh())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 7, scriptType: msig.toP2shAddress().type, @@ -371,28 +351,24 @@ void _spendFrom10DifferentTypeToP2pkh() async { multiSigAddress: msig, address: msig.toP2shAddress())), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.001"), vout: 8, scriptType: msig.toP2wshAddress(network: network).type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails.multiSigAddress( - multiSigAddress: msig, - address: msig.toP2wshAddress(network: network))), + multiSigAddress: msig, address: msig.toP2wshAddress(network: network))), UtxoWithAddress( utxo: BitcoinUtxo( - txHash: - "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", + txHash: "05411dce1a1c9e3f44b54413bdf71e7ab3eff1e2f94818a3568c39814c27b258", value: BtcUtils.toSatoshi("0.0015783"), vout: 9, scriptType: msig2.toP2wshInP2shAddress(network: network).type, blockHeight: 0, ), ownerDetails: UtxoAddressDetails.multiSigAddress( - multiSigAddress: msig2, - address: msig2.toP2wshInP2shAddress(network: network))), + multiSigAddress: msig2, address: msig2.toP2wshInP2shAddress(network: network))), ]); /// Build the transaction by invoking the buildTransaction method on the BitcoinTransactionBuilder diff --git a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart index 4f16538..12b0f72 100644 --- a/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart +++ b/example/lib/global/old_examples/spending_with_scripts/spending_single_type.dart @@ -1,16 +1,14 @@ // ignore_for_file: unused_local_variable import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; import 'spending_builders.dart'; // Define the network as the Testnet (used for testing and development purposes). const network = BitcoinNetwork.testnet; -final service = BitcoinApiService(); // Initialize an API provider for interacting with the Testnet's blockchain data. -final api = ApiProvider.fromMempool(network, service); +final api = ApiProvider.fromMempool(network); // In these tutorials, you will learn how to spend various types of UTXOs. // Each method is specific to a type of UTXO. @@ -28,16 +26,15 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // P2WPKH final sender = publicKey.toP2wpkhAddress(); // Read UTXOs of accounts from the BlockCypher API. - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: publicKey.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: publicKey.toHex())); // The total amount of UTXOs that we can spend. final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } // Receive network fees - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); // feeRate.medium, feeRate.high ,feeRate.low P/KB // In this section, we select the transaction outputs; the number and type of addresses are not important @@ -64,8 +61,7 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // Now that we've determined the transaction size, let's calculate the transaction fee // based on the transaction size and the desired fee rate. - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); // We subtract the fee from the total amount of UTXOs to calculate // the actual amount we can spend in this transaction. @@ -74,9 +70,8 @@ Future spendingP2WPKH(ECPrivate sWallet, ECPrivate rWallet) async { // We specify the desired amount for each address. Here, I have divided the desired total // amount by the number of outputs to ensure an equal amount for each. final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); // I use the 'buildP2wpkTransaction' method to create a transaction. @@ -112,15 +107,14 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2WSH ADDRESS final sender = addr.toP2wshAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -133,13 +127,11 @@ Future spendingP2WSH(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2WSHTransaction( receiver: outPutWithValue, @@ -161,15 +153,14 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2PKH final sender = addr.toP2pkhAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -181,13 +172,11 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2pkhTransaction( @@ -205,23 +194,21 @@ Future spendingP2PKH(ECPrivate sWallet, ECPrivate rWallet) async { // Spend P2SH(P2PKH) or P2SH(P2PK): Please note that all input addresses must be of P2SH(P2PKH) or P2SH(P2PK) type; otherwise, the transaction will fail. // This method is for standard 1-1 Multisig P2SH. // For standard n-of-m multi-signature scripts, please refer to the 'multi_sig_transactions.dart' tutorial. -Future spendingP2SHNoneSegwit( - ECPrivate sWallet, ECPrivate rWallet) async { +Future spendingP2SHNoneSegwit(ECPrivate sWallet, ECPrivate rWallet) async { // All the steps are the same as in the first tutorial; // the only difference is the transaction input type, // and we use method `buildP2shNoneSegwitTransaction` to create the transaction. final addr = sWallet.getPublic(); // P2SH(P2PK) final sender = addr.toP2pkInP2sh(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -233,13 +220,11 @@ Future spendingP2SHNoneSegwit( ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2shNoneSegwitTransaction( receiver: outPutWithValue, @@ -263,15 +248,14 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2SH(P2PWKH) final sender = addr.toP2wpkhInP2sh(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -284,13 +268,11 @@ Future spendingP2shSegwit(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); // return; @@ -315,15 +297,14 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { final addr = sWallet.getPublic(); // P2TR address final sender = addr.toTaprootAddress(); - final utxo = await api.getAccountUtxo( - UtxoAddressDetails(address: sender, publicKey: addr.toHex())); + final utxo = + await api.getAccountUtxo(UtxoAddressDetails(address: sender, publicKey: addr.toHex())); final sumOfUtxo = utxo.sumOfUtxosValue(); if (sumOfUtxo == BigInt.zero) { - throw Exception( - "account does not have any unspent transaction or mybe no confirmed"); + throw Exception("account does not have any unspent transaction or mybe no confirmed"); } - final feeRate = await api.getNetworkFeeRate(); + final feeRate = await api.getRecommendedFeeRate(); final prive = sWallet; final recPub = rWallet.getPublic(); @@ -335,13 +316,11 @@ Future spendingP2TR(ECPrivate sWallet, ECPrivate rWallet) async { ]; final transactionSize = BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxo, outputs: outputsAdress, network: network); - final estimateFee = feeRate.getEstimate(transactionSize, - feeRateType: BitcoinFeeRateType.medium); + final estimateFee = feeRate.getEstimate(transactionSize, feeRateType: BitcoinFeeRateType.medium); final canSpend = sumOfUtxo - estimateFee; final outPutWithValue = outputsAdress - .map((e) => BitcoinOutput( - address: e.address, - value: canSpend ~/ BigInt.from(outputsAdress.length))) + .map((e) => + BitcoinOutput(address: e.address, value: canSpend ~/ BigInt.from(outputsAdress.length))) .toList(); final transaction = buildP2trTransaction( diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart index 794deb6..98d0e9c 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart @@ -4,7 +4,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; void main() async { final service = BitcoinApiService(); @@ -13,7 +12,7 @@ void main() async { // select api for read accounts UTXOs and send transaction // Mempool or BlockCypher - final api = ApiProvider.fromMempool(network, service); + final api = ApiProvider.fromMempool(network); final mnemonic = Bip39SeedGenerator(Mnemonic.fromString( "spy often critic spawn produce volcano depart fire theory fog turn retire")) @@ -59,18 +58,14 @@ void main() async { // P2WSH Multisig 4-6 // tb1qxt3c7849m0m6cv3z3s35c3zvdna3my3yz0r609qd9g0dcyyk580sgyldhe - final p2wshMultiSigAddress = - multiSignatureAddress.toP2wshAddress(network: network).toP2pkhAddress(network); + final p2wshMultiSigAddress = multiSignatureAddress.toP2wshAddress(network: network); // p2sh(p2wsh) multisig - final signerP2sh1 = - MultiSignatureSigner(publicKey: public5.toHex(), weight: 1); + final signerP2sh1 = MultiSignatureSigner(publicKey: public5.toHex(), weight: 1); - final signerP2sh2 = - MultiSignatureSigner(publicKey: public6.toHex(), weight: 1); + final signerP2sh2 = MultiSignatureSigner(publicKey: public6.toHex(), weight: 1); - final signerP2sh3 = - MultiSignatureSigner(publicKey: public1.toHex(), weight: 1); + final signerP2sh3 = MultiSignatureSigner(publicKey: public1.toHex(), weight: 1); final MultiSignatureAddress p2shMultiSignature = MultiSignatureAddress( threshold: 2, @@ -78,9 +73,7 @@ void main() async { ); // P2SH(P2WSH) miltisig 2-3 // 2N8co8bth9CNKtnWGfHW6HuUNgnNPNdpsMj - final p2shMultisigAddress = p2shMultiSignature - .toP2wshInP2shAddress(network: network) - .toP2pkhAddress(network); + final p2shMultisigAddress = p2shMultiSignature.toP2wshInP2shAddress(network: network); // P2TR final exampleAddr2 = public2.toTaprootAddress(); @@ -149,11 +142,9 @@ void main() async { utxos: utxos, outputs: [ BitcoinOutput( - address: p2shMultiSignature.toP2wshInP2shAddress(network: network), - value: BigInt.zero), + address: p2shMultiSignature.toP2wshInP2shAddress(network: network), value: BigInt.zero), BitcoinOutput( - address: multiSignatureAddress.toP2wshAddress(network: network), - value: BigInt.zero), + address: multiSignatureAddress.toP2wshAddress(network: network), value: BigInt.zero), BitcoinOutput(address: exampleAddr2, value: BigInt.zero), BitcoinOutput(address: exampleAddr4, value: BigInt.zero) ], @@ -168,7 +159,7 @@ void main() async { // That's my perspective, of course. final blockCypher = ApiProvider.fromBlocCypher(network, service); - final feeRate = await blockCypher.getNetworkFeeRate(); + final feeRate = await blockCypher.getRecommendedFeeRate(); // fee rate inKB // feeRate.medium: 32279 P/KB // feeRate.high: 43009 P/KB @@ -190,12 +181,9 @@ void main() async { address: p2shMultiSignature.toP2wshInP2shAddress(network: network), value: BigInt.from(365449)); final output2 = BitcoinOutput( - address: multiSignatureAddress.toP2wshAddress(network: network), - value: BigInt.from(365449)); - final output3 = - BitcoinOutput(address: exampleAddr2, value: BigInt.from(365448)); - final output4 = - BitcoinOutput(address: exampleAddr4, value: BigInt.from(365448)); + address: multiSignatureAddress.toP2wshAddress(network: network), value: BigInt.from(365449)); + final output3 = BitcoinOutput(address: exampleAddr2, value: BigInt.from(365448)); + final output4 = BitcoinOutput(address: exampleAddr4, value: BigInt.from(365448)); // Well, now it is clear to whom we are going to pay the amount // Now let's create the transaction @@ -233,8 +221,7 @@ void main() async { // I've added a method for signing the transaction as a parameter. // This method sends you the public key for each UTXO, // allowing you to sign the desired input with the associated private key - final transaction = - transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { + final transaction = transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { late ECPrivate key; // ok we have the public key of the current UTXO and we use some conditions to find private key and sign transaction diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart index 851259f..701f72b 100644 --- a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart +++ b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart @@ -1,6 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:example/services_examples/explorer_service/explorer_service.dart'; // spend from 8 different address type to 10 different output void main() async { @@ -116,26 +115,16 @@ void main() async { // now we have 1,174,140 satoshi for spending let do it // we create 10 different output with different address type like (pt2r, p2sh(p2wpkh), p2sh(p2wsh), p2pkh, etc.) // We consider the spendable amount for 10 outputs and divide by 10, each output 117,414 - final output1 = - BitcoinOutput(address: exampleAddr4, value: BigInt.from(117414)); - final output2 = - BitcoinOutput(address: exampleAddr9, value: BigInt.from(117414)); - final output3 = - BitcoinOutput(address: exampleAddr10, value: BigInt.from(117414)); - final output4 = - BitcoinOutput(address: exampleAddr1, value: BigInt.from(117414)); - final output5 = - BitcoinOutput(address: exampleAddr3, value: BigInt.from(117414)); - final output6 = - BitcoinOutput(address: exampleAddr2, value: BigInt.from(117414)); - final output7 = - BitcoinOutput(address: exampleAddr7, value: BigInt.from(117414)); - final output8 = - BitcoinOutput(address: exampleAddr8, value: BigInt.from(117414)); - final output9 = - BitcoinOutput(address: exampleAddr5, value: BigInt.from(117414)); - final output10 = - BitcoinOutput(address: exampleAddr6, value: BigInt.from(117414)); + final output1 = BitcoinOutput(address: exampleAddr4, value: BigInt.from(117414)); + final output2 = BitcoinOutput(address: exampleAddr9, value: BigInt.from(117414)); + final output3 = BitcoinOutput(address: exampleAddr10, value: BigInt.from(117414)); + final output4 = BitcoinOutput(address: exampleAddr1, value: BigInt.from(117414)); + final output5 = BitcoinOutput(address: exampleAddr3, value: BigInt.from(117414)); + final output6 = BitcoinOutput(address: exampleAddr2, value: BigInt.from(117414)); + final output7 = BitcoinOutput(address: exampleAddr7, value: BigInt.from(117414)); + final output8 = BitcoinOutput(address: exampleAddr8, value: BigInt.from(117414)); + final output9 = BitcoinOutput(address: exampleAddr5, value: BigInt.from(117414)); + final output10 = BitcoinOutput(address: exampleAddr6, value: BigInt.from(117414)); // Well, now it is clear to whom we are going to pay the amount // Now let's create the transaction @@ -183,8 +172,7 @@ void main() async { // parameters // utxo infos with owner details // trDigest transaction digest of current UTXO (must be sign with correct privateKey) - final transaction = - transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { + final transaction = transactionBuilder.buildTransaction((trDigest, utxo, publicKey, sighash) { late ECPrivate key; // ok we have the public key of the current UTXO and we use some conditions to find private key and sign transaction diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart index 362c306..1313aa7 100644 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ b/example/lib/services_examples/electrum/electrum_ssl_service.dart @@ -1,41 +1,49 @@ -/// Simple example how to send request to electurm with secure socket +/// Simple example how to send request to electurm with tcp import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; +import 'package:rxdart/rxdart.dart'; -class ElectrumSSLService with BitcoinBaseElectrumRPCService { - ElectrumSSLService._( +class SocketTask { + SocketTask({required this.isSubscription, this.completer, this.subject}); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; +} + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { + ElectrumTCPService._( this.url, SecureSocket channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = _socket!.listen(_onMessage, onError: _onClose, onDone: _onDone); } SecureSocket? _socket; StreamSubscription>? _subscription; final Duration defaultRequestTimeOut; - Map requests = {}; - bool _isDiscounnect = false; + final Map _tasks = {}; - bool get isConnected => _isDiscounnect; + bool _isDisconnected = false; + @override + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.add(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; _socket = null; _subscription?.cancel().catchError((e) {}); @@ -46,50 +54,83 @@ class ElectrumSSLService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } - static Future connect( + static Future connect( String url, { Iterable? protocols, Duration defaultRequestTimeOut = const Duration(seconds: 30), final Duration connectionTimeOut = const Duration(seconds: 30), }) async { final parts = url.split(":"); - final channel = await SecureSocket.connect( - parts[0], - int.parse(parts[1]), - onBadCertificate: (certificate) => true, - ).timeout(connectionTimeOut); - - return ElectrumSSLService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); + final channel = + await SecureSocket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); + + return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(List event) { + void _onMessage(List event) { final Map decode = json.decode(utf8.decode(event)); if (decode.containsKey("id")) { + _finish(decode["id"]!.toString(), decode); final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); + final request = _tasks.remove(id); + request?.completer?.complete(decode); } } + void _finish(String id, Map decode) { + final int id = int.parse(decode["id"]!.toString()); + if (_tasks[id] == null) { + return; + } + + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer!.complete(decode); + } + + final isSubscription = _tasks[id]?.isSubscription ?? false; + if (!isSubscription) { + _tasks.remove(id); + } else { + _tasks[id]?.subject?.add(decode); + } + } + + void _registerSubscription(int id, BehaviorSubject subject) => + _tasks[id] = SocketTask(subject: subject, isSubscription: true); + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + + try { + _registerSubscription(params.id, subscription.subscription); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + void _registerTask(int id, Completer completer) => + _tasks[id] = SocketTask(completer: completer, isSubscription: false); + @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { + final completer = AsyncRequestCompleter(params.params); try { - requests[params.id] = compeleter; + _registerTask(params.id, completer.completer); add(params.toTCPParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); return result; } finally { - requests.remove(params.id); + _tasks.remove(params.id); } } } diff --git a/example/lib/services_examples/electrum/electrum_tcp_service.dart b/example/lib/services_examples/electrum/electrum_tcp_service.dart index 750aad0..efc2506 100644 --- a/example/lib/services_examples/electrum/electrum_tcp_service.dart +++ b/example/lib/services_examples/electrum/electrum_tcp_service.dart @@ -4,38 +4,46 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; +import 'package:rxdart/rxdart.dart'; -class ElectrumTCPService with BitcoinBaseElectrumRPCService { +class SocketTask { + SocketTask({required this.isSubscription, this.completer, this.subject}); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; +} + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { ElectrumTCPService._( this.url, Socket channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = - _socket!.listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = _socket!.listen(_onMessage, onError: _onClose, onDone: _onDone); } Socket? _socket; StreamSubscription>? _subscription; final Duration defaultRequestTimeOut; - Map requests = {}; - bool _isDiscounnect = false; + final Map _tasks = {}; - bool get isConnected => _isDiscounnect; + bool _isDisconnected = false; + @override + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.add(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; _socket = null; _subscription?.cancel().catchError((e) {}); @@ -46,7 +54,8 @@ class ElectrumTCPService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } @@ -57,36 +66,70 @@ class ElectrumTCPService with BitcoinBaseElectrumRPCService { final Duration connectionTimeOut = const Duration(seconds: 30), }) async { final parts = url.split(":"); - final channel = await Socket.connect(parts[0], int.parse(parts[1])) - .timeout(connectionTimeOut); + final channel = await Socket.connect(parts[0], int.parse(parts[1])).timeout(connectionTimeOut); - return ElectrumTCPService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); + return ElectrumTCPService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(List event) { + void _onMessage(List event) { final Map decode = json.decode(utf8.decode(event)); if (decode.containsKey("id")) { + _finish(decode["id"]!.toString(), decode); final int id = int.parse(decode["id"]!.toString()); - final request = requests.remove(id); - request?.completer.complete(decode); + final request = _tasks.remove(id); + request?.completer?.complete(decode); } } + void _finish(String id, Map decode) { + final int id = int.parse(decode["id"]!.toString()); + if (_tasks[id] == null) { + return; + } + + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer!.complete(decode); + } + + final isSubscription = _tasks[id]?.isSubscription ?? false; + if (!isSubscription) { + _tasks.remove(id); + } else { + _tasks[id]?.subject?.add(decode); + } + } + + void _registerSubscription(int id, BehaviorSubject subject) => + _tasks[id] = SocketTask(subject: subject, isSubscription: true); + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + + try { + _registerSubscription(params.id, subscription.subscription); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + void _registerTask(int id, Completer completer) => + _tasks[id] = SocketTask(completer: completer, isSubscription: false); + @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { + final completer = AsyncRequestCompleter(params.params); try { - requests[params.id] = compeleter; - add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); + _registerTask(params.id, completer.completer); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); return result; } finally { - requests.remove(params.id); + _tasks.remove(params.id); } } } diff --git a/example/lib/services_examples/electrum/electrum_websocket_service.dart b/example/lib/services_examples/electrum/electrum_websocket_service.dart index 6ca10a2..2ec9bb7 100644 --- a/example/lib/services_examples/electrum/electrum_websocket_service.dart +++ b/example/lib/services_examples/electrum/electrum_websocket_service.dart @@ -3,40 +3,39 @@ import 'dart:async'; import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:example/services_examples/cross_platform_websocket/core.dart'; -import 'package:example/services_examples/electrum/request_completer.dart'; -class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { +class ElectrumWebSocketService implements BitcoinBaseElectrumRPCService { ElectrumWebSocketService._( this.url, WebSocketCore channel, { this.defaultRequestTimeOut = const Duration(seconds: 30), }) : _socket = channel { - _subscription = channel.stream - .cast() - .listen(_onMessge, onError: _onClose, onDone: _onDone); + _subscription = + channel.stream.cast().listen(_onMessage, onError: _onClose, onDone: _onDone); } WebSocketCore? _socket; StreamSubscription? _subscription; final Duration defaultRequestTimeOut; Map requests = {}; - bool _isDiscounnect = false; + bool _isDisconnected = false; - bool get isConnected => _isDiscounnect; + bool get isConnected => !_isDisconnected; @override final String url; void add(List params) { - if (_isDiscounnect) { - throw StateError("socket has been discounected"); + if (_isDisconnected) { + throw StateError("socket has been disconnected"); } _socket?.sink(params); } void _onClose(Object? error) { - _isDiscounnect = true; + _isDisconnected = true; + _socket?.close(); _socket = null; _subscription?.cancel().catchError((e) {}); _subscription = null; @@ -46,7 +45,8 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { _onClose(null); } - void discounnect() { + @override + void disconnect() { _onClose(null); } @@ -56,14 +56,12 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { Duration defaultRequestTimeOut = const Duration(seconds: 30), final Duration connectionTimeOut = const Duration(seconds: 30), }) async { - final channel = - await WebSocketCore.connect(url, protocols: protocols?.toList()); + final channel = await WebSocketCore.connect(url, protocols: protocols?.toList()); - return ElectrumWebSocketService._(url, channel, - defaultRequestTimeOut: defaultRequestTimeOut); + return ElectrumWebSocketService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); } - void _onMessge(String event) { + void _onMessage(String event) { final Map decode = json.decode(event); if (decode.containsKey("id")) { final int id = int.parse(decode["id"]!.toString()); @@ -73,16 +71,13 @@ class ElectrumWebSocketService with BitcoinBaseElectrumRPCService { } @override - Future> call(ElectrumRequestDetails params, - [Duration? timeout]) async { - final AsyncRequestCompleter compeleter = - AsyncRequestCompleter(params.params); + Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { + final AsyncRequestCompleter compeleter = AsyncRequestCompleter(params.params); try { requests[params.id] = compeleter; add(params.toWebSocketParams()); - final result = await compeleter.completer.future - .timeout(timeout ?? defaultRequestTimeOut); + final result = await compeleter.completer.future.timeout(timeout ?? defaultRequestTimeOut); return result; } finally { requests.remove(params.id); diff --git a/example/lib/services_examples/electrum/request_completer.dart b/example/lib/services_examples/electrum/request_completer.dart deleted file mode 100644 index e6f00dc..0000000 --- a/example/lib/services_examples/electrum/request_completer.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:async'; - -class AsyncRequestCompleter { - AsyncRequestCompleter(this.params); - final Completer> completer = Completer(); - final Map params; -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 318e7a4..0aa5320 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bitcoin_base: dependency: "direct main" description: @@ -19,11 +27,9 @@ packages: blockchain_utils: dependency: "direct main" description: - path: "." - ref: cake-update-v2 - resolved-ref: "2767a54ed2b0a23494e4e96a3fe5b5022b834b70" - url: "https://github.com/cake-tech/blockchain_utils" - source: git + path: "/home/rafael/Working/blockchain_utils/" + relative: false + source: path version: "3.3.0" boolean_selector: dependency: transitive @@ -33,6 +39,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -57,6 +71,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -91,14 +121,22 @@ packages: description: flutter source: sdk version: "0.0.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" http: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" http_parser: dependency: transitive description: @@ -107,6 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: @@ -171,6 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sky_engine: dependency: transitive description: flutter @@ -252,9 +322,9 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 25d16d4..11a339d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,9 +38,7 @@ dependencies: bitcoin_base: path: ../ blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ http: ^1.2.0 dev_dependencies: diff --git a/lib/bitcoin_base.dart b/lib/bitcoin_base.dart index cb559d7..a2c93a8 100644 --- a/lib/bitcoin_base.dart +++ b/lib/bitcoin_base.dart @@ -12,12 +12,16 @@ export 'package:bitcoin_base/src/bitcoin/address/util.dart'; export 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; +export 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; + export 'package:bitcoin_base/src/crypto/crypto.dart'; export 'package:bitcoin_base/src/models/network.dart'; export 'package:bitcoin_base/src/provider/api_provider.dart'; +export 'package:bitcoin_base/src/provider/models/electrum/electrum_utxo.dart'; + export 'package:bitcoin_base/src/utils/utils.dart'; export 'package:bitcoin_base/src/cash_token/cash_token.dart'; diff --git a/lib/src/bitcoin/address/address.dart b/lib/src/bitcoin/address/address.dart index 4dd7d7e..c644cf8 100644 --- a/lib/src/bitcoin/address/address.dart +++ b/lib/src/bitcoin/address/address.dart @@ -10,7 +10,6 @@ library bitcoin_base.address; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; -import 'package:bitcoin_base/src/utils/enumerate.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bitcoin_base/src/utils/script.dart'; part 'core.dart'; @@ -18,3 +17,4 @@ part 'legacy_address.dart'; part 'utils/address_utils.dart'; part 'segwit_address.dart'; part 'network_address.dart'; +part 'derivations.dart'; diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index f5daaba..368e198 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -60,14 +60,13 @@ abstract class BitcoinAddressType implements Enumerate { } abstract class BitcoinBaseAddress { - BitcoinBaseAddress({this.network}); + BitcoinBaseAddress(); BitcoinAddressType get type; - String toAddress([BasedUtxoNetwork? network]); + String toAddress(BasedUtxoNetwork network); Script toScriptPubKey(); String pubKeyHash(); String get addressProgram; - BasedUtxoNetwork? network; static BitcoinBaseAddress fromString( String address, [ diff --git a/lib/src/bitcoin/address/derivations.dart b/lib/src/bitcoin/address/derivations.dart new file mode 100644 index 0000000..303a889 --- /dev/null +++ b/lib/src/bitcoin/address/derivations.dart @@ -0,0 +1,216 @@ +// ignore_for_file: constant_identifier_names +// ignore_for_file: non_constant_identifier_names +part of 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +class BitcoinDerivationInfo { + BitcoinDerivationInfo({ + required this.derivationType, + required String derivationPath, + required this.scriptType, + this.description, + }) : derivationPath = Bip32PathParser.parse(derivationPath); + final BitcoinDerivationType derivationType; + final Bip32Path derivationPath; + final BitcoinAddressType scriptType; + final String? description; + + factory BitcoinDerivationInfo.fromJSON(Map json) { + return BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.values[json['derivationType']], + derivationPath: json['derivationPath'], + scriptType: BitcoinAddressType.values.firstWhere( + (type) => type.toString() == json['scriptType'], + ), + description: json['description'], + ); + } + + Map toJSON() { + return { + 'derivationType': derivationType.index, + 'derivationPath': derivationPath.toString(), + 'scriptType': scriptType.toString(), + 'description': description, + }; + } +} + +enum BitcoinDerivationType { bip39, electrum } + +// Define constant paths +abstract class BitcoinDerivationPaths { + static const String ELECTRUM = "m/0'"; + static const String BIP44 = "m/44'/0'/0'"; + static const String BIP49 = "m/49'/0'/0'"; + static const String BIP84 = "m/84'/0'/0'"; + static const String BIP86 = "m/86'/0'/0'"; + static const String NON_STANDARD = "m/0'"; + + static const String SILENT_PAYMENTS_SCAN = "m/352'/0'/0'/1'"; + static const String SILENT_PAYMENTS_SPEND = "m/352'/0'/0'/0'"; + + static const String LITECOIN = "m/84'/2'/0'"; + + static const String SAMOURAI_BAD_BANK = "m/84'/0'/2147483644'"; + static const String SAMOURAI_WHIRLPOOL_PREMIX = "m/84'/0'/2147483645'"; + static const String SAMOURAI_WHIRLPOOL_POSTMIX = "m/84'/0'/2147483646'"; + static const String SAMOURAI_RICOCHET_LEGACY = "m/44'/0'/2147483647'"; + static const String SAMOURAI_RICOCHET_COMPATIBILITY_SEGWIT = "m/49'/0'/2147483647'"; + static const String SAMOURAI_RICOCHET_NATIVE_SEGWIT = "m/84'/0'/2147483647'"; +} + +abstract class BitcoinDerivationInfos { + static final BitcoinDerivationInfo ELECTRUM = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.electrum, + derivationPath: BitcoinDerivationPaths.ELECTRUM, + description: "Electrum", + scriptType: SegwitAddresType.p2wpkh, + ); + + static final BitcoinDerivationInfo BIP44 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Standard BIP44", + scriptType: P2pkhAddressType.p2pkh, + ); + static final BitcoinDerivationInfo BIP49 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Standard BIP49 compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ); + static final BitcoinDerivationInfo BIP84 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP84, + description: "Standard BIP84 native segwit", + scriptType: SegwitAddresType.p2wpkh, + ); + static final BitcoinDerivationInfo BIP86 = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP86, + description: "Standard BIP86 Taproot", + scriptType: SegwitAddresType.p2tr, + ); + + static final BitcoinDerivationInfo LITECOIN = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.LITECOIN, + description: "Default Litecoin", + scriptType: SegwitAddresType.p2wpkh, + ); + + static final BitcoinDerivationInfo SILENT_PAYMENTS_SCAN = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SILENT_PAYMENTS_SCAN, + description: "Silent Payments Scan", + scriptType: SilentPaymentsAddresType.p2sp, + ); + + static final BitcoinDerivationInfo SILENT_PAYMENTS_SPEND = BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, + description: "Silent Payments Spend", + scriptType: SilentPaymentsAddresType.p2sp, + ); +} + +final Map> BITCOIN_DERIVATIONS = { + BitcoinDerivationType.electrum: [BitcoinDerivationInfos.ELECTRUM], + BitcoinDerivationType.bip39: [ + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Standard BIP44", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Standard BIP49 compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP84, + description: "Standard BIP84 native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP86, + description: "Standard BIP86 Taproot", + scriptType: SegwitAddresType.p2tr, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard legacy", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.NON_STANDARD, + description: "Non-standard native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP44, + description: "Samourai Deposit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.BIP49, + description: "Samourai Deposit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_BAD_BANK, + description: "Samourai Bad Bank (toxic change)", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_PREMIX, + description: "Samourai Whirlpool Pre Mix", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_WHIRLPOOL_POSTMIX, + description: "Samourai Whirlpool Post Mix", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_LEGACY, + description: "Samourai Ricochet legacy", + scriptType: P2pkhAddressType.p2pkh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_COMPATIBILITY_SEGWIT, + description: "Samourai Ricochet compatibility segwit", + scriptType: P2shAddressType.p2wpkhInP2sh, + ), + BitcoinDerivationInfo( + derivationType: BitcoinDerivationType.bip39, + derivationPath: BitcoinDerivationPaths.SAMOURAI_RICOCHET_NATIVE_SEGWIT, + description: "Samourai Ricochet native segwit", + scriptType: SegwitAddresType.p2wpkh, + ), + BitcoinDerivationInfos.LITECOIN, + BitcoinDerivationInfos.SILENT_PAYMENTS_SCAN, + BitcoinDerivationInfos.SILENT_PAYMENTS_SPEND, + ], +}; + +const String ELECTRUM_PATH = BitcoinDerivationPaths.ELECTRUM; diff --git a/lib/src/bitcoin/address/legacy_address.dart b/lib/src/bitcoin/address/legacy_address.dart index c2b9798..f998c15 100644 --- a/lib/src/bitcoin/address/legacy_address.dart +++ b/lib/src/bitcoin/address/legacy_address.dart @@ -9,12 +9,11 @@ abstract class LegacyAddress extends BitcoinBaseAddress { LegacyAddress.fromHash160({ required String h160, required BitcoinAddressType type, - super.network, }) : _addressProgram = _BitcoinAddressUtils.validateAddressProgram(h160, type), super(); LegacyAddress.fromAddress({required String address, required BasedUtxoNetwork network}) - : super(network: network) { + : super() { final decode = _BitcoinAddressUtils.decodeLegacyAddressWithNetworkAndType( address: address, type: type, @@ -28,14 +27,14 @@ abstract class LegacyAddress extends BitcoinBaseAddress { _addressProgram = decode; } - LegacyAddress.fromPubkey({required ECPublic pubkey, super.network}) + LegacyAddress.fromPubkey({required ECPublic pubkey}) : _pubkey = pubkey, _addressProgram = _BitcoinAddressUtils.pubkeyToHash160(pubkey.toHex()); - LegacyAddress.fromRedeemScript({required Script script, super.network}) + LegacyAddress.fromRedeemScript({required Script script}) : _addressProgram = _BitcoinAddressUtils.scriptToHash160(script); - LegacyAddress.fromScriptSig({required Script script, super.network}) { + LegacyAddress.fromScriptSig({required Script script}) { switch (type) { case PubKeyAddressType.p2pk: _signature = script.findScriptParam(0); @@ -79,13 +78,7 @@ abstract class LegacyAddress extends BitcoinBaseAddress { } @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { throw BitcoinBasePluginException("network does not support ${type.value} address"); } @@ -104,11 +97,10 @@ abstract class LegacyAddress extends BitcoinBaseAddress { } class P2shAddress extends LegacyAddress { - static RegExp get regex => RegExp(r'[23M][a-km-zA-HJ-NP-Z1-9]{25,34}'); + static final regex = RegExp(r'[23M][a-km-zA-HJ-NP-Z1-9]{25,34}'); P2shAddress.fromRedeemScript({ required super.script, - super.network, this.type = P2shAddressType.p2pkInP2sh, }) : super.fromRedeemScript(); @@ -120,20 +112,48 @@ class P2shAddress extends LegacyAddress { P2shAddress.fromHash160({ required super.h160, - super.network, this.type = P2shAddressType.p2pkInP2sh, }) : super.fromHash160(type: type); + factory P2shAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + P2shAddressType type = P2shAddressType.p2pkInP2sh, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + final pubkey = ECPublic.fromBip32(bip32.derive(fullPath).publicKey); + + switch (type) { + case P2shAddressType.p2pkInP2sh: + return pubkey.toP2pkInP2sh(); + case P2shAddressType.p2pkhInP2sh: + return pubkey.toP2pkhInP2sh(); + case P2shAddressType.p2wshInP2sh: + return pubkey.toP2wshInP2sh(); + case P2shAddressType.p2wpkhInP2sh: + return pubkey.toP2wpkhInP2sh(); + default: + throw UnimplementedError(); + } + } + + factory P2shAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2wpkhInP2sh(); + } + factory P2shAddress.fromScriptPubkey({ required Script script, - BasedUtxoNetwork? network, type = P2shAddressType.p2pkInP2sh, }) { if (script.getAddressType() is! P2shAddressType) { throw ArgumentError("Invalid scriptPubKey"); } - return P2shAddress.fromHash160(h160: script.findScriptParam(1), network: network, type: type); + return P2shAddress.fromHash160(h160: script.findScriptParam(1), type: type); } @override @@ -155,18 +175,17 @@ class P2shAddress extends LegacyAddress { } class P2pkhAddress extends LegacyAddress { - static RegExp get regex => RegExp(r'[1mnL][a-km-zA-HJ-NP-Z1-9]{25,34}'); + static final regex = RegExp(r'[1mnL][a-km-zA-HJ-NP-Z1-9]{25,34}'); factory P2pkhAddress.fromScriptPubkey({ required Script script, - BasedUtxoNetwork? network, P2pkhAddressType type = P2pkhAddressType.p2pkh, }) { if (script.getAddressType() != P2pkhAddressType.p2pkh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2pkhAddress.fromHash160(h160: script.findScriptParam(2), network: network, type: type); + return P2pkhAddress.fromHash160(h160: script.findScriptParam(2), type: type); } P2pkhAddress.fromAddress({ @@ -175,17 +194,27 @@ class P2pkhAddress extends LegacyAddress { this.type = P2pkhAddressType.p2pkh, }) : super.fromAddress(); - P2pkhAddress.fromHash160({ - required super.h160, - super.network, - this.type = P2pkhAddressType.p2pkh, - }) : super.fromHash160(type: type); + P2pkhAddress.fromHash160({required super.h160, this.type = P2pkhAddressType.p2pkh}) + : super.fromHash160(type: type); - P2pkhAddress.fromScriptSig({ - required super.script, - super.network, - this.type = P2pkhAddressType.p2pkh, - }) : super.fromScriptSig(); + P2pkhAddress.fromScriptSig({required super.script, this.type = P2pkhAddressType.p2pkh}) + : super.fromScriptSig(); + + factory P2pkhAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2pkhAddress(); + } + + factory P2pkhAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2pkhAddress(); + } @override Script toScriptPubKey() { @@ -209,7 +238,7 @@ class P2pkhAddress extends LegacyAddress { class P2pkAddress extends LegacyAddress { static RegExp get regex => RegExp(r'1([A-Za-z0-9]{34})'); - P2pkAddress({required ECPublic publicKey, super.network}) + P2pkAddress({required ECPublic publicKey}) : _pubkeyHex = publicKey.toHex(), super.fromPubkey(pubkey: publicKey); @@ -233,13 +262,7 @@ class P2pkAddress extends LegacyAddress { } @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { return _BitcoinAddressUtils.legacyToAddress( network: network, addressProgram: _BitcoinAddressUtils.pubkeyToHash160(_pubkeyHex), diff --git a/lib/src/bitcoin/address/network_address.dart b/lib/src/bitcoin/address/network_address.dart index 8b8fd7f..bed4732 100644 --- a/lib/src/bitcoin/address/network_address.dart +++ b/lib/src/bitcoin/address/network_address.dart @@ -9,7 +9,7 @@ abstract class BitcoinNetworkAddress { /// Converts the address to a string representation for the specified network [T]. String toAddress([T? network]) { - return network == null ? address : baseAddress.toAddress(); + return network == null ? address : baseAddress.toAddress(network); } /// The type of the Bitcoin address. @@ -28,7 +28,7 @@ class BitcoinAddress extends BitcoinNetworkAddress { factory BitcoinAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -45,7 +45,7 @@ class DogeAddress extends BitcoinNetworkAddress { factory DogeAddress.fromBaseAddress(BitcoinBaseAddress address, {DogecoinNetwork network = DogecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DogeAddress._(baseAddress, baseAddress.toAddress()); + return DogeAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -63,7 +63,7 @@ class PepeAddress extends BitcoinNetworkAddress { factory PepeAddress.fromBaseAddress(BitcoinBaseAddress address, {PepeNetwork network = PepeNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return PepeAddress._(baseAddress, baseAddress.toAddress()); + return PepeAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -81,7 +81,7 @@ class LitecoinAddress extends BitcoinNetworkAddress { factory LitecoinAddress.fromBaseAddress(BitcoinBaseAddress address, {LitecoinNetwork network = LitecoinNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return LitecoinAddress._(baseAddress, baseAddress.toAddress()); + return LitecoinAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -108,7 +108,7 @@ class BitcoinCashAddress extends BitcoinNetworkAddress { factory BitcoinCashAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinCashNetwork network = BitcoinCashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinCashAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinCashAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -133,7 +133,7 @@ class DashAddress extends BitcoinNetworkAddress { factory DashAddress.fromBaseAddress(BitcoinBaseAddress address, {DashNetwork network = DashNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return DashAddress._(baseAddress, baseAddress.toAddress()); + return DashAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; @@ -150,7 +150,7 @@ class BitcoinSVAddress extends BitcoinNetworkAddress { factory BitcoinSVAddress.fromBaseAddress(BitcoinBaseAddress address, {BitcoinSVNetwork network = BitcoinSVNetwork.mainnet}) { final baseAddress = _BitcoinAddressUtils.validateAddress(address, network); - return BitcoinSVAddress._(baseAddress, baseAddress.toAddress()); + return BitcoinSVAddress._(baseAddress, baseAddress.toAddress(network)); } @override final BitcoinBaseAddress baseAddress; diff --git a/lib/src/bitcoin/address/segwit_address.dart b/lib/src/bitcoin/address/segwit_address.dart index 740e1ea..b45f4b0 100644 --- a/lib/src/bitcoin/address/segwit_address.dart +++ b/lib/src/bitcoin/address/segwit_address.dart @@ -5,7 +5,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { required String address, required BasedUtxoNetwork network, required this.segwitVersion, - }) : super(network: network) { + }) : super() { addressProgram = _BitcoinAddressUtils.toSegwitProgramWithVersionAndNetwork( address: address, version: segwitVersion, @@ -16,7 +16,6 @@ abstract class SegwitAddress extends BitcoinBaseAddress { SegwitAddress.fromProgram({ required String program, required SegwitAddresType addressType, - super.network, required this.segwitVersion, this.pubkey, }) : addressProgram = _BitcoinAddressUtils.validateAddressProgram(program, addressType), @@ -24,7 +23,6 @@ abstract class SegwitAddress extends BitcoinBaseAddress { SegwitAddress.fromRedeemScript({ required Script script, - super.network, required this.segwitVersion, }) : addressProgram = _BitcoinAddressUtils.segwitScriptToSHA256(script); @@ -34,13 +32,7 @@ abstract class SegwitAddress extends BitcoinBaseAddress { ECPublic? pubkey; @override - String toAddress([BasedUtxoNetwork? network]) { - network ??= this.network; - - if (network == null) { - throw const BitcoinBasePluginException("Network is required"); - } - + String toAddress(BasedUtxoNetwork network) { if (!network.supportedAddress.contains(type)) { throw BitcoinBasePluginException("network does not support ${type.value} address"); } @@ -59,26 +51,43 @@ abstract class SegwitAddress extends BitcoinBaseAddress { } class P2wpkhAddress extends SegwitAddress { - static RegExp get regex => RegExp(r'(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}'); + static final regex = RegExp(r'(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}'); P2wpkhAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); - P2wpkhAddress.fromProgram({required super.program, super.network}) + P2wpkhAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, addressType: SegwitAddresType.p2wpkh, ); - P2wpkhAddress.fromRedeemScript({required super.script, super.network}) + P2wpkhAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); - factory P2wpkhAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2wpkhAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2wpkhAddress(); + } + + factory P2wpkhAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2wpkhAddress(); + } + + factory P2wpkhAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2wpkh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2wpkhAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2wpkhAddress.fromProgram(program: script.findScriptParam(1)); } /// returns the scriptPubKey of a P2WPKH witness script @@ -93,27 +102,43 @@ class P2wpkhAddress extends SegwitAddress { } class P2trAddress extends SegwitAddress { - static RegExp get regex => + static final regex = RegExp(r'(bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'); P2trAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV1); - P2trAddress.fromProgram({required super.program, super.network, super.pubkey}) + P2trAddress.fromProgram({required super.program, super.pubkey}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV1, addressType: SegwitAddresType.p2tr, ); - P2trAddress.fromRedeemScript({required super.script, super.network}) + P2trAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV1); - factory P2trAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2trAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2trAddress(); + } + + factory P2trAddress.fromPath({required Bip32Base bip32, required Bip32Path path}) { + return ECPublic.fromBip32(bip32.derive(path).publicKey).toP2trAddress(); + } + + factory P2trAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2tr) { throw ArgumentError("Invalid scriptPubKey"); } - return P2trAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2trAddress.fromProgram(program: script.findScriptParam(1)); } /// returns the scriptPubKey of a P2TR witness script @@ -128,26 +153,38 @@ class P2trAddress extends SegwitAddress { } class P2wshAddress extends SegwitAddress { - static RegExp get regex => RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{40,80}'); + static final regex = RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{40,80}'); P2wshAddress.fromAddress({required super.address, required super.network}) : super.fromAddress(segwitVersion: _BitcoinAddressUtils.segwitV0); - P2wshAddress.fromProgram({required super.program, super.network}) + P2wshAddress.fromProgram({required super.program}) : super.fromProgram( segwitVersion: _BitcoinAddressUtils.segwitV0, addressType: SegwitAddresType.p2wsh, ); - P2wshAddress.fromRedeemScript({required super.script, super.network}) + P2wshAddress.fromRedeemScript({required super.script}) : super.fromRedeemScript(segwitVersion: _BitcoinAddressUtils.segwitV0); - factory P2wshAddress.fromScriptPubkey({required Script script, BasedUtxoNetwork? network}) { + factory P2wshAddress.fromDerivation({ + required Bip32Base bip32, + required BitcoinDerivationInfo derivationInfo, + required bool isChange, + required int index, + }) { + final fullPath = derivationInfo.derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + return ECPublic.fromBip32(bip32.derive(fullPath).publicKey).toP2wshAddress(); + } + + factory P2wshAddress.fromScriptPubkey({required Script script}) { if (script.getAddressType() != SegwitAddresType.p2wsh) { throw ArgumentError("Invalid scriptPubKey"); } - return P2wshAddress.fromProgram(program: script.findScriptParam(1), network: network); + return P2wshAddress.fromProgram(program: script.findScriptParam(1)); } /// Returns the scriptPubKey of a P2WPKH witness script diff --git a/lib/src/bitcoin/address/util.dart b/lib/src/bitcoin/address/util.dart index 7ec257d..b878886 100644 --- a/lib/src/bitcoin/address/util.dart +++ b/lib/src/bitcoin/address/util.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/src/bitcoin/script/scripts.dart'; import 'package:bitcoin_base/src/utils/utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bitcoin_base/src/bitcoin/address/address.dart'; @@ -26,6 +27,28 @@ class BitcoinAddressUtils { return addressType.toScriptPubKey().toBytes(); } + static String addressFromOutputScript(Script script, BasedUtxoNetwork network) { + try { + switch (script.getAddressType()) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkhInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2tr: + return P2trAddress.fromScriptPubkey(script: script).toAddress(network); + default: + } + } catch (_) {} + + return ''; + } + static String scriptHash(String address, {required BasedUtxoNetwork network}) { final outputScript = addressToOutputScript(address: address, network: network); final parts = BytesUtils.toHexString(QuickCrypto.sha256Hash(outputScript)).split(''); @@ -41,4 +64,52 @@ class BitcoinAddressUtils { return res; } + + static BitcoinAddressType getScriptType(BitcoinBaseAddress type) { + if (type is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (type is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (type is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (type is P2trAddress) { + return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; + } else if (type is SilentPaymentsAddresType) { + return SilentPaymentsAddresType.p2sp; + } else { + return SegwitAddresType.p2wpkh; + } + } + + static int getAccountFromChange(bool isChange) { + return isChange ? 1 : 0; + } + + static BitcoinDerivationInfo getDerivationFromType( + BitcoinAddressType scriptType, { + bool? isElectrum = false, + }) { + switch (scriptType) { + case P2pkhAddressType.p2pkh: + return BitcoinDerivationInfos.BIP44; + case P2shAddressType.p2wpkhInP2sh: + return BitcoinDerivationInfos.BIP49; + case SegwitAddresType.p2wpkh: + if (isElectrum == true) { + return BitcoinDerivationInfos.ELECTRUM; + } else { + return BitcoinDerivationInfos.BIP84; + } + case SegwitAddresType.p2tr: + return BitcoinDerivationInfos.BIP86; + case SegwitAddresType.mweb: + return BitcoinDerivationInfos.BIP86; + case SegwitAddresType.p2wsh: + return BitcoinDerivationInfos.BIP84; + default: + throw Exception("Derivation not available for $scriptType"); + } + } } diff --git a/lib/src/bitcoin/amount/amount.dart b/lib/src/bitcoin/amount/amount.dart new file mode 100644 index 0000000..df7f858 --- /dev/null +++ b/lib/src/bitcoin/amount/amount.dart @@ -0,0 +1,5 @@ +library bitcoin_base.amount; + +import 'package:intl/intl.dart'; + +part 'utils.dart'; diff --git a/lib/src/bitcoin/amount/utils.dart b/lib/src/bitcoin/amount/utils.dart new file mode 100644 index 0000000..8b1f8bc --- /dev/null +++ b/lib/src/bitcoin/amount/utils.dart @@ -0,0 +1,33 @@ +part of 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; + +class BitcoinAmountUtils { + static const bitcoinAmountLength = 8; + static const bitcoinAmountDivider = 100000000; + static final bitcoinAmountFormat = NumberFormat() + ..maximumFractionDigits = bitcoinAmountLength + ..minimumFractionDigits = 1; + + static double cryptoAmountToDouble({required num amount, required num divider}) => + amount / divider; + + static String bitcoinAmountToString({required int amount}) => + bitcoinAmountFormat.format(cryptoAmountToDouble( + amount: amount, + divider: bitcoinAmountDivider, + )); + + static double bitcoinAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); + + static int stringDoubleToBitcoinAmount(String amount) { + int result = 0; + + try { + result = (double.parse(amount) * bitcoinAmountDivider).round(); + } catch (e) { + result = 0; + } + + return result; + } +} diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 3ea3079..4169ddc 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -148,16 +148,17 @@ class BtcTransaction { mwebBytes = rawtx.sublist(cursor, rawtx.length - 4); cursor = rawtx.length - 4; } - List lock = rawtx.sublist(cursor, cursor + 4); + List lock = rawtx.sublist(rawtx.length - 4); return BtcTransaction( - inputs: inputs, - outputs: outputs, - witnesses: witnesses, - hasSegwit: hasSegwit, - canReplaceByFee: canReplaceByFee, - mwebBytes: mwebBytes, - version: version, - lock: lock); + inputs: inputs, + outputs: outputs, + witnesses: witnesses, + hasSegwit: hasSegwit, + canReplaceByFee: canReplaceByFee, + mwebBytes: mwebBytes, + version: version, + lock: lock, + ); } /// returns the transaction input's digest that is to be signed according. @@ -242,7 +243,7 @@ class BtcTransaction { if (mwebBytes != null) { data.add(mwebBytes!); } - data.add(locktime); + data.add([0, 0, 0, 0]); return data.toBytes(); } diff --git a/lib/src/bitcoin/silent_payments/address.dart b/lib/src/bitcoin/silent_payments/address.dart index e622851..4c3afea 100644 --- a/lib/src/bitcoin/silent_payments/address.dart +++ b/lib/src/bitcoin/silent_payments/address.dart @@ -2,10 +2,6 @@ // ignore_for_file: non_constant_identifier_names part of 'package:bitcoin_base/src/bitcoin/silent_payments/silent_payments.dart'; -const SCAN_PATH = "m/352'/1'/0'/1'/0"; - -const SPEND_PATH = "m/352'/1'/0'/0'/0"; - class SilentPaymentOwner extends SilentPaymentAddress { final ECPrivate b_scan; final ECPrivate b_spend; @@ -16,7 +12,6 @@ class SilentPaymentOwner extends SilentPaymentAddress { required super.B_spend, required this.b_scan, required this.b_spend, - super.network, }) : super(); factory SilentPaymentOwner.fromPrivateKeys({ @@ -30,33 +25,38 @@ class SilentPaymentOwner extends SilentPaymentAddress { b_spend: b_spend, B_scan: b_scan.getPublic(), B_spend: b_spend.getPublic(), - network: network, version: version ?? 0, ); } - factory SilentPaymentOwner.fromHd(Bip32Slip10Secp256k1 bip32, {String? hrp, int? version}) { - final scanDerivation = bip32.derivePath(SCAN_PATH); - final spendDerivation = bip32.derivePath(SPEND_PATH); + factory SilentPaymentOwner.fromBip32(Bip32Slip10Secp256k1 bip32, {int? version}) { + final scanDerivation = bip32.derive( + Bip32PathParser.parse(BitcoinDerivationPaths.SILENT_PAYMENTS_SCAN), + ); + final spendDerivation = bip32.derive( + Bip32PathParser.parse(BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND), + ); return SilentPaymentOwner( b_scan: ECPrivate(scanDerivation.privateKey), b_spend: ECPrivate(spendDerivation.privateKey), B_scan: ECPublic.fromBip32(scanDerivation.publicKey), B_spend: ECPublic.fromBip32(spendDerivation.publicKey), - network: hrp == "tsp" ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, version: version ?? 0, ); } - factory SilentPaymentOwner.fromMnemonic(String mnemonic, {String? hrp, int? version}) { - return SilentPaymentOwner.fromHd( - Bip32Slip10Secp256k1.fromSeed( - Bip39MnemonicDecoder().decode(mnemonic), - hrp == "tsp" ? Bip32Const.testNetKeyNetVersions : Bip32Const.mainNetKeyNetVersions, - ), - hrp: hrp, - version: version); + factory SilentPaymentOwner.fromMnemonic(String mnemonic, + {BasedUtxoNetwork? network, int? version}) { + return SilentPaymentOwner.fromBip32( + Bip32Slip10Secp256k1.fromSeed( + Bip39MnemonicDecoder().decode(mnemonic), + network == BitcoinNetwork.testnet + ? Bip32Const.testNetKeyNetVersions + : Bip32Const.mainNetKeyNetVersions, + ), + version: version, + ); } List generateLabel(int m) { @@ -70,10 +70,29 @@ class SilentPaymentOwner extends SilentPaymentAddress { b_spend: b_spend, B_scan: B_scan, B_spend: B_m, - network: network, version: version, ); } + + Map toJson() { + return { + 'version': version, + 'B_scan': B_scan.toHex(), + 'B_spend': B_spend.toHex(), + 'b_scan': b_scan.toHex(), + 'b_spend': b_spend.toHex(), + }; + } + + static SilentPaymentOwner fromJson(Map json) { + return SilentPaymentOwner( + version: json['version'] as int, + B_scan: ECPublic.fromHex(json['B_scan'] as String), + B_spend: ECPublic.fromHex(json['B_spend'] as String), + b_scan: ECPrivate.fromHex(json['b_scan'] as String), + b_spend: ECPrivate.fromHex(json['b_spend'] as String), + ); + } } class SilentPaymentDestination extends SilentPaymentAddress { @@ -81,7 +100,6 @@ class SilentPaymentDestination extends SilentPaymentAddress { required super.version, required ECPublic scanPubkey, required ECPublic spendPubkey, - super.network, required this.amount, }) : super(B_scan: scanPubkey, B_spend: spendPubkey); @@ -93,7 +111,6 @@ class SilentPaymentDestination extends SilentPaymentAddress { return SilentPaymentDestination( scanPubkey: receiver.B_scan, spendPubkey: receiver.B_spend, - network: receiver.network, version: receiver.version, amount: amount, ); @@ -106,16 +123,12 @@ class SilentPaymentAddress implements BitcoinBaseAddress { final int version; final ECPublic B_scan; final ECPublic B_spend; - @override - BasedUtxoNetwork? network; - final String hrp; SilentPaymentAddress({ required this.B_scan, required this.B_spend, - this.network = BitcoinNetwork.mainnet, this.version = 0, - }) : hrp = (network == BitcoinNetwork.testnet ? "tsp" : "sp") { + }) { if (version != 0) { throw Exception("Can't have other version than 0 for now"); } @@ -143,20 +156,19 @@ class SilentPaymentAddress implements BitcoinBaseAddress { return SilentPaymentAddress( B_scan: ECPublic.fromBytes(key.sublist(0, 33)), B_spend: ECPublic.fromBytes(key.sublist(33)), - network: prefix == 'tsp' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, version: version, ); } @override - String toAddress([BasedUtxoNetwork? network]) { + String toAddress(BasedUtxoNetwork network) { return toString(network: network); } @override String toString({BasedUtxoNetwork? network}) { return Bech32EncoderBase.encodeBech32( - hrp, + network == BitcoinNetwork.testnet ? 'tsp' : 'sp', [ version, ...Bech32BaseUtils.convertToBase32( diff --git a/lib/src/crypto/keypair/ec_private.dart b/lib/src/crypto/keypair/ec_private.dart index e8ace7a..c5ace75 100644 --- a/lib/src/crypto/keypair/ec_private.dart +++ b/lib/src/crypto/keypair/ec_private.dart @@ -4,7 +4,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:pointycastle/export.dart'; -import 'package:bip32/bip32.dart' as bip32; import 'package:bip32/src/utils/ecurve.dart' as ecc; /// Represents an ECDSA private key. @@ -33,6 +32,18 @@ class ECPrivate { return ECPrivate.fromBytes(decode.item1); } + factory ECPrivate.fromBip32({required Bip32Base bip32, int? account, int? index}) { + if (account != null) { + bip32 = bip32.childKey(Bip32KeyIndex(account)); + + if (index != null) { + bip32 = bip32.childKey(Bip32KeyIndex(index)); + } + } + + return ECPrivate(bip32.privateKey); + } + /// returns as WIFC (compressed) or WIF format (string) String toWif({bool compressed = true, BitcoinNetwork? network}) { List bytes = [...(network ?? BitcoinNetwork.mainnet).wifNetVer, ...toBytes()]; @@ -57,7 +68,6 @@ class ECPrivate { /// Returns a Bitcoin compact signature in hex String signMessage(List message, {String messagePrefix = '\x18Bitcoin Signed Message:\n'}) { - final messageHash = QuickCrypto.sha256Hash(BitcoinSignerUtils.magicMessage(message, messagePrefix)); diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index 18a5ec4..045f993 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -116,6 +116,10 @@ class ECPublic { return P2trAddress.fromProgram(program: pubKey, pubkey: ECPublic.fromHex(pubKey)); } + P2trAddress toP2trAddress({List>? scripts, bool tweak = true}) { + return toTaprootAddress(scripts: scripts, tweak: tweak); + } + /// toP2wpkhInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2WPKH (Pay-to-Witness-Public-Key-Hash) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. diff --git a/lib/src/provider/api_provider/api_provider.dart b/lib/src/provider/api_provider/api_provider.dart index 3f470df..81a09ac 100644 --- a/lib/src/provider/api_provider/api_provider.dart +++ b/lib/src/provider/api_provider/api_provider.dart @@ -7,10 +7,13 @@ import 'package:blockchain_utils/utils/string/string.dart'; class ApiProvider { ApiProvider({required this.api, Map? header, required this.service}) : _header = header ?? {"Content-Type": "application/json"}; - factory ApiProvider.fromMempool(BasedUtxoNetwork network, ApiService service, - {Map? header}) { - final api = APIConfig.mempool(network); - return ApiProvider(api: api, header: header, service: service); + factory ApiProvider.fromMempool( + BasedUtxoNetwork network, { + Map? header, + String? baseUrl, + }) { + final api = APIConfig.mempool(network, baseUrl); + return ApiProvider(api: api, header: header, service: BitcoinApiService()); } factory ApiProvider.fromBlocCypher(BasedUtxoNetwork network, ApiService service, {Map? header}) { @@ -80,7 +83,7 @@ class ApiProvider { } } - Future getNetworkFeeRate({String Function(String)? tokenize}) async { + Future getRecommendedFeeRate({String Function(String)? tokenize}) async { final apiUrl = api.getFeeApiUrl(); final url = tokenize?.call(apiUrl) ?? apiUrl; final response = await _getRequest>(url); diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index 4db27db..e9a348e 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -1,35 +1,96 @@ +import 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; -import 'package:blockchain_utils/exception/exceptions.dart'; +typedef ListenerCallback = StreamSubscription Function( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, +}); class ElectrumApiProvider { final BitcoinBaseElectrumRPCService rpc; - ElectrumApiProvider(this.rpc); + ElectrumApiProvider._(this.rpc); int _id = 0; + Timer? _aliveTimer; + + static Future connect(Future rpc) async { + final provider = ElectrumApiProvider._(await rpc); + provider.keepAlive(); + return provider; + } /// Sends a request to the Electrum server using the specified [request] parameter. /// /// The [timeout] parameter, if provided, sets the maximum duration for the request. - Future request(ElectrumRequest request, [Duration? timeout]) async { + Future request(ElectrumRequest request, [Duration? timeout]) async { final id = ++_id; final params = request.toRequest(id); - final data = await rpc.call(params, timeout); - return request.onResonse(_findResult(data, params)); + final result = await rpc.call(params, timeout); + return request.onResponse(result); + } + + // Preserving generic type T in subscribe method + ListenerCallback? subscribe(ElectrumRequest request) { + final id = ++_id; + final params = request.toRequest(id); + final subscription = rpc.subscribe(params); + + if (subscription == null) return null; + + try { + // Create a transformer that uses the request's response handler + final stream = subscription.subscription.map(request.onResponse); + + // Return a properly typed listener callback + return ( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + }; + } catch (_) { + return null; + } } - dynamic _findResult(Map data, ElectrumRequestDetails request) { - if (data["error"] != null) { - final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; - final message = data["error"]?['message'] ?? ""; - throw RPCError( - errorCode: code, - message: message, - data: data["error"]?["data"], - request: data["request"] ?? request.params, - ); + Future> getFeeRates() async { + try { + final topDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 1)); + final middleDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 5)); + final bottomDoubleString = await request(ElectrumEstimateFee(numberOfBlock: 10)); + final top = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) + .round(); + final middle = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) + .round(); + final bottom = + (BitcoinAmountUtils.stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) + .round(); + + return [bottom, middle, top]; + } catch (_) { + return []; } + } + + void keepAlive() { + _aliveTimer?.cancel(); + _aliveTimer = Timer.periodic(const Duration(seconds: 6), (_) async => ping()); + } - return data["result"]; + void ping() async { + try { + return await request(ElectrumPing()); + } catch (_) {} } } diff --git a/lib/src/provider/electrum_methods/methods.dart b/lib/src/provider/electrum_methods/methods.dart index 052a047..39733df 100644 --- a/lib/src/provider/electrum_methods/methods.dart +++ b/lib/src/provider/electrum_methods/methods.dart @@ -27,3 +27,4 @@ export 'methods/relay_fee.dart'; export 'methods/scripthash_unsubscribe.dart'; export 'methods/server_peer_subscribe.dart'; export 'methods/status.dart'; +export 'methods/tweaks_subscribe.dart'; diff --git a/lib/src/provider/electrum_methods/methods/add_peer.dart b/lib/src/provider/electrum_methods/methods/add_peer.dart index fb23bba..2488bb1 100644 --- a/lib/src/provider/electrum_methods/methods/add_peer.dart +++ b/lib/src/provider/electrum_methods/methods/add_peer.dart @@ -21,7 +21,7 @@ class ElectrumAddPeer extends ElectrumRequest { /// A boolean indicating whether the request was tentatively accepted /// The requesting server will appear in server.peers.subscribe() when further sanity checks complete successfully. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/block_headers.dart b/lib/src/provider/electrum_methods/methods/block_headers.dart index ef75150..2f24a6a 100644 --- a/lib/src/provider/electrum_methods/methods/block_headers.dart +++ b/lib/src/provider/electrum_methods/methods/block_headers.dart @@ -2,10 +2,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Return a concatenated chunk of block headers from the main chain. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumBlockHeaders - extends ElectrumRequest, Map> { - ElectrumBlockHeaders( - {required this.startHeight, required this.count, required this.cpHeight}); +class ElectrumBlockHeaders extends ElectrumRequest, Map> { + ElectrumBlockHeaders({required this.startHeight, required this.count, required this.cpHeight}); /// The height of the first header requested, a non-negative integer. final int startHeight; @@ -27,7 +25,7 @@ class ElectrumBlockHeaders /// A dictionary @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/broad_cast.dart b/lib/src/provider/electrum_methods/methods/broad_cast.dart index fef43d0..1dfdc1c 100644 --- a/lib/src/provider/electrum_methods/methods/broad_cast.dart +++ b/lib/src/provider/electrum_methods/methods/broad_cast.dart @@ -11,7 +11,7 @@ class ElectrumBroadCastTransaction extends ElectrumRequest { /// blockchain.transaction.broadcast @override - String get method => ElectrumRequestMethods.broadCast.method; + String get method => ElectrumRequestMethods.broadcast.method; @override List toJson() { @@ -20,7 +20,7 @@ class ElectrumBroadCastTransaction extends ElectrumRequest { /// The transaction hash as a hexadecimal string. @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/donate_address.dart b/lib/src/provider/electrum_methods/methods/donate_address.dart index 4f59989..f66127f 100644 --- a/lib/src/provider/electrum_methods/methods/donate_address.dart +++ b/lib/src/provider/electrum_methods/methods/donate_address.dart @@ -13,7 +13,7 @@ class ElectrumDonationAddress extends ElectrumRequest { } @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index 1a1f858..5fb0e63 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -22,7 +22,7 @@ class ElectrumVersion extends ElectrumRequest, List> { /// identifying the server and the protocol version that will be used for future communication. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/estimate_fee.dart b/lib/src/provider/electrum_methods/methods/estimate_fee.dart index 9e3b2f5..5e59971 100644 --- a/lib/src/provider/electrum_methods/methods/estimate_fee.dart +++ b/lib/src/provider/electrum_methods/methods/estimate_fee.dart @@ -20,7 +20,7 @@ class ElectrumEstimateFee extends ElectrumRequest { /// The estimated transaction fee in Bigint(satoshi) @override - BigInt onResonse(result) { + BigInt onResponse(result) { return BtcUtils.toSatoshi(result.toString()).abs(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_balance.dart b/lib/src/provider/electrum_methods/methods/get_balance.dart index c9a49e0..a8a0ef9 100644 --- a/lib/src/provider/electrum_methods/methods/get_balance.dart +++ b/lib/src/provider/electrum_methods/methods/get_balance.dart @@ -22,7 +22,7 @@ class ElectrumGetScriptHashBalance /// A dictionary with keys confirmed and unconfirmed. /// The value of each is the appropriate balance in minimum coin units (satoshis). @override - Map onResonse(Map result) { + Map onResponse(Map result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart index 8c89c23..512da7a 100644 --- a/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart +++ b/lib/src/provider/electrum_methods/methods/get_fee_histogram.dart @@ -3,8 +3,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Return a histogram of the fee rates paid by transactions in the memory pool, weighted by transaction size. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetFeeHistogram - extends ElectrumRequest>, List> { +class ElectrumGetFeeHistogram extends ElectrumRequest>, List> { /// mempool.get_fee_histogram @override String get method => ElectrumRequestMethods.getFeeHistogram.method; @@ -19,7 +18,7 @@ class ElectrumGetFeeHistogram /// fee uses sat/vbyte as unit, and must be a non-negative integer or float. /// vsize uses vbyte as unit, and must be a non-negative integer. @override - List> onResonse(result) { + List> onResponse(result) { return result.map((e) => List.from(e)).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_history.dart b/lib/src/provider/electrum_methods/methods/get_history.dart index 9f8efa2..ccf3ccd 100644 --- a/lib/src/provider/electrum_methods/methods/get_history.dart +++ b/lib/src/provider/electrum_methods/methods/get_history.dart @@ -23,7 +23,7 @@ class ElectrumScriptHashGetHistory /// with the output of blockchain.scripthash.get_mempool() appended to the list. /// Each confirmed transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_mempool.dart b/lib/src/provider/electrum_methods/methods/get_mempool.dart index fcd33a2..3fc91a7 100644 --- a/lib/src/provider/electrum_methods/methods/get_mempool.dart +++ b/lib/src/provider/electrum_methods/methods/get_mempool.dart @@ -21,7 +21,7 @@ class ElectrumScriptHashGetMempool /// A list of mempool transactions in arbitrary order. Each mempool transaction is a dictionary @override - List> onResonse(List result) { + List> onResponse(List result) { return result.map((e) => Map.from(e)).toList(); } } diff --git a/lib/src/provider/electrum_methods/methods/get_merkle.dart b/lib/src/provider/electrum_methods/methods/get_merkle.dart index a39cd8a..a4d196c 100644 --- a/lib/src/provider/electrum_methods/methods/get_merkle.dart +++ b/lib/src/provider/electrum_methods/methods/get_merkle.dart @@ -3,8 +3,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Return the merkle branch to a confirmed transaction given its hash and height. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetMerkle - extends ElectrumRequest, Map> { +class ElectrumGetMerkle extends ElectrumRequest, Map> { ElectrumGetMerkle({required this.transactionHash, required this.height}); /// The transaction hash as a hexadecimal string. @@ -23,7 +22,7 @@ class ElectrumGetMerkle } @override - Map onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_transaction.dart b/lib/src/provider/electrum_methods/methods/get_transaction.dart index f18dd84..2fbb74a 100644 --- a/lib/src/provider/electrum_methods/methods/get_transaction.dart +++ b/lib/src/provider/electrum_methods/methods/get_transaction.dart @@ -3,14 +3,38 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Return a raw transaction. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumGetTransaction extends ElectrumRequest { - ElectrumGetTransaction({required this.transactionHash, this.verbose = false}); +class ElectrumGetTransactionHex extends ElectrumRequest { + ElectrumGetTransactionHex({required this.transactionHash}); /// The transaction hash as a hexadecimal string. final String transactionHash; - /// Whether a verbose coin-specific response is required. - final bool verbose; + /// blockchain.transaction.get + @override + String get method => ElectrumRequestMethods.getTransaction.method; + + @override + List toJson() { + return [transactionHash, false]; + } + + /// If verbose is false: + /// The raw transaction as a hexadecimal string. + /// + /// If verbose is true: + /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. + @override + String onResponse(result) { + return result; + } +} + +class ElectrumGetTransactionVerbose + extends ElectrumRequest, Map> { + ElectrumGetTransactionVerbose({required this.transactionHash}); + + /// The transaction hash as a hexadecimal string. + final String transactionHash; /// blockchain.transaction.get @override @@ -18,7 +42,7 @@ class ElectrumGetTransaction extends ElectrumRequest { @override List toJson() { - return [transactionHash, verbose]; + return [transactionHash, true]; } /// If verbose is false: @@ -27,7 +51,7 @@ class ElectrumGetTransaction extends ElectrumRequest { /// If verbose is true: /// The result is a coin-specific dictionary – whatever the coin daemon returns when asked for a verbose form of the raw transaction. @override - dynamic onResonse(result) { + Map onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/get_unspet.dart b/lib/src/provider/electrum_methods/methods/get_unspet.dart index 2a957d1..c9e3dd1 100644 --- a/lib/src/provider/electrum_methods/methods/get_unspet.dart +++ b/lib/src/provider/electrum_methods/methods/get_unspet.dart @@ -4,10 +4,8 @@ import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; /// Return an ordered list of UTXOs sent to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashListUnspent - extends ElectrumRequest, List> { - ElectrumScriptHashListUnspent( - {required this.scriptHash, this.includeTokens = false}); +class ElectrumScriptHashListUnspent extends ElectrumRequest, List> { + ElectrumScriptHashListUnspent({required this.scriptHash, this.includeTokens = false}); /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) final String scriptHash; @@ -29,9 +27,8 @@ class ElectrumScriptHashListUnspent /// Mempool transactions paying to the address are included at the end of the list in an undefined order. /// Any output that is spent in the mempool does not appear. @override - List onResonse(result) { - final List utxos = - result.map((e) => ElectrumUtxo.fromJson(e)).toList(); + List onResponse(result) { + final List utxos = result.map((e) => ElectrumUtxo.fromJson(e)).toList(); return utxos; } } diff --git a/lib/src/provider/electrum_methods/methods/get_value_proof.dart b/lib/src/provider/electrum_methods/methods/get_value_proof.dart index bebe814..8bfe134 100644 --- a/lib/src/provider/electrum_methods/methods/get_value_proof.dart +++ b/lib/src/provider/electrum_methods/methods/get_value_proof.dart @@ -25,7 +25,7 @@ class ElectrumGetValueProof /// from the most recent update back to either the registration transaction or a /// checkpointed transaction (whichever is later). @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/header.dart b/lib/src/provider/electrum_methods/methods/header.dart index 97120b5..f961340 100644 --- a/lib/src/provider/electrum_methods/methods/header.dart +++ b/lib/src/provider/electrum_methods/methods/header.dart @@ -22,7 +22,7 @@ class ElectrumBlockHeader extends ElectrumRequest { /// This provides a proof that the given header is present in the blockchain; /// presumably the client has the merkle root hard-coded as a checkpoint. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart index 651c695..2d681b5 100644 --- a/lib/src/provider/electrum_methods/methods/headers_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/headers_subscribe.dart @@ -1,9 +1,24 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; +class ElectrumHeaderResponse { + final String hex; + final int height; + + ElectrumHeaderResponse(this.hex, this.height); + + factory ElectrumHeaderResponse.fromJson(Map json) { + return ElectrumHeaderResponse(json['hex'], json['height']); + } + + Map toJson() { + return {'hex': hex, 'height': height}; + } +} + /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html class ElectrumHeaderSubscribe - extends ElectrumRequest, Map> { + extends ElectrumRequest> { /// blockchain.headers.subscribe @override String get method => ElectrumRequestMethods.headersSubscribe.method; @@ -15,7 +30,7 @@ class ElectrumHeaderSubscribe /// The header of the current block chain tip. @override - Map onResonse(result) { - return result; + ElectrumHeaderResponse onResponse(result) { + return ElectrumHeaderResponse.fromJson(result); } } diff --git a/lib/src/provider/electrum_methods/methods/id_from_pos.dart b/lib/src/provider/electrum_methods/methods/id_from_pos.dart index 865f0b2..cf7b2e8 100644 --- a/lib/src/provider/electrum_methods/methods/id_from_pos.dart +++ b/lib/src/provider/electrum_methods/methods/id_from_pos.dart @@ -27,7 +27,7 @@ class ElectrumIdFromPos extends ElectrumRequest { /// If merkle is false, the transaction hash as a hexadecimal string. If true, a dictionary @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart index f3169d3..64be82a 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_announce_broadcast.dart @@ -20,7 +20,7 @@ class ElectrumMasternodeAnnounceBroadcast extends ElectrumRequest { /// true if the message was broadcasted successfully otherwise false. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_list.dart b/lib/src/provider/electrum_methods/methods/masternode_list.dart index 437dc3b..316767d 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_list.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_list.dart @@ -2,8 +2,7 @@ import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; /// Returns the list of masternodes. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumMasternodeList - extends ElectrumRequest, List> { +class ElectrumMasternodeList extends ElectrumRequest, List> { ElectrumMasternodeList({required this.payees}); /// An array of masternode payee addresses. @@ -20,7 +19,7 @@ class ElectrumMasternodeList /// An array with the masternodes information. @override - List onResonse(result) { + List onResponse(result) { return List.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart index 2734415..e9b1445 100644 --- a/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/masternode_subscribe.dart @@ -22,7 +22,7 @@ class ElectrumMasternodeSubscribe extends ElectrumRequest { /// the internet connection, the offline time and even the collateral /// amount, so this subscription notice these changes to the user. @override - String onResonse(result) { + String onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/ping.dart b/lib/src/provider/electrum_methods/methods/ping.dart index 4d41704..d29fdb8 100644 --- a/lib/src/provider/electrum_methods/methods/ping.dart +++ b/lib/src/provider/electrum_methods/methods/ping.dart @@ -12,7 +12,7 @@ class ElectrumPing extends ElectrumRequest { } @override - dynamic onResonse(result) { + dynamic onResponse(result) { return null; } } diff --git a/lib/src/provider/electrum_methods/methods/protx_diff.dart b/lib/src/provider/electrum_methods/methods/protx_diff.dart index cca5d7d..7004fae 100644 --- a/lib/src/provider/electrum_methods/methods/protx_diff.dart +++ b/lib/src/provider/electrum_methods/methods/protx_diff.dart @@ -22,7 +22,7 @@ class ElectrumProtXDiff extends ElectrumRequest, dynamic> { /// A dictionary with deterministic masternode lists diff plus proof data. @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/protx_info.dart b/lib/src/provider/electrum_methods/methods/protx_info.dart index 7685699..1f78505 100644 --- a/lib/src/provider/electrum_methods/methods/protx_info.dart +++ b/lib/src/provider/electrum_methods/methods/protx_info.dart @@ -19,7 +19,7 @@ class ElectrumProtXInfo extends ElectrumRequest, dynamic> { /// A dictionary with detailed deterministic masternode data @override - Map onResonse(result) { + Map onResponse(result) { return Map.from(result); } } diff --git a/lib/src/provider/electrum_methods/methods/relay_fee.dart b/lib/src/provider/electrum_methods/methods/relay_fee.dart index 267dccf..aea1478 100644 --- a/lib/src/provider/electrum_methods/methods/relay_fee.dart +++ b/lib/src/provider/electrum_methods/methods/relay_fee.dart @@ -15,7 +15,7 @@ class ElectrumRelayFee extends ElectrumRequest { /// relay fee in Bigint(satoshi) @override - BigInt onResonse(result) { + BigInt onResponse(result) { return BtcUtils.toSatoshi(result.toString()); } } diff --git a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart index 4990b6d..90cfcb8 100644 --- a/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart +++ b/lib/src/provider/electrum_methods/methods/scripthash_unsubscribe.dart @@ -21,7 +21,7 @@ class ElectrumScriptHashUnSubscribe extends ElectrumRequest { /// otherwise False. Note that False might be returned even /// for something subscribed to earlier, because the server can drop subscriptions in rare circumstances. @override - bool onResonse(result) { + bool onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_banner.dart b/lib/src/provider/electrum_methods/methods/server_banner.dart index 92e3ab4..4a2c0ac 100644 --- a/lib/src/provider/electrum_methods/methods/server_banner.dart +++ b/lib/src/provider/electrum_methods/methods/server_banner.dart @@ -12,7 +12,7 @@ class ElectrumServerBanner extends ElectrumRequest { } @override - String onResonse(result) { + String onResponse(result) { return result.toString(); } } diff --git a/lib/src/provider/electrum_methods/methods/server_features.dart b/lib/src/provider/electrum_methods/methods/server_features.dart index 7e9c939..725d6ef 100644 --- a/lib/src/provider/electrum_methods/methods/server_features.dart +++ b/lib/src/provider/electrum_methods/methods/server_features.dart @@ -15,7 +15,7 @@ class ElectrumServerFeatures extends ElectrumRequest { /// A dictionary of keys and values. Each key represents a feature or service of the server, /// and the value gives additional information. @override - dynamic onResonse(result) { + dynamic onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart index d1a3db5..04e8709 100644 --- a/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/server_peer_subscribe.dart @@ -2,8 +2,7 @@ import 'package:bitcoin_base/src/provider/api_provider.dart'; /// Return a list of peer servers. Despite the name this is not a subscription and the server must send no notifications.. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumServerPeersSubscribe - extends ElectrumRequest, List> { +class ElectrumServerPeersSubscribe extends ElectrumRequest, List> { /// server.peers.subscribe @override String get method => ElectrumRequestMethods.serverPeersSubscribe.method; @@ -15,7 +14,7 @@ class ElectrumServerPeersSubscribe /// An array of peer servers, each returned as a 3-element array @override - List onResonse(result) { + List onResponse(result) { return result; } } diff --git a/lib/src/provider/electrum_methods/methods/status.dart b/lib/src/provider/electrum_methods/methods/status.dart index f12a0fa..2967676 100644 --- a/lib/src/provider/electrum_methods/methods/status.dart +++ b/lib/src/provider/electrum_methods/methods/status.dart @@ -2,8 +2,7 @@ import 'package:bitcoin_base/src/provider/api_provider.dart'; /// Subscribe to a script hash. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html -class ElectrumScriptHashSubscribe - extends ElectrumRequest, dynamic> { +class ElectrumScriptHashSubscribe extends ElectrumRequest { ElectrumScriptHashSubscribe({required this.scriptHash}); /// /// The script hash as a hexadecimal string (BitcoinBaseAddress.pubKeyHash()) @@ -20,7 +19,7 @@ class ElectrumScriptHashSubscribe /// The status of the script hash. @override - Map onResonse(result) { - return Map.from(result); + String onResponse(result) { + return result; } } diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart new file mode 100644 index 0000000..18cf2ee --- /dev/null +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -0,0 +1,98 @@ +import 'package:bitcoin_base/src/provider/service/electrum/electrum.dart'; + +class TweakOutputData { + final int vout; + final int amount; + final dynamic spendingInput; + + TweakOutputData({ + required this.vout, + required this.amount, + this.spendingInput, + }); +} + +class TweakData { + final String tweak; + final Map outputPubkeys; + + TweakData({required this.tweak, required this.outputPubkeys}); +} + +class ElectrumTweaksSubscribeResponse { + final String? message; + final int block; + final Map blockTweaks; + + ElectrumTweaksSubscribeResponse({required this.block, required this.blockTweaks, this.message}); + + factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { + late int block; + final blockTweaks = {}; + + try { + for (final key in json.keys) { + block = int.parse(key); + final txs = json[key] as Map; + + for (final txid in txs.keys) { + final tweakResponseData = txs[txid] as Map; + + final tweakHex = tweakResponseData["tweak"].toString(); + final outputPubkeys = (tweakResponseData["output_pubkeys"] as Map); + + final tweakOutputData = {}; + + for (final vout in outputPubkeys.keys) { + final outputData = outputPubkeys[vout]; + tweakOutputData[outputData[0]] = TweakOutputData( + vout: int.parse(vout.toString()), + amount: outputData[1], + spendingInput: outputData.length > 2 ? outputData[2] : null, + ); + } + + final tweakData = TweakData(tweak: tweakHex, outputPubkeys: tweakOutputData); + blockTweaks[txid] = tweakData; + } + } + } catch (_) { + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: 0, + blockTweaks: {}, + ); + } + + return ElectrumTweaksSubscribeResponse( + message: json.containsKey('message') ? json['message'] : null, + block: block, + blockTweaks: blockTweaks, + ); + } +} + +/// Subscribe to receive block headers when a new block is found. +/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +class ElectrumTweaksSubscribe + extends ElectrumRequest> { + /// blockchain.tweaks.subscribe + ElectrumTweaksSubscribe({required this.height, required this.count}); + + final int height; + final int count; + + @override + String get method => ElectrumRequestMethods.tweaksSubscribe.method; + + @override + List toJson() { + return [height, count]; + } + + /// The header of the current block chain tip. + @override + ElectrumTweaksSubscribeResponse onResponse(result) { + return ElectrumTweaksSubscribeResponse.fromJson(result); + } +} diff --git a/lib/src/provider/models/config.dart b/lib/src/provider/models/config.dart index 9369398..444098e 100644 --- a/lib/src/provider/models/config.dart +++ b/lib/src/provider/models/config.dart @@ -71,8 +71,7 @@ class APIConfig { } return APIConfig( - url: - "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", + url: "$baseUrl/addrs/###/?unspentOnly=true&includeScript=true&limit=2000", feeRate: baseUrl, transaction: "$baseUrl/txs/###", sendTransaction: "$baseUrl/txs/push", @@ -82,18 +81,19 @@ class APIConfig { blockHeight: "$baseUrl/blocks/###"); } - factory APIConfig.mempool(BasedUtxoNetwork network) { - String baseUrl; - switch (network) { - case BitcoinNetwork.mainnet: - baseUrl = BtcApiConst.mempoolMainBaseURL; - break; - case BitcoinNetwork.testnet: - baseUrl = BtcApiConst.mempoolBaseURL; - break; - default: - throw BitcoinBasePluginException( - "mempool does not support ${network.conf.coinName.name}"); + factory APIConfig.mempool(BasedUtxoNetwork network, [String? baseUrl]) { + if (baseUrl == null) { + switch (network) { + case BitcoinNetwork.mainnet: + baseUrl = BtcApiConst.mempoolMainBaseURL; + break; + case BitcoinNetwork.testnet: + baseUrl = BtcApiConst.mempoolBaseURL; + break; + default: + throw BitcoinBasePluginException( + "mempool does not support ${network.conf.coinName.name}"); + } } return APIConfig( diff --git a/lib/src/provider/models/electrum/electrum_utxo.dart b/lib/src/provider/models/electrum/electrum_utxo.dart index 9fd9abc..9ec03d7 100644 --- a/lib/src/provider/models/electrum/electrum_utxo.dart +++ b/lib/src/provider/models/electrum/electrum_utxo.dart @@ -30,11 +30,25 @@ class ElectrumUtxo implements UTXO { @override BitcoinUtxo toUtxo(BitcoinAddressType addressType) { return BitcoinUtxo( - txHash: txId, - value: value, - vout: vout, - scriptType: addressType, - blockHeight: height, - token: token); + txHash: txId, + value: value, + vout: vout, + scriptType: addressType, + blockHeight: height, + token: token, + ); + } + + Map toJson() { + return { + "height": height, + "tx_hash": txId, + "tx_pos": vout, + "value": value.toString(), + }; + } + + static List fromJsonList(List json) { + return json.map((e) => ElectrumUtxo.fromJson(e)).toList(); } } diff --git a/lib/src/provider/models/fee_rate/fee_rate.dart b/lib/src/provider/models/fee_rate/fee_rate.dart index 09c6312..37aa345 100644 --- a/lib/src/provider/models/fee_rate/fee_rate.dart +++ b/lib/src/provider/models/fee_rate/fee_rate.dart @@ -2,30 +2,45 @@ import 'package:bitcoin_base/src/exception/exception.dart'; enum BitcoinFeeRateType { low, medium, high } +class BitcoinFee { + BitcoinFee({int? satoshis, BigInt? bytes}) + : satoshis = satoshis ?? _parseKbFees(bytes!), + bytes = bytes ?? _parseMempoolFees(satoshis!); + + final int satoshis; + final BigInt bytes; + + @override + String toString() { + return 'satoshis: $satoshis, bytes: $bytes'; + } +} + class BitcoinFeeRate { - BitcoinFeeRate( - {required this.high, - required this.medium, - required this.low, - this.economyFee, - this.hourFee}); + BitcoinFeeRate({ + required this.high, + required this.medium, + required this.low, + this.economyFee, + this.minimumFee, + }); /// High fee rate in satoshis per kilobyte - final BigInt high; + final BitcoinFee high; /// Medium fee rate in satoshis per kilobyte - final BigInt medium; + final BitcoinFee medium; /// low fee rate in satoshis per kilobyte - final BigInt low; + final BitcoinFee low; /// only mnenpool api - final BigInt? economyFee; + final BitcoinFee? economyFee; /// only mnenpool api - final BigInt? hourFee; + final BitcoinFee? minimumFee; - BigInt _feeRatrete(BitcoinFeeRateType feeRateType) { + BitcoinFee _feeRate(BitcoinFeeRateType feeRateType) { switch (feeRateType) { case BitcoinFeeRateType.low: return low; @@ -36,6 +51,10 @@ class BitcoinFeeRate { } } + int toSat(BigInt feeRate) { + return _parseKbFees(feeRate); + } + /// GetEstimate calculates the estimated fee in satoshis for a given transaction size /// and fee rate (in satoshis per kilobyte) using the formula: // @@ -48,16 +67,15 @@ class BitcoinFeeRate { /// Returns: /// - BigInt: A BigInt containing the estimated fee in satoshis. BigInt getEstimate(int trSize, - {BigInt? customFeeRatePerKb, - BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { - BigInt feeRate = customFeeRatePerKb ?? _feeRatrete(feeRateType); + {BigInt? customFeeRatePerKb, BitcoinFeeRateType feeRateType = BitcoinFeeRateType.medium}) { + BigInt feeRate = customFeeRatePerKb ?? _feeRate(feeRateType).bytes; final trSizeBigInt = BigInt.from(trSize); return (trSizeBigInt * feeRate) ~/ BigInt.from(1000); } @override String toString() { - return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee hourFee: $hourFee'; + return 'high: ${high.toString()} medium: ${medium.toString()} low: ${low.toString()}, economyFee: $economyFee minimumFee: $minimumFee'; } /// NewBitcoinFeeRateFromMempool creates a BitcoinFeeRate structure from JSON data retrieved @@ -65,14 +83,11 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromMempool(Map json) { return BitcoinFeeRate( - high: _parseMempoolFees(json['fastestFee']), - medium: _parseMempoolFees(json['halfHourFee']), - low: _parseMempoolFees(json['minimumFee']), - economyFee: json['economyFee'] == null - ? null - : _parseMempoolFees(json['economyFee']), - hourFee: - json['hourFee'] == null ? null : _parseMempoolFees(json['hourFee']), + high: BitcoinFee(satoshis: json['fastestFee']), + medium: BitcoinFee(satoshis: json['halfHourFee']), + low: BitcoinFee(satoshis: json['hourFee']), + economyFee: json['economyFee'] == null ? null : BitcoinFee(satoshis: json['economyFee']), + minimumFee: json['minimumFee'] == null ? null : BitcoinFee(satoshis: json['minimumFee']), ); } @@ -81,9 +96,10 @@ class BitcoinFeeRate { /// information for high, medium, and low fee levels. factory BitcoinFeeRate.fromBlockCypher(Map json) { return BitcoinFeeRate( - high: BigInt.from((json['high_fee_per_kb'] as int)), - medium: BigInt.from((json['medium_fee_per_kb'] as int)), - low: BigInt.from((json['low_fee_per_kb'] as int))); + high: BitcoinFee(bytes: BigInt.from((json['high_fee_per_kb'] as int))), + medium: BitcoinFee(bytes: BigInt.from((json['medium_fee_per_kb'] as int))), + low: BitcoinFee(bytes: BigInt.from((json['low_fee_per_kb'] as int))), + ); } } @@ -103,3 +119,12 @@ BigInt _parseMempoolFees(dynamic data) { "cannot parse mempool fees excepted double, string got ${data.runtimeType}"); } } + +/// ParseMempoolFees takes a data dynamic and converts it to a BigInt representing +/// mempool fees in satoshis per kilobyte (sat/KB). The function performs the conversion +/// based on the type of the input data, which can be either a double (floating-point +/// fee rate) or an int (integer fee rate in satoshis per byte). +int _parseKbFees(BigInt fee) { + const kb = 1024; + return (fee.toInt() / kb).round(); +} diff --git a/lib/src/provider/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index c8b56be..8be4045 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -1,3 +1,7 @@ export 'methods.dart'; export 'params.dart'; export 'service.dart'; +export 'request_completer.dart'; +// export 'electrum_ssl_service.dart'; +export 'electrum_tcp_service.dart'; +// export 'electrum_websocket_service.dart'; diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart new file mode 100644 index 0000000..57e1cbd --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumSSLService implements BitcoinBaseElectrumRPCService { + ElectrumSSLService._( + this.url, + SecureSocket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + SecureSocket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + await _socket?.close(); + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + onConnectionStatusChange = null; + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await SecureSocket.connect(uri.host, uri.port).timeout(connectionTimeOut); + + return ElectrumSSLService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] as int?; + if (id == null) { + _tasks.forEach((key, value) { + if (value.request.method == response['method']) { + id = key; + } + }); + } + + try { + final result = _findResult(response, _tasks[id]!.request); + + if (result != null) { + _finish(id!, result); + } + } catch (_) {} + } + + void _onMessage(List event) { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } + + dynamic _findResult(dynamic data, ElectrumRequestDetails request) { + if (data["error"] != null) { + if (data["error"] is String) { + _errors[request.id] = RPCError( + data: data["error"], + errorCode: 0, + message: data["error"], + request: request.params, + ); + } else { + final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; + final message = data["error"]?['message'] ?? ""; + _errors[request.id] = RPCError( + errorCode: code, + message: message, + data: data["error"]?["data"], + request: data["request"] ?? request.params, + ); + } + + throw _errors[request.id]!; + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + task.completer!.complete(result); + } + + if (!task.isSubscription) { + _tasks.remove(id); + } else { + task.subject?.add(result); + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart new file mode 100644 index 0000000..ea965af --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/exception/exceptions.dart'; + +class ElectrumTCPService implements BitcoinBaseElectrumRPCService { + ElectrumTCPService._( + this.url, + Socket channel, { + this.defaultRequestTimeOut = const Duration(seconds: 30), + this.onConnectionStatusChange, + }) : _socket = channel { + _setConnectionStatus(ConnectionStatus.connected); + _subscription = _socket!.listen(_onMessage, onError: close, onDone: _onDone); + } + Socket? _socket; + StreamSubscription>? _subscription; + final Duration defaultRequestTimeOut; + String unterminatedString = ''; + final Map _errors = {}; + final Map _tasks = {}; + + ConnectionStatus _connectionStatus = ConnectionStatus.connecting; + bool get _isDisconnected => _connectionStatus == ConnectionStatus.disconnected; + @override + bool get isConnected => !_isDisconnected; + void Function(ConnectionStatus)? onConnectionStatusChange; + + @override + final String url; + + void add(List params) { + if (_isDisconnected) { + throw StateError("socket has been disconnected"); + } + _socket?.add(params); + } + + void _setConnectionStatus(ConnectionStatus status) { + onConnectionStatusChange?.call(status); + _connectionStatus = status; + if (!isConnected) { + try { + _socket?.destroy(); + } catch (_) {} + _socket = null; + } + } + + @override + void reconnect() { + if (_isDisconnected) { + _setConnectionStatus(ConnectionStatus.connecting); + connect(Uri.parse(url)).then((value) { + _setConnectionStatus(ConnectionStatus.connected); + }).catchError((e) { + _setConnectionStatus(ConnectionStatus.failed); + }); + } + } + + void close(Object? error) async { + await _socket?.close(); + _socket = null; + _subscription?.cancel().catchError((e) {}); + _subscription = null; + + onConnectionStatusChange = null; + } + + void _onDone() { + close(null); + } + + @override + void disconnect() { + close(null); + } + + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) async { + final channel = await Socket.connect(uri.host, uri.port).timeout(connectionTimeOut); + + return ElectrumTCPService._( + uri.toString(), + channel, + defaultRequestTimeOut: defaultRequestTimeOut, + onConnectionStatusChange: onConnectionStatusChange, + ); + } + + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (_) {} + } + + void _handleResponse(Map response) { + var id = response['id'] as int?; + if (id == null) { + _tasks.forEach((key, value) { + if (value.request.method == response['method']) { + id = key; + } + }); + } + + try { + final result = _findResult(response, _tasks[id]!.request); + + if (result != null) { + _finish(id!, result); + } + } catch (_) {} + } + + void _onMessage(List event) { + final msg = utf8.decode(event.toList()); + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + + _parseResponse(message); + } + } + + dynamic _findResult(dynamic data, ElectrumRequestDetails request) { + if (data["error"] != null) { + if (data["error"] is String) { + _errors[request.id] = RPCError( + data: data["error"], + errorCode: 0, + message: data["error"], + request: request.params, + ); + } else { + final code = int.tryParse(((data["error"]?['code']?.toString()) ?? "0")) ?? 0; + final message = data["error"]?['message'] ?? ""; + _errors[request.id] = RPCError( + errorCode: code, + message: message, + data: data["error"]?["data"], + request: data["request"] ?? request.params, + ); + } + + throw _errors[request.id]!; + } + + return data["result"] ?? data["params"]?[0]; + } + + void _finish(int id, dynamic result) { + final task = _tasks[id]; + if (task == null) { + return; + } + + final notCompleted = task.completer != null && task.completer!.isCompleted == false; + if (notCompleted) { + task.completer!.complete(result); + } + + if (!task.isSubscription) { + _tasks.remove(id); + } else { + task.subject?.add(result); + } + } + + AsyncBehaviorSubject _registerSubscription(ElectrumRequestDetails params) { + final subscription = AsyncBehaviorSubject(params.params); + _tasks[params.id] = SocketTask( + subject: subscription.subscription, + request: params, + isSubscription: true, + ); + return subscription; + } + + @override + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params) { + try { + final subscription = _registerSubscription(params); + add(params.toTCPParams()); + + return subscription; + } catch (e) { + return null; + } + } + + AsyncRequestCompleter _registerTask(ElectrumRequestDetails params) { + final completer = AsyncRequestCompleter(params.params); + + _tasks[params.id] = SocketTask( + completer: completer.completer, + request: params, + isSubscription: false, + ); + + return completer; + } + + @override + Future call(ElectrumRequestDetails params, [Duration? timeout]) async { + try { + final completer = _registerTask(params); + add(params.toTCPParams()); + final result = await completer.completer.future.timeout(timeout ?? defaultRequestTimeOut); + return result; + } finally { + _tasks.remove(params.id); + } + } + + String getErrorMessage(int id) => _errors[id]?.data ?? ''; +} diff --git a/lib/src/provider/service/electrum/electrum_websocket_service.dart b/lib/src/provider/service/electrum/electrum_websocket_service.dart new file mode 100644 index 0000000..1a460a6 --- /dev/null +++ b/lib/src/provider/service/electrum/electrum_websocket_service.dart @@ -0,0 +1,85 @@ +// import 'dart:async'; +// import 'dart:convert'; +// import 'package:bitcoin_base/bitcoin_base.dart'; +// import 'package:example/services_examples/cross_platform_websocket/core.dart'; + +// class ElectrumWebSocketService implements BitcoinBaseElectrumRPCService { +// ElectrumWebSocketService._( +// this.url, +// WebSocketCore channel, { +// this.defaultRequestTimeOut = const Duration(seconds: 30), +// }) : _socket = channel { +// _subscription = +// channel.stream.cast().listen(_onMessage, onError: _onClose, onDone: _onDone); +// } +// WebSocketCore? _socket; +// StreamSubscription? _subscription; +// final Duration defaultRequestTimeOut; + +// Map requests = {}; +// bool _isDisconnected = false; + +// bool get isConnected => !_isDisconnected; + +// @override +// final String url; + +// void add(List params) { +// if (_isDisconnected) { +// throw StateError("socket has been disconnected"); +// } +// _socket?.sink(params); +// } + +// void _onClose(Object? error) { +// _isDisconnected = true; + +// _socket?.close(); +// _socket = null; +// _subscription?.cancel().catchError((e) {}); +// _subscription = null; +// } + +// void _onDone() { +// _onClose(null); +// } + +// @override +// void disconnect() { +// _onClose(null); +// } + +// static Future connect( +// String url, { +// Iterable? protocols, +// Duration defaultRequestTimeOut = const Duration(seconds: 30), +// final Duration connectionTimeOut = const Duration(seconds: 30), +// }) async { +// final channel = await WebSocketCore.connect(url, protocols: protocols?.toList()); + +// return ElectrumWebSocketService._(url, channel, defaultRequestTimeOut: defaultRequestTimeOut); +// } + +// void _onMessage(String event) { +// final Map decode = json.decode(event); +// if (decode.containsKey("id")) { +// final int id = int.parse(decode["id"]!.toString()); +// final request = requests.remove(id); +// request?.completer.complete(decode); +// } +// } + +// @override +// Future> call(ElectrumRequestDetails params, [Duration? timeout]) async { +// final AsyncRequestCompleter compeleter = AsyncRequestCompleter(params.params); + +// try { +// requests[params.id] = compeleter; +// add(params.toWebSocketParams()); +// final result = await compeleter.completer.future.timeout(timeout ?? defaultRequestTimeOut); +// return result; +// } finally { +// requests.remove(params.id); +// } +// } +// } diff --git a/lib/src/provider/service/electrum/methods.dart b/lib/src/provider/service/electrum/methods.dart index 66984e5..e5fe2df 100644 --- a/lib/src/provider/service/electrum/methods.dart +++ b/lib/src/provider/service/electrum/methods.dart @@ -15,20 +15,21 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods serverAddPeer = ElectrumRequestMethods._("server.add_peer"); /// Subscribe to a script hash. + static const String scripthashesSubscribeMethod = "blockchain.scripthash.subscribe"; static const ElectrumRequestMethods scriptHashSubscribe = - ElectrumRequestMethods._("blockchain.scripthash.subscribe"); + ElectrumRequestMethods._(scripthashesSubscribeMethod); /// Unsubscribe from a script hash, preventing future notifications if its status changes. static const ElectrumRequestMethods scriptHashUnSubscribe = ElectrumRequestMethods._("blockchain.scripthash.unsubscribe"); /// Return an ordered list of UTXOs sent to a script hash. - static const ElectrumRequestMethods listunspent = - ElectrumRequestMethods._("blockchain.scripthash.listunspent"); + static const String listunspentMethod = "blockchain.scripthash.listunspent"; + static const ElectrumRequestMethods listunspent = ElectrumRequestMethods._(listunspentMethod); /// Return the confirmed and unconfirmed balances of a script hash. - static const ElectrumRequestMethods getBalance = - ElectrumRequestMethods._("blockchain.scripthash.get_balance"); + static const String getBalanceMethod = "blockchain.scripthash.get_balance"; + static const ElectrumRequestMethods getBalance = ElectrumRequestMethods._(getBalanceMethod); /// Return a raw transaction. static const ElectrumRequestMethods getTransaction = @@ -59,16 +60,16 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("blockchain.estimatefee"); /// Return the confirmed and unconfirmed history of a script hash. - static const ElectrumRequestMethods getHistory = - ElectrumRequestMethods._("blockchain.scripthash.get_history"); + static const String getHistoryMethod = "blockchain.scripthash.get_history"; + static const ElectrumRequestMethods getHistory = ElectrumRequestMethods._(getHistoryMethod); /// Return the unconfirmed transactions of a script hash. static const ElectrumRequestMethods getMempool = ElectrumRequestMethods._("blockchain.scripthash.get_mempool"); /// Broadcast a transaction to the network. - static const ElectrumRequestMethods broadCast = - ElectrumRequestMethods._("blockchain.transaction.broadcast"); + static const String broadcastMethod = "blockchain.transaction.broadcast"; + static const ElectrumRequestMethods broadcast = ElectrumRequestMethods._(broadcastMethod); /// Return a banner to be shown in the Electrum console. static const ElectrumRequestMethods serverBanner = ElectrumRequestMethods._("server.banner"); @@ -83,8 +84,14 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods version = ElectrumRequestMethods._("server.version"); /// Subscribe to receive block headers when a new block is found. + static const String headersSubscribeMethod = "blockchain.headers.subscribe"; static const ElectrumRequestMethods headersSubscribe = - ElectrumRequestMethods._("blockchain.headers.subscribe"); + ElectrumRequestMethods._(headersSubscribeMethod); + + /// Subscribe to receive block headers when a new block is found. + static const String tweaksSubscribeMethod = "blockchain.tweaks.subscribe"; + static const ElectrumRequestMethods tweaksSubscribe = + ElectrumRequestMethods._(tweaksSubscribeMethod); /// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. static const ElectrumRequestMethods relayFee = ElectrumRequestMethods._("blockchain.relayfee"); diff --git a/lib/src/provider/service/electrum/params.dart b/lib/src/provider/service/electrum/params.dart index 2945af7..d253ff7 100644 --- a/lib/src/provider/service/electrum/params.dart +++ b/lib/src/provider/service/electrum/params.dart @@ -34,7 +34,7 @@ class ElectrumRequestDetails { abstract class ElectrumRequest implements ElectrumRequestParams { String? get validate => null; - RESULT onResonse(RESPONSE result) { + RESULT onResponse(RESPONSE result) { return result as RESULT; } diff --git a/lib/src/provider/service/electrum/request_completer.dart b/lib/src/provider/service/electrum/request_completer.dart new file mode 100644 index 0000000..d238dbc --- /dev/null +++ b/lib/src/provider/service/electrum/request_completer.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +class AsyncRequestCompleter { + AsyncRequestCompleter(this.params); + final Completer completer = Completer(); + final Map params; +} + +class AsyncBehaviorSubject { + AsyncBehaviorSubject(this.params); + final BehaviorSubject subscription = BehaviorSubject(); + final Map params; +} diff --git a/lib/src/provider/service/electrum/service.dart b/lib/src/provider/service/electrum/service.dart index 03e4ca2..972641d 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -1,12 +1,50 @@ +import 'dart:convert'; + import 'package:bitcoin_base/src/provider/service/electrum/params.dart'; +import 'package:bitcoin_base/src/provider/service/electrum/request_completer.dart'; +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + +enum ConnectionStatus { connected, disconnected, connecting, failed } + +class SocketTask { + SocketTask({ + required this.isSubscription, + required this.request, + this.completer, + this.subject, + }); + + final Completer? completer; + final BehaviorSubject? subject; + final bool isSubscription; + final ElectrumRequestDetails request; +} + +/// Abstract class for providing JSON-RPC service functionality. +abstract class BitcoinBaseElectrumRPCService { + BitcoinBaseElectrumRPCService(); -/// A mixin for providing JSON-RPC service functionality. -mixin BitcoinBaseElectrumRPCService { /// Represents the URL endpoint for JSON-RPC calls. String get url; - /// Makes an HTTP GET request to the Tron network with the specified [params]. + AsyncBehaviorSubject? subscribe(ElectrumRequestDetails params); + + /// Makes an HTTP GET request with the specified [params]. /// /// The optional [timeout] parameter sets the maximum duration for the request. - Future> call(ElectrumRequestDetails params, [Duration? timeout]); + Future call(ElectrumRequestDetails params, [Duration? timeout]); + + bool get isConnected; + void disconnect(); + void reconnect(); +} + +bool isJSONStringCorrect(String source) { + try { + json.decode(source); + return true; + } catch (_) { + return false; + } } diff --git a/lib/src/provider/service/http/http_service.dart b/lib/src/provider/service/http/http_service.dart index be0e94a..c0ddd78 100644 --- a/lib/src/provider/service/http/http_service.dart +++ b/lib/src/provider/service/http/http_service.dart @@ -1,3 +1,8 @@ +import 'dart:convert'; + +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:http/http.dart' as http; + /// The [ApiService] abstract class defines a contract for making HTTP requests. abstract class ApiService { /// Performs an HTTP GET request to the specified [url]. @@ -15,3 +20,68 @@ abstract class ApiService { Future post(String url, {Map headers = const {"Content-Type": "application/json"}, Object? body}); } + +class ApiProviderException implements Exception { + final String message; + final int? statusCode; + final Map? responseData; + const ApiProviderException(this.message, [this.statusCode, this.responseData]); + @override + String toString() { + return "status: $statusCode $message ${responseData ?? ""}"; + } +} + +class BitcoinApiService implements ApiService { + BitcoinApiService([http.Client? client]) : _client = client ?? http.Client(); + final http.Client _client; + @override + Future get(String url) async { + final response = await _client.get(Uri.parse(url)); + return _readResponse(response); + } + + @override + Future post(String url, + {Map headers = const {"Content-Type": "application/json"}, + Object? body}) async { + final response = await _client.post(Uri.parse(url), headers: headers, body: body); + return _readResponse(response); + } + + T _readResponse(http.Response response) { + final String toString = _readBody(response); + switch (T) { + case String: + return toString as T; + case List: + case Map: + return jsonDecode(toString) as T; + default: + try { + return jsonDecode(toString) as T; + } catch (e) { + throw const ApiProviderException("invalid request"); + } + } + } + + String _readBody(http.Response response) { + _readErr(response); + return StringUtils.decode(response.bodyBytes); + } + + void _readErr(http.Response response) { + if (response.statusCode == 200 || response.statusCode == 201) return; + String toString = StringUtils.decode(response.bodyBytes); + Map? errorResult; + try { + if (toString.isNotEmpty) { + errorResult = StringUtils.toJson(toString); + } + // ignore: empty_catches + } catch (e) {} + toString = toString.isEmpty ? "request_error" : toString; + throw ApiProviderException(toString, response.statusCode, errorResult); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a4d6530..059c368 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,9 +20,10 @@ dependencies: pointycastle: ^3.7.4 bip32: ^2.0.0 blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ + rxdart: ^0.28.0 + intl: ^0.18.0 + http: ^1.1.0 dev_dependencies: diff --git a/test/encode_decode_transaction_test.dart b/test/encode_decode_transaction_test.dart index b77dfdb..d3588a1 100644 --- a/test/encode_decode_transaction_test.dart +++ b/test/encode_decode_transaction_test.dart @@ -5,8 +5,7 @@ void main() { test("test1", () { final tx = BtcTransaction(inputs: [ TxInput( - txId: - "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 0, scriptSig: Script( script: [ @@ -16,8 +15,7 @@ void main() { ), ), TxInput( - txId: - "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", + txId: "daaa0beab7411cee74768b3f1d7da7ad55fcbc9d835fda0afbe1b9a41ae42f75", txIndex: 2, scriptSig: Script(script: [ "3044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd41", @@ -38,8 +36,7 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: - "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.from(2000), ), @@ -53,8 +50,7 @@ void main() { TxOutput( amount: BigInt.from(1000), cashToken: CashToken( - category: - "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", + category: "4e7873d4529edfd2c6459139257042950230baa9297f111b8675829443f70430", bitfield: 16, amount: BigInt.parse("799999999999996000"), ), @@ -69,8 +65,9 @@ void main() { )), ]); const String raw = - "0200000002752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada000000006a47304402203795e2aa978afbbd676b36c0edd1a39478744d320c5e02dd6a39d154755a5a3e02205545a7765bae3db9b820b1db9be2f8955a8998fa92ac08b0d7416aa30a3cee42412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff752fe41aa4b9e1fb0ada5f839dbcfc55ada77d1d3f8b7674ee1c41b7ea0baada020000006a473044022071b39d6aefdea7a7837310e5d78b1069ae82ed007991f8bca80bf3baf26340cc02200bc356be6e9f0019849b75b00322262a23ce179e7c0313207addfdbe596dabfd412103ac064f4489ff812c643471ed25b990e6be7212566c932b8680b900c8e6db0fd9ffffffff03389c9900000000001976a914b8d913342894ec7b066420952a618ec2a8269bc288ace80300000000000048ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10fdd007aa201d12ffe8e85fdab36794cb09418982efdbd5c8cee5fbeb216ac43887ea4817b887e80300000000000044ef3004f743948275861b117f29a9ba300295427025399145c6d2df9e52d473784e10ff60f04fecc22b1a0b76a914b8d913342894ec7b066420952a618ec2a8269bc288ac00000000"; + "010000000001072b50f5750fc84801618b558e79d8e3c37d2c513c59cdbb590d90d043d3a9aa83000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffbcf5ae0a0c5efd15b06b380d59e535cf4fec65150ad301906012ea796f3a4607000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4be773c0882999ba5ae2ee574951f926a72037637aeda4344fa37655769ed78d000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff4b29cb407a116c0c26c73332a3a2dd5e878b3c66dd0bdc516c9b763344cb8119000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff0b76aceb79e3f2570b82fe4ec5709b5affd04ac9bf7898fdee4a5dac5a429c82000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff5469f86d1c2ceb0d7da957f6d82d98f3b0f3e4138800fb9f2fb23444476c4ed7000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffffc1fb64b4a9153728ccdfb8f5a2f0d60d42644bb39f0be24408f181c369c9337f000000001716001496de4122da32c2d428e70b44f8d07f2b26334b6ff0ffffff028c43e32e00000000160014455980d0b130ff894536fa79a322db815ab9cde4d9efb0000000000017a914424f29a8a84fa867814ff9ded43379c9dc9a6814870248304502210096990ba2bb32d80dc35ee55548bb300237744e9fdf08fcaf918672c60698da7b02202fe20657730114cdcbe01eef860b537be0b34380505d3d891d741ef2344330ca012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022010699f80eb6495d01ce1b00c70dacc76ed8ea5b1652f9cb04b91456d94cfafe30220111ca2bbb5192ec35247cb6250d1a68f84bf93fe0151f330f20077d3b9b4b5af012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd6024730440220170abcd7371d5ac83afff93751e3ea028627d1b38f4bed7da2c897d73d61420402205af8871c7befa740f325deb83483c1450dbcf5388e2836582c8bdaab8bf6a35b012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100b0702718bc906cf8691b7cbc95baf64e0ab3312a2abfd139b1928715757c0dbc022054f45fbc4cd9517eca6da4771dd8c23e5b0d393e61f5b7dd488086fe24029f80012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100d8055802be7aabefc62c6f5da69ee556be064c9727c8eab4d6e26ced2f81472c0220294599ad7b030c908923cb3ae1227b2b3f91275e3c00bc4943cdfe1c5e0b6843012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602473044022011a60277e781e891fce3dc920d0f2a0785b0561eb033c54a15a5c06e7c8bfa5a02207bec23c576fcafc1da59936dd6d35e526d3631f20e089f199245a1c4dcf96bb6012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd602483045022100c58ee4955ec0d8d84e1b89e4dd4435ab02776dcf5b3b7cb45b837483c157be1002201733ad33460d48e69558bfac47fe045194b3840046c80e4005daff272401d030012102550e8b9eaa471d31c4a544a6aad1d8a3b6e4c4b127ddfdd629e85a888d3f8dd600000000"; final tr = BtcTransaction.fromRaw(raw); + print(tr.txId()); expect(tx.toHex(), raw); expect(tr.toHex(), raw); From 49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:15:34 -0300 Subject: [PATCH 11/62] chore: deps --- pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 059c368..20e5037 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,9 @@ dependencies: pointycastle: ^3.7.4 bip32: ^2.0.0 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 rxdart: ^0.28.0 intl: ^0.18.0 http: ^1.1.0 From 0fab5761517a6da7b87791fe8065b9312d8fe4c2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 6 Nov 2024 12:06:12 -0300 Subject: [PATCH 12/62] feat: new method strings --- lib/src/provider/service/electrum/methods.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/provider/service/electrum/methods.dart b/lib/src/provider/service/electrum/methods.dart index e5fe2df..bd4121a 100644 --- a/lib/src/provider/service/electrum/methods.dart +++ b/lib/src/provider/service/electrum/methods.dart @@ -56,8 +56,8 @@ class ElectrumRequestMethods { ElectrumRequestMethods._("blockchain.block.headers"); /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of blocks. - static const ElectrumRequestMethods estimateFee = - ElectrumRequestMethods._("blockchain.estimatefee"); + static const String estimateFeeMethod = "blockchain.estimatefee"; + static const ElectrumRequestMethods estimateFee = ElectrumRequestMethods._(estimateFeeMethod); /// Return the confirmed and unconfirmed history of a script hash. static const String getHistoryMethod = "blockchain.scripthash.get_history"; @@ -81,7 +81,8 @@ class ElectrumRequestMethods { static const ElectrumRequestMethods ping = ElectrumRequestMethods._("server.ping"); /// Identify the client to the server and negotiate the protocol version. Only the first server.version() message is accepted. - static const ElectrumRequestMethods version = ElectrumRequestMethods._("server.version"); + static const String versionMethod = "server.version"; + static const ElectrumRequestMethods version = ElectrumRequestMethods._(versionMethod); /// Subscribe to receive block headers when a new block is found. static const String headersSubscribeMethod = "blockchain.headers.subscribe"; From 1cf5abb68e17284c21936572d685972edaee9866 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 12:22:36 -0300 Subject: [PATCH 13/62] feat: ssl, historical mode --- .../api_provider/electrum_api_provider.dart | 26 +++---------------- .../methods/tweaks_subscribe.dart | 15 ++++++++--- .../provider/service/electrum/electrum.dart | 2 +- .../electrum/electrum_ssl_service.dart | 6 ++++- .../provider/service/electrum/service.dart | 9 +++++++ 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/lib/src/provider/api_provider/electrum_api_provider.dart b/lib/src/provider/api_provider/electrum_api_provider.dart index e9a348e..c9d59cf 100644 --- a/lib/src/provider/api_provider/electrum_api_provider.dart +++ b/lib/src/provider/api_provider/electrum_api_provider.dart @@ -2,6 +2,8 @@ import 'package:bitcoin_base/src/bitcoin/amount/amount.dart'; import 'package:bitcoin_base/src/provider/api_provider.dart'; import 'dart:async'; +import 'package:rxdart/rxdart.dart'; + typedef ListenerCallback = StreamSubscription Function( void Function(T)? onData, { Function? onError, @@ -32,34 +34,14 @@ class ElectrumApiProvider { } // Preserving generic type T in subscribe method - ListenerCallback? subscribe(ElectrumRequest request) { + BehaviorSubject? subscribe(ElectrumRequest request) { final id = ++_id; final params = request.toRequest(id); final subscription = rpc.subscribe(params); if (subscription == null) return null; - try { - // Create a transformer that uses the request's response handler - final stream = subscription.subscription.map(request.onResponse); - - // Return a properly typed listener callback - return ( - void Function(T)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - }; - } catch (_) { - return null; - } + return subscription.subscription; } Future> getFeeRates() async { diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart index 18cf2ee..61941c4 100644 --- a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -24,7 +24,11 @@ class ElectrumTweaksSubscribeResponse { final int block; final Map blockTweaks; - ElectrumTweaksSubscribeResponse({required this.block, required this.blockTweaks, this.message}); + ElectrumTweaksSubscribeResponse({ + required this.block, + required this.blockTweaks, + this.message, + }); factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { late int block; @@ -77,17 +81,22 @@ class ElectrumTweaksSubscribeResponse { class ElectrumTweaksSubscribe extends ElectrumRequest> { /// blockchain.tweaks.subscribe - ElectrumTweaksSubscribe({required this.height, required this.count}); + ElectrumTweaksSubscribe({ + required this.height, + required this.count, + required this.historicalMode, + }); final int height; final int count; + final bool historicalMode; @override String get method => ElectrumRequestMethods.tweaksSubscribe.method; @override List toJson() { - return [height, count]; + return [height, count, historicalMode]; } /// The header of the current block chain tip. diff --git a/lib/src/provider/service/electrum/electrum.dart b/lib/src/provider/service/electrum/electrum.dart index 8be4045..bf6e9cb 100644 --- a/lib/src/provider/service/electrum/electrum.dart +++ b/lib/src/provider/service/electrum/electrum.dart @@ -2,6 +2,6 @@ export 'methods.dart'; export 'params.dart'; export 'service.dart'; export 'request_completer.dart'; -// export 'electrum_ssl_service.dart'; +export 'electrum_ssl_service.dart'; export 'electrum_tcp_service.dart'; // export 'electrum_websocket_service.dart'; diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart index 57e1cbd..880e4d8 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -85,7 +85,11 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { final Duration connectionTimeOut = const Duration(seconds: 30), void Function(ConnectionStatus)? onConnectionStatusChange, }) async { - final channel = await SecureSocket.connect(uri.host, uri.port).timeout(connectionTimeOut); + final channel = await SecureSocket.connect( + uri.host, + uri.port, + onBadCertificate: (_) => true, + ).timeout(connectionTimeOut); return ElectrumSSLService._( uri.toString(), diff --git a/lib/src/provider/service/electrum/service.dart b/lib/src/provider/service/electrum/service.dart index 972641d..96090d7 100644 --- a/lib/src/provider/service/electrum/service.dart +++ b/lib/src/provider/service/electrum/service.dart @@ -38,6 +38,15 @@ abstract class BitcoinBaseElectrumRPCService { bool get isConnected; void disconnect(); void reconnect(); + static Future connect( + Uri uri, { + Iterable? protocols, + Duration defaultRequestTimeOut = const Duration(seconds: 30), + final Duration connectionTimeOut = const Duration(seconds: 30), + void Function(ConnectionStatus)? onConnectionStatusChange, + }) { + throw UnimplementedError(); + } } bool isJSONStringCorrect(String source) { From 3be1d3c1007e796023364fdc3fa925830135a86b Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:30:28 +0330 Subject: [PATCH 14/62] V4.9.2 Update dependencies --- CHANGELOG.md | 5 +++++ pubspec.yaml | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a086184..dc0f857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.9.2 + +* Update dependencies +* Resolved issue with transaction deserialization (unsigned tx) + ## 4.9.1 * Resolved issue with transaction deserialization (Issue #9) diff --git a/pubspec.yaml b/pubspec.yaml index 7f21203..3c57644 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bitcoin_base description: A versatile library for Bitcoin, Dogecoin, Litecoin, Dash, BSV, and BCH. Supports P2PKH, P2SH, P2WPKH, P2WSH, P2TR, with advanced creation, signing, and spending capabilities. -version: 4.9.1 +version: 4.9.2 homepage: "https://github.com/mrtnetwork/bitcoin_base" repository: "https://github.com/mrtnetwork/bitcoin_base" Author: mrhaydari.t@gmail.com @@ -16,10 +16,10 @@ environment: dependencies: - blockchain_utils: ^3.4.0 + # blockchain_utils: ^3.4.0 - # blockchain_utils: - # path: ../blockchain_utils + blockchain_utils: + path: ../blockchain_utils dev_dependencies: From 89caf39a19f4f0eb79a3642bd0e918b16cc1119b Mon Sep 17 00:00:00 2001 From: Mohsen <56779182+mrtnetwork@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:31:44 +0330 Subject: [PATCH 15/62] Update dependencies --- example/pubspec.lock | 6 +++--- example/pubspec.yaml | 2 +- pubspec.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 14f2df9..b2187bd 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,15 +15,15 @@ packages: path: ".." relative: true source: path - version: "4.9.0" + version: "4.9.2" blockchain_utils: dependency: "direct main" description: name: blockchain_utils - sha256: ebb6c336ba0120de0982c50d8bc597cb494a530bd22bd462895bb5cebde405af + sha256: eebd06ff3709be5c660be72f0b0ee8f524fbb9e20752436f19f07f3be8ee6d1f url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.5.0" boolean_selector: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2fad8b2..bce4f5a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: path: ../ # blockchain_utils: # path: ../../blockchain_utils - blockchain_utils: ^3.4.0 + blockchain_utils: ^3.5.0 http: ^1.2.0 dev_dependencies: diff --git a/pubspec.yaml b/pubspec.yaml index 3c57644..73f4adf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,10 @@ environment: dependencies: - # blockchain_utils: ^3.4.0 + blockchain_utils: ^3.5.0 - blockchain_utils: - path: ../blockchain_utils + # blockchain_utils: + # path: ../blockchain_utils dev_dependencies: From 7b202b8e39f9a15f72f128cd9aa8530a0ceb21c0 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 22 Nov 2024 18:40:20 -0300 Subject: [PATCH 16/62] feat: fix disconnect & find unsupported methods --- .../methods/tweaks_subscribe.dart | 19 +++++++++-- .../electrum/electrum_ssl_service.dart | 32 +++++++++++++++---- .../electrum/electrum_tcp_service.dart | 32 +++++++++++++++---- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart index 61941c4..b2f6bea 100644 --- a/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart +++ b/lib/src/provider/electrum_methods/methods/tweaks_subscribe.dart @@ -30,7 +30,20 @@ class ElectrumTweaksSubscribeResponse { this.message, }); - factory ElectrumTweaksSubscribeResponse.fromJson(Map json) { + static ElectrumTweaksSubscribeResponse? fromJson(Map json) { + if (json.isEmpty) { + return null; + } + + if (json.containsKey('params')) { + final params = json['params'] as List; + final message = params.first["message"]; + + if (message != null) { + return null; + } + } + late int block; final blockTweaks = {}; @@ -79,7 +92,7 @@ class ElectrumTweaksSubscribeResponse { /// Subscribe to receive block headers when a new block is found. /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html class ElectrumTweaksSubscribe - extends ElectrumRequest> { + extends ElectrumRequest> { /// blockchain.tweaks.subscribe ElectrumTweaksSubscribe({ required this.height, @@ -101,7 +114,7 @@ class ElectrumTweaksSubscribe /// The header of the current block chain tip. @override - ElectrumTweaksSubscribeResponse onResponse(result) { + ElectrumTweaksSubscribeResponse? onResponse(result) { return ElectrumTweaksSubscribeResponse.fromJson(result); } } diff --git a/lib/src/provider/service/electrum/electrum_ssl_service.dart b/lib/src/provider/service/electrum/electrum_ssl_service.dart index 880e4d8..2e3c8ab 100644 --- a/lib/src/provider/service/electrum/electrum_ssl_service.dart +++ b/lib/src/provider/service/electrum/electrum_ssl_service.dart @@ -66,7 +66,7 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { _subscription?.cancel().catchError((e) {}); _subscription = null; - onConnectionStatusChange = null; + _setConnectionStatus(ConnectionStatus.disconnected); } void _onDone() { @@ -138,10 +138,26 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { } void _handleResponse(Map response) { - var id = response['id'] as int?; + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + if (response["error"] != null) { + final message = error["message"]; + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + _tasks.forEach((key, value) { - if (value.request.method == response['method']) { + if (value.request.method == method) { id = key; } }); @@ -149,10 +165,7 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { try { final result = _findResult(response, _tasks[id]!.request); - - if (result != null) { - _finish(id!, result); - } + _finish(id!, result); } catch (_) {} } @@ -186,6 +199,11 @@ class ElectrumSSLService implements BitcoinBaseElectrumRPCService { data: data["error"]?["data"], request: data["request"] ?? request.params, ); + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } } throw _errors[request.id]!; diff --git a/lib/src/provider/service/electrum/electrum_tcp_service.dart b/lib/src/provider/service/electrum/electrum_tcp_service.dart index ea965af..bddecb1 100644 --- a/lib/src/provider/service/electrum/electrum_tcp_service.dart +++ b/lib/src/provider/service/electrum/electrum_tcp_service.dart @@ -66,7 +66,7 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { _subscription?.cancel().catchError((e) {}); _subscription = null; - onConnectionStatusChange = null; + _setConnectionStatus(ConnectionStatus.disconnected); } void _onDone() { @@ -134,10 +134,26 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { } void _handleResponse(Map response) { - var id = response['id'] as int?; + var id = response['id'] == null ? null : int.parse(response['id']!.toString()); + if (id == null) { + String? method = response['method']; + + if (method == null) { + final error = response["error"]; + if (response["error"] != null) { + final message = error["message"]; + if (message != null) { + final isFulcrum = message.toLowerCase().contains("unsupported request"); + final match = (isFulcrum ? RegExp(r'request:\s*(\S+)') : RegExp(r'"([^"]*)"')) + .firstMatch(message); + method = match?.group(1) ?? ''; + } + } + } + _tasks.forEach((key, value) { - if (value.request.method == response['method']) { + if (value.request.method == method) { id = key; } }); @@ -145,10 +161,7 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { try { final result = _findResult(response, _tasks[id]!.request); - - if (result != null) { - _finish(id!, result); - } + _finish(id!, result); } catch (_) {} } @@ -182,6 +195,11 @@ class ElectrumTCPService implements BitcoinBaseElectrumRPCService { data: data["error"]?["data"], request: data["request"] ?? request.params, ); + + if (message.toLowerCase().contains("unknown method") || + message.toLowerCase().contains("unsupported request")) { + return {}; + } } throw _errors[request.id]!; From bff40edbeb609fa51fa23e572eb7b6f3e99e65af Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 24 Nov 2024 19:05:44 -0300 Subject: [PATCH 17/62] fix: electrum protocol version --- .../provider/electrum_methods/methods/electrum_version.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/provider/electrum_methods/methods/electrum_version.dart b/lib/src/provider/electrum_methods/methods/electrum_version.dart index 5fb0e63..d080b7d 100644 --- a/lib/src/provider/electrum_methods/methods/electrum_version.dart +++ b/lib/src/provider/electrum_methods/methods/electrum_version.dart @@ -8,8 +8,7 @@ class ElectrumVersion extends ElectrumRequest, List> { /// A string identifying the connecting client software. final String clientName; - /// An array [protocol_min, protocol_max], each of which is a string. - final List protocolVersion; + final String protocolVersion; /// blockchain.version @override From 82306ae21ea247ba400b9dc3823631d69ae45699 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 25 Nov 2024 12:45:22 -0300 Subject: [PATCH 18/62] chore: intl dep --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 20e5037..d14b166 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 rxdart: ^0.28.0 - intl: ^0.18.0 + intl: ^0.19.0 http: ^1.1.0 From 997762fa354e3ac70dfd7061f900232434eb43a1 Mon Sep 17 00:00:00 2001 From: Sandro Simas Date: Fri, 29 Nov 2024 18:11:40 -0300 Subject: [PATCH 19/62] Small fix in omnixep network description --- lib/src/models/network.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index 3280519..fd293a4 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -495,7 +495,7 @@ class PepeNetwork implements BasedUtxoNetwork { } } -/// Class representing a Dogecoin network, implementing the `BasedUtxoNetwork` abstract class. +/// Class representing a OmniXEP network, implementing the `BasedUtxoNetwork` abstract class. class OmniXepNetwork implements BasedUtxoNetwork { /// Mainnet configuration with associated `CoinConf`. static const OmniXepNetwork mainnet = @@ -505,7 +505,7 @@ class OmniXepNetwork implements BasedUtxoNetwork { @override final CoinConf conf; - /// Constructor for creating a Dogecoin network with a specific configuration. + /// Constructor for creating a OmniXEP network with a specific configuration. const OmniXepNetwork._(this.value, this.conf); @override From b1c3ebf0dbb783dde4ab067bd0922b2cfd243701 Mon Sep 17 00:00:00 2001 From: Sandro Simas Date: Fri, 29 Nov 2024 18:12:35 -0300 Subject: [PATCH 20/62] Small fix in omnixep network description --- lib/src/models/network.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/network.dart b/lib/src/models/network.dart index fd293a4..d8194c6 100644 --- a/lib/src/models/network.dart +++ b/lib/src/models/network.dart @@ -544,4 +544,4 @@ class OmniXepNetwork implements BasedUtxoNetwork { } return []; } -} \ No newline at end of file +} From 1e1b0d46eb70bbb3c7b047942481c735cfe620d4 Mon Sep 17 00:00:00 2001 From: Sandro Simas Date: Fri, 29 Nov 2024 18:13:01 -0300 Subject: [PATCH 21/62] Updating pubspec --- pubspec.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 66b442c..73f4adf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,6 @@ topics: - bitcoincash - dash -publish_to: 'none' - environment: sdk: '>=2.15.0 <4.0.0' From 9e22e516daf0598cad328c8536e851a5a7d47e32 Mon Sep 17 00:00:00 2001 From: Mohsen Date: Sat, 30 Nov 2024 21:59:25 +0330 Subject: [PATCH 22/62] Support for uncompressed non-SegWit addresses --- ...amples_multisig_taproot_legacy_uncomp.dart | 131 ++++++++++++++++ .../global/old_examples/bitcoin_example.dart | 8 +- .../multi_sig_transactions.dart | 2 +- .../transaction_builder_example.dart | 2 +- ...er_from_7_account_to_6_accout_example.dart | 2 +- .../global/transfer_to_8_account_example.dart | 2 +- .../electrum/electrum_ssl_service.dart | 1 + example/pubspec.lock | 7 +- example/pubspec.yaml | 6 +- example/test/bch_test.dart | 1 + example/test/btc2_test.dart | 1 + example/test/btc_test.dart | 118 +++++++++++++++ example/test/e_test.dart | 1 + example/test/widget_test.dart | 98 +++++++++++- lib/src/bitcoin/address/core.dart | 37 +++-- lib/src/bitcoin/script/input.dart | 51 ++++--- lib/src/bitcoin/script/op_code/constant.dart | 9 ++ lib/src/bitcoin/script/op_code/tools.dart | 24 +-- lib/src/bitcoin/script/output.dart | 26 ++-- lib/src/bitcoin/script/script.dart | 69 +++++---- lib/src/bitcoin/script/transaction.dart | 33 ++-- lib/src/bitcoin/script/witness.dart | 8 +- lib/src/cash_token/cash_token.dart | 9 ++ lib/src/crypto/keypair/ec_public.dart | 26 ++-- lib/src/provider/models/multisig_script.dart | 34 +++-- lib/src/provider/models/utxo_details.dart | 106 +++++++------ .../provider/service/electrum/exception.dart | 51 ------- .../forked_transaction_builder.dart | 142 ++++++++++-------- .../transaction_builder.dart | 92 ++++++------ lib/src/utils/btc_utils.dart | 5 + pubspec.yaml | 2 +- test/deserialize_test.dart | 21 +++ test/p2tr_test.dart | 8 + 33 files changed, 773 insertions(+), 360 deletions(-) create mode 100644 example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart create mode 100644 example/test/bch_test.dart create mode 100644 example/test/btc2_test.dart create mode 100644 example/test/btc_test.dart create mode 100644 example/test/e_test.dart delete mode 100644 lib/src/provider/service/electrum/exception.dart create mode 100644 test/deserialize_test.dart diff --git a/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart new file mode 100644 index 0000000..1bd9fc1 --- /dev/null +++ b/example/lib/global/btc_examples_multisig_taproot_legacy_uncomp.dart @@ -0,0 +1,131 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:example/services_examples/electrum/electrum_ssl_service.dart'; + +void main() async { + final one = ECPrivate.fromBytes(List.filled(32, 12)); + final two = ECPrivate.fromBytes(List.filled(32, 13)); + final three = ECPrivate.fromBytes(List.filled(32, 14)); + final four = ECPrivate.fromBytes(List.filled(32, 15)); + final five = ECPrivate.fromBytes(List.filled(32, 16)); + final six = ECPrivate.fromBytes(List.filled(32, 17)); + final seven = ECPrivate.fromBytes(List.filled(32, 18)); + final eight = ECPrivate.fromBytes(List.filled(32, 19)); + final Map keys = { + for (final i in [one, two, three, four, five, six, seven, eight]) + i.getPublic().toHex(): i + }; + final account = MultiSignatureAddress(threshold: 8, signers: [ + MultiSignatureSigner( + publicKey: one.getPublic().toHex(compressed: false), weight: 1), + MultiSignatureSigner(publicKey: two.getPublic().toHex(), weight: 1), + MultiSignatureSigner(publicKey: three.getPublic().toHex(), weight: 1), + MultiSignatureSigner( + publicKey: four.getPublic().toHex(compressed: false), weight: 1), + MultiSignatureSigner(publicKey: five.getPublic().toHex(), weight: 1), + MultiSignatureSigner(publicKey: six.getPublic().toHex(), weight: 1), + MultiSignatureSigner( + publicKey: seven.getPublic().toHex(compressed: false), weight: 1), + MultiSignatureSigner(publicKey: eight.getPublic().toHex(), weight: 1), + ]); + + /// connect to electrum service with websocket + /// please see `services_examples` folder for how to create electrum websocket service + final service = await ElectrumSSLService.connect( + "testnet4-electrumx.wakiyamap.dev:51002"); + + /// create provider with service + final provider = ElectrumApiProvider(service); + + final addrOne = one.getPublic().toP2pkAddress(compressed: false); + + final addrTwo = two.getPublic().toAddress(compressed: false); + + final addrThree = three.getPublic().toP2pkInP2sh(compressed: false); + final addrFour = four.getPublic().toP2pkhInP2sh(compressed: false); + final addrFive = four.getPublic().toSegwitAddress(); + final addrSix = account.toP2shAddress(); + final addr7 = eight.getPublic().toTaprootAddress(); + final addr8 = eight.getPublic().toP2wshAddress(); + final addr9 = eight.getPublic().toP2wshInP2sh(); + final List pubkys = [ + one.getPublic().toHex(compressed: false), + two.getPublic().toHex(compressed: false), + three.getPublic().toHex(compressed: false), + four.getPublic().toHex(compressed: false), + four.getPublic().toHex(), + four.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + eight.getPublic().toHex(), + ]; + final addresses = [ + one.getPublic().toP2pkAddress(compressed: false), + two.getPublic().toAddress(compressed: false), + three.getPublic().toP2pkInP2sh(compressed: false), + four.getPublic().toP2pkhInP2sh(compressed: false), + four.getPublic().toSegwitAddress(), + four.getPublic().toP2wshInP2sh(), + addrSix, + addr7, + addr8, + addr9 + ]; + List utxos = []; + for (int i = 0; i < addresses.length; i++) { + final address = addresses[i]; + final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + scriptHash: address.pubKeyHash(), includeTokens: false)); + if (elctrumUtxos.isEmpty) continue; + if (i == 6) { + utxos.addAll(elctrumUtxos.map((e) => UtxoWithAddress( + utxo: e.toUtxo(address.type), + ownerDetails: UtxoAddressDetails.multiSigAddress( + multiSigAddress: account, address: address)))); + continue; + } + utxos.addAll(elctrumUtxos + .map((e) => UtxoWithAddress( + utxo: e.toUtxo(address.type), + ownerDetails: + UtxoAddressDetails(publicKey: pubkys[i], address: address))) + .toList()); + } + + final sumOfUtxo = utxos.sumOfUtxosValue(); + + if (sumOfUtxo == BigInt.zero) { + return; + } + final change = + sumOfUtxo - (BigInt.from(1000) * BigInt.from(11) + BigInt.from(4295)); + final bchTransaction = BitcoinTransactionBuilder(outPuts: [ + /// change input (sumofutxos - spend) + BitcoinOutput(address: addrOne, value: change), + BitcoinOutput(address: addrOne, value: BigInt.from(1000)), + BitcoinOutput(address: addrTwo, value: BigInt.from(1000)), + BitcoinOutput(address: addrThree, value: BigInt.from(1000)), + BitcoinOutput(address: addrFour, value: BigInt.from(1000)), + BitcoinOutput(address: addrFour, value: BigInt.from(1000)), + BitcoinOutput(address: addrFive, value: BigInt.from(1000)), + BitcoinOutput(address: addrSix, value: BigInt.from(1000)), + BitcoinOutput(address: addrSix, value: BigInt.from(1000)), + BitcoinOutput(address: addr7, value: BigInt.from(1000)), + BitcoinOutput(address: addr8, value: BigInt.from(1000)), + BitcoinOutput(address: addr9, value: BigInt.from(1000)), + ], fee: BigInt.from(4295), network: BitcoinNetwork.testnet, utxos: utxos); + final transaaction = + bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { + final pk = ECPublic.fromHex(publicKey); + if (utxo.utxo.isP2tr) { + return keys[pk.toHex()]!.signTapRoot(trDigest, sighash: sighash); + } + return keys[pk.toHex()]!.signInput(trDigest, sigHash: sighash); + }); + + final transactionRaw = transaaction.toHex(); + await provider + .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); +} + +/// https://mempool.space/testnet4/tx/a7f08f07739de45a6a4f8871f8e6ad79e0aefbc940086df76571354ba22263fa diff --git a/example/lib/global/old_examples/bitcoin_example.dart b/example/lib/global/old_examples/bitcoin_example.dart index 0ef7b04..98c3412 100644 --- a/example/lib/global/old_examples/bitcoin_example.dart +++ b/example/lib/global/old_examples/bitcoin_example.dart @@ -166,7 +166,7 @@ void _spendFromP2pkhTo10DifferentType() async { /// and sign the transaction digest to construct the unlocking script. if (publicKey == examplePublicKey2.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return examplePrivateKey.signTapRoot(trDigest); } return examplePrivateKey.signInput(trDigest, sigHash: sighash); @@ -400,19 +400,19 @@ void _spendFrom10DifferentTypeToP2pkh() async { /// For each input in the transaction, locate the corresponding private key /// and sign the transaction digest to construct the unlocking script. if (publicKey == childKey1PublicKey.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return childKey1PrivateKey.signTapRoot(trDigest, sighash: sighash); } return childKey1PrivateKey.signInput(trDigest, sigHash: sighash); } if (publicKey == examplePublicKey.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return childKey2PrivateKey.signTapRoot(trDigest, sighash: sighash); } return childKey2PrivateKey.signInput(trDigest, sigHash: sighash); } if (publicKey == examplePublicKey2.toHex()) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return examplePrivateKey.signTapRoot(trDigest, sighash: sighash); } return examplePrivateKey.signInput(trDigest, sigHash: sighash); diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart b/example/lib/global/old_examples/spending_with_transaction_builder/multi_sig_transactions.dart index 6b4542e..8166e85 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 @@ -265,7 +265,7 @@ void main() async { // Ok, now we have the private key, we need to check which method to use for signing // We check whether the UTX corresponds to the P2TR address or not. - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. diff --git a/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart b/example/lib/global/old_examples/spending_with_transaction_builder/transaction_builder_example.dart index 5da3143..a303df7 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 @@ -212,7 +212,7 @@ void main() async { // Ok, now we have the private key, we need to check which method to use for signing // We check whether the UTX corresponds to the P2TR address or not. - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { // yes is p2tr utxo and now we use SignTaprootTransaction(Schnorr sign) // for now this transaction builder support only tweak transaction // If you want to spend a Taproot script-path spending, you must create your own transaction builder. diff --git a/example/lib/global/transfer_from_7_account_to_6_accout_example.dart b/example/lib/global/transfer_from_7_account_to_6_accout_example.dart index 295d906..61d02cc 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 @@ -158,7 +158,7 @@ void main() async { /// create transaction and sign it final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return privateKey.signTapRoot(trDigest, sighash: sighash); } return privateKey.signInput(trDigest, sigHash: sighash); diff --git a/example/lib/global/transfer_to_8_account_example.dart b/example/lib/global/transfer_to_8_account_example.dart index 6830feb..235c673 100644 --- a/example/lib/global/transfer_to_8_account_example.dart +++ b/example/lib/global/transfer_to_8_account_example.dart @@ -136,7 +136,7 @@ void main() async { /// create transaction and sign it final transaction = builder.buildTransaction((trDigest, utxo, publicKey, sighash) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return examplePrivateKey2.signTapRoot(trDigest, sighash: sighash); } return examplePrivateKey2.signInput(trDigest, sigHash: sighash); diff --git a/example/lib/services_examples/electrum/electrum_ssl_service.dart b/example/lib/services_examples/electrum/electrum_ssl_service.dart index 362c306..cf404a1 100644 --- a/example/lib/services_examples/electrum/electrum_ssl_service.dart +++ b/example/lib/services_examples/electrum/electrum_ssl_service.dart @@ -68,6 +68,7 @@ class ElectrumSSLService with BitcoinBaseElectrumRPCService { } void _onMessge(List event) { + print("Event ${utf8.decode(event)}"); final Map decode = json.decode(utf8.decode(event)); if (decode.containsKey("id")) { final int id = int.parse(decode["id"]!.toString()); diff --git a/example/pubspec.lock b/example/pubspec.lock index b2187bd..7873d10 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -19,10 +19,9 @@ packages: blockchain_utils: dependency: "direct main" description: - name: blockchain_utils - sha256: eebd06ff3709be5c660be72f0b0ee8f524fbb9e20752436f19f07f3be8ee6d1f - url: "https://pub.dev" - source: hosted + path: "../../blockchain_utils" + relative: true + source: path version: "3.5.0" boolean_selector: dependency: transitive diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bce4f5a..c2fdaea 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -37,9 +37,9 @@ dependencies: cupertino_icons: ^1.0.2 bitcoin_base: path: ../ - # blockchain_utils: - # path: ../../blockchain_utils - blockchain_utils: ^3.5.0 + blockchain_utils: + path: ../../../blockchain_utils + # blockchain_utils: ^3.5.0 http: ^1.2.0 dev_dependencies: diff --git a/example/test/bch_test.dart b/example/test/bch_test.dart new file mode 100644 index 0000000..f76cc54 --- /dev/null +++ b/example/test/bch_test.dart @@ -0,0 +1 @@ +void main() async {} diff --git a/example/test/btc2_test.dart b/example/test/btc2_test.dart new file mode 100644 index 0000000..f76cc54 --- /dev/null +++ b/example/test/btc2_test.dart @@ -0,0 +1 @@ +void main() async {} diff --git a/example/test/btc_test.dart b/example/test/btc_test.dart new file mode 100644 index 0000000..4690921 --- /dev/null +++ b/example/test/btc_test.dart @@ -0,0 +1,118 @@ +void main() async { + // final one = ECPrivate.fromBytes(List.filled(32, 12)); + // final two = ECPrivate.fromBytes(List.filled(32, 13)); + // final three = ECPrivate.fromBytes(List.filled(32, 14)); + // final four = ECPrivate.fromBytes(List.filled(32, 15)); + // final five = ECPrivate.fromBytes(List.filled(32, 16)); + // final six = ECPrivate.fromBytes(List.filled(32, 17)); + // final seven = ECPrivate.fromBytes(List.filled(32, 18)); + // final eight = ECPrivate.fromBytes(List.filled(32, 19)); + // final Map keys = { + // for (final i in [one, two, three, four, five, six, seven, eight]) + // i.getPublic().toHex(): i + // }; + // final account = MultiSignatureAddress(threshold: 8, signers: [ + // MultiSignatureSigner(publicKey: one.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: two.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: three.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: four.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: five.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: six.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: seven.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: eight.getPublic().toHex(), weight: 1), + // ]); + // final addr = account.toP2shAddress(); + // // return; + + // /// connect to electrum service with websocket + // /// please see `services_examples` folder for how to create electrum websocket service + // final service = await ElectrumSSLService.connect( + // "testnet4-electrumx.wakiyamap.dev:51002"); + + // /// create provider with service + // final provider = ElectrumApiProvider(service); + + // final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + // scriptHash: addr.pubKeyHash(), + // includeTokens: false, + // )); + // final List utxos = elctrumUtxos + // .map((e) => UtxoWithAddress( + // utxo: e.toUtxo(addr.type), + // ownerDetails: UtxoAddressDetails.multiSigAddress( + // multiSigAddress: account, address: addr))) + // .toList(); + + // final sumOfUtxo = utxos.sumOfUtxosValue(); + // if (sumOfUtxo == BigInt.zero) { + // return; + // } + // final addrOne = one.getPublic().toP2pkAddress(compressed: false); + // final addrTwo = two.getPublic().toAddress(compressed: false); + // final addrThree = three.getPublic().toP2pkInP2sh(compressed: false); + // final addrFour = four.getPublic().toP2pkhInP2sh(compressed: false); + // final addrFive = four.getPublic().toSegwitAddress(); + // final addrSix = four.getPublic().toP2wshInP2sh(); + // // print(sumOfUtxo); + // // return; + // final bchTransaction = BitcoinTransactionBuilder( + // outPuts: [ + // /// change input (sumofutxos - spend) + // BitcoinOutput( + // address: addr, + // value: sumOfUtxo - + // (BigInt.from(1000) * BigInt.from(7) + BigInt.from(2200)), + // ), + // BitcoinOutput( + // address: addrOne, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrTwo, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrThree, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrFour, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrFour, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrFive, + // value: BigInt.from(1000), + // ), + // BitcoinOutput( + // address: addrSix, + // value: BigInt.from(1000), + // ), + // ], + // fee: BigInt.from(2200), + // network: BitcoinNetwork.testnet, + // utxos: utxos, + // ); + // final transaaction = + // bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { + // return keys[publicKey]!.signInput(trDigest, sigHash: sighash); + // }); + + // /// transaction ID + // transaaction.txId(); + + // /// for calculation fee + // print(transaaction.getSize()); + // // return; + + // /// raw of encoded transaction in hex + // final transactionRaw = transaaction.toHex(); + // print(transactionRaw); + // // print(elctrumUtxos.length); + // final d = await provider + // .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + // print(d); +} diff --git a/example/test/e_test.dart b/example/test/e_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/example/test/e_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index f76cc54..4f8fc15 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -1 +1,97 @@ -void main() async {} +void main() async { + // final one = ECPrivate.fromBytes(List.filled(32, 12)); + // final two = ECPrivate.fromBytes(List.filled(32, 13)); + // final three = ECPrivate.fromBytes(List.filled(32, 14)); + // final four = ECPrivate.fromBytes(List.filled(32, 15)); + // final five = ECPrivate.fromBytes(List.filled(32, 16)); + // final six = ECPrivate.fromBytes(List.filled(32, 17)); + // final seven = ECPrivate.fromBytes(List.filled(32, 18)); + // final eight = ECPrivate.fromBytes(List.filled(32, 19)); + // final Map keys = { + // for (final i in [one, two, three, four, five, six, seven, eight]) + // i.getPublic().toHex(): i + // }; + // final account = MultiSignatureAddress(threshold: 8, signers: [ + // MultiSignatureSigner(publicKey: one.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: two.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: three.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: four.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: five.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: six.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: seven.getPublic().toHex(), weight: 1), + // MultiSignatureSigner(publicKey: eight.getPublic().toHex(), weight: 1), + // ]); + // final addr = account.toP2shAddress(); + // // print(account.multiSigScript.script); + // // return; + + // /// connect to electrum service with websocket + // /// please see `services_examples` folder for how to create electrum websocket service + // final service = await ElectrumWebSocketService.connect( + // "wss://chipnet.imaginary.cash:50004"); + + // /// create provider with service + // final provider = ElectrumApiProvider(service); + + // final elctrumUtxos = await provider.request(ElectrumScriptHashListUnspent( + // scriptHash: addr.pubKeyHash(), + // includeTokens: false, + // )); + // final List utxos = elctrumUtxos + // .map((e) => UtxoWithAddress( + // utxo: e.toUtxo(addr.type), + // ownerDetails: UtxoAddressDetails.multiSigAddress( + // multiSigAddress: account, address: addr))) + // .toList(); + + // final sumOfUtxo = utxos.sumOfUtxosValue(); + // if (sumOfUtxo == BigInt.zero) { + // return; + // } + // final bchTransaction = ForkedTransactionBuilder( + // outPuts: [ + // /// change input (sumofutxos - spend) + // BitcoinOutput( + // address: addr, + // value: sumOfUtxo - + // (BtcUtils.toSatoshi("0.0001") + BtcUtils.toSatoshi("0.00003")), + // ), + // BitcoinOutput( + // address: BitcoinCashAddress( + // "bchtest:pw054wtjjc70rrvx4ftl4p63gluedyt0qmpgz705f8x3gxygrzarzls7vp2sj", + // network: BitcoinCashNetwork.testnet) + // .baseAddress, + // value: BtcUtils.toSatoshi("0.0001"), + // ), + // ], + // fee: BtcUtils.toSatoshi("0.00003"), + // network: BitcoinCashNetwork.testnet, + // utxos: utxos, + // ); + // final transaaction = + // bchTransaction.buildTransaction((trDigest, utxo, publicKey, sighash) { + // final sing = keys[publicKey]!.signInput(trDigest, sigHash: sighash); + // print("====================="); + // print("key ${keys[publicKey]?.prive.toHex()}"); + // print("diget ${BytesUtils.toHexString(trDigest)}"); + // print("sign ${sing}"); + // print("Sighash $sighash"); + + // return keys[publicKey]!.signInput(trDigest, sigHash: sighash); + // }); + + // /// transaction ID + // transaaction.txId(); + + // /// for calculation fee + // transaaction.getSize(); + + // /// raw of encoded transaction in hex + // final transactionRaw = transaaction.toHex(); + // print(transactionRaw); + // return; + // // print(elctrumUtxos.length); + // final d = await provider + // .request(ElectrumBroadCastTransaction(transactionRaw: transactionRaw)); + // print(d); +} diff --git a/lib/src/bitcoin/address/core.dart b/lib/src/bitcoin/address/core.dart index 51a04e8..a8a1bd7 100644 --- a/lib/src/bitcoin/address/core.dart +++ b/lib/src/bitcoin/address/core.dart @@ -36,6 +36,14 @@ abstract class BitcoinAddressType implements Enumerate { P2shAddressType.p2pkInP2shwt, P2pkhAddressType.p2pkhwt ]; + T cast() { + if (this is! T) { + throw DartBitcoinPluginException("BitcoinAddressType casting failed.", + details: {"excepted": "$T", "type": value}); + } + return this as T; + } + @override String toString() { return "BitcoinAddressType.$value"; @@ -50,15 +58,14 @@ abstract class BitcoinBaseAddress { String get addressProgram; } -class PubKeyAddressType implements BitcoinAddressType { - const PubKeyAddressType._(this.value); +class PubKeyAddressType extends BitcoinAddressType { + const PubKeyAddressType._(String value) : super._(value); static const PubKeyAddressType p2pk = PubKeyAddressType._("P2PK"); @override bool get isP2sh => false; @override bool get isSegwit => false; - @override - final String value; + @override int get hashLength => 20; @override @@ -67,8 +74,8 @@ class PubKeyAddressType implements BitcoinAddressType { } } -class P2pkhAddressType implements BitcoinAddressType { - const P2pkhAddressType._(this.value); +class P2pkhAddressType extends BitcoinAddressType { + const P2pkhAddressType._(String value) : super._(value); static const P2pkhAddressType p2pkh = P2pkhAddressType._("P2PKH"); static const P2pkhAddressType p2pkhwt = P2pkhAddressType._("P2PKHWT"); @@ -77,9 +84,6 @@ class P2pkhAddressType implements BitcoinAddressType { @override bool get isSegwit => false; - @override - final String value; - @override int get hashLength => 20; @override @@ -88,8 +92,9 @@ class P2pkhAddressType implements BitcoinAddressType { } } -class P2shAddressType implements BitcoinAddressType { - const P2shAddressType._(this.value, this.hashLength, this.withToken); +class P2shAddressType extends BitcoinAddressType { + const P2shAddressType._(String value, this.hashLength, this.withToken) + : super._(value); static const P2shAddressType p2wshInP2sh = P2shAddressType._( "P2SH/P2WSH", _BitcoinAddressUtils.hash160DigestLength, false); static const P2shAddressType p2wpkhInP2sh = P2shAddressType._( @@ -131,17 +136,14 @@ class P2shAddressType implements BitcoinAddressType { static const P2shAddressType p2pkInP2shwt = P2shAddressType._( "P2SHWT/P2PK", _BitcoinAddressUtils.hash160DigestLength, true); - @override - final String value; - @override String toString() { return "P2shAddressType.$value"; } } -class SegwitAddresType implements BitcoinAddressType { - const SegwitAddresType._(this.value); +class SegwitAddresType extends BitcoinAddressType { + const SegwitAddresType._(String value) : super._(value); static const SegwitAddresType p2wpkh = SegwitAddresType._("P2WPKH"); static const SegwitAddresType p2tr = SegwitAddresType._("P2TR"); static const SegwitAddresType p2wsh = SegwitAddresType._("P2WSH"); @@ -150,9 +152,6 @@ class SegwitAddresType implements BitcoinAddressType { @override bool get isSegwit => true; - @override - final String value; - @override int get hashLength { switch (this) { diff --git a/lib/src/bitcoin/script/input.dart b/lib/src/bitcoin/script/input.dart index c9d0aee..70d2b57 100644 --- a/lib/src/bitcoin/script/input.dart +++ b/lib/src/bitcoin/script/input.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/utils/utils.dart'; @@ -42,50 +44,53 @@ class TxInput { List toBytes() { final txidBytes = BytesUtils.fromHexString(txId).reversed.toList(); - final txoutBytes = List.filled(4, 0); - writeUint32LE(txIndex, txoutBytes); + final txoutBytes = + IntUtils.toBytes(txIndex, length: 4, byteOrder: Endian.little); + // writeUint32LE(txIndex, txoutBytes); final scriptSigBytes = scriptSig.toBytes(); - final scriptSigLengthVarint = IntUtils.encodeVarint(scriptSigBytes.length); final data = List.from([ ...txidBytes, ...txoutBytes, ...scriptSigLengthVarint, ...scriptSigBytes, - ...sequence, + ...sequence ]); return data; } - static Tuple fromRaw( - {required String raw, int cursor = 0, bool hasSegwit = false}) { - final txInputRaw = BytesUtils.fromHexString(raw); - List inpHash = - txInputRaw.sublist(cursor, cursor + 32).reversed.toList(); - if (inpHash.isEmpty) { - throw const DartBitcoinPluginException( - "Input transaction hash not found. Probably malformed raw transaction"); - } - List outputN = - txInputRaw.sublist(cursor + 32, cursor + 36).reversed.toList(); - cursor += 36; - final vi = IntUtils.decodeVarint(txInputRaw.sublist(cursor, cursor + 9)); + static Tuple deserialize( + {required List bytes, int cursor = 0, bool hasSegwit = false}) { + List inpHash = bytes.sublist(cursor, cursor + 32).reversed.toList(); + cursor += 32; + int outputN = IntUtils.fromBytes(bytes.sublist(cursor, cursor + 4), + byteOrder: Endian.little); + cursor += 4; + final vi = IntUtils.decodeVarint(bytes.sublist(cursor)); cursor += vi.item2; - List unlockingScript = txInputRaw.sublist(cursor, cursor + vi.item1); + List unlockingScript = bytes.sublist(cursor, cursor + vi.item1); cursor += vi.item1; - List sequenceNumberData = txInputRaw.sublist(cursor, cursor + 4); + List sequenceNumberData = bytes.sublist(cursor, cursor + 4); cursor += 4; return Tuple( TxInput( txId: BytesUtils.toHexString(inpHash), - txIndex: int.parse(BytesUtils.toHexString(outputN), radix: 16), - scriptSig: Script.fromRaw( - hexData: BytesUtils.toHexString(unlockingScript), - hasSegwit: hasSegwit), + txIndex: outputN, + scriptSig: Script.deserialize( + bytes: unlockingScript, hasSegwit: hasSegwit), sequance: sequenceNumberData), cursor); } + Map toJson() { + return { + "txid": txId, + "txIndex": txIndex, + "scriptSig": scriptSig.script, + "sequance": BytesUtils.toHexString(sequence), + }; + } + @override String toString() { return "TxInput{txId: $txId, txIndex: $txIndex, scriptSig: $scriptSig, sequence: ${BytesUtils.toHexString(sequence)}}"; diff --git a/lib/src/bitcoin/script/op_code/constant.dart b/lib/src/bitcoin/script/op_code/constant.dart index cb6b952..4825e66 100644 --- a/lib/src/bitcoin/script/op_code/constant.dart +++ b/lib/src/bitcoin/script/op_code/constant.dart @@ -2,6 +2,15 @@ /// Constants and identifiers used in the Bitcoin-related code. // ignore_for_file: constant_identifier_names, non_constant_identifier_names, equal_keys_in_map class BitcoinOpCodeConst { + static const int opPushData1 = 0x4c; + static const int opPushData2 = 0x4d; + static const int opPushData4 = 0x4e; + static bool isOpPushData(int byte) { + return byte == BitcoinOpCodeConst.opPushData1 || + byte == BitcoinOpCodeConst.opPushData2 || + byte == BitcoinOpCodeConst.opPushData4; + } + static const Map> OP_CODES = { 'OP_0': [0x00], 'OP_FALSE': [0x00], diff --git a/lib/src/bitcoin/script/op_code/tools.dart b/lib/src/bitcoin/script/op_code/tools.dart index 8d17869..94977e6 100644 --- a/lib/src/bitcoin/script/op_code/tools.dart +++ b/lib/src/bitcoin/script/op_code/tools.dart @@ -1,23 +1,23 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_base/src/bitcoin/script/op_code/constant.dart'; import 'package:bitcoin_base/src/exception/exception.dart'; import 'package:blockchain_utils/utils/utils.dart'; List opPushData(String hexData) { final List dataBytes = BytesUtils.fromHexString(hexData); - if (dataBytes.length < 0x4c) { - return List.from([dataBytes.length]) + dataBytes; + if (dataBytes.length < BitcoinOpCodeConst.opPushData1) { + return [dataBytes.length, ...dataBytes]; } else if (dataBytes.length < mask8) { - return List.from([0x4c]) + - List.from([dataBytes.length]) + - dataBytes; + return [BitcoinOpCodeConst.opPushData1, dataBytes.length, ...dataBytes]; } else if (dataBytes.length < mask16) { - var lengthBytes = List.filled(2, 0); - - writeUint16LE(dataBytes.length, lengthBytes); - return List.from([0x4d, ...lengthBytes, ...dataBytes]); + final lengthBytes = + IntUtils.toBytes(dataBytes.length, length: 2, byteOrder: Endian.little); + return [BitcoinOpCodeConst.opPushData2, ...lengthBytes, ...dataBytes]; } else if (dataBytes.length < mask32) { - var lengthBytes = List.filled(4, 0); - writeUint32LE(lengthBytes.length, lengthBytes); - return List.from([0x4e, ...lengthBytes, ...dataBytes]); + final lengthBytes = + IntUtils.toBytes(dataBytes.length, length: 4, byteOrder: Endian.little); + return [BitcoinOpCodeConst.opPushData4, ...lengthBytes, ...dataBytes]; } else { throw const DartBitcoinPluginException( "Data too large. Cannot push into script"); diff --git a/lib/src/bitcoin/script/output.dart b/lib/src/bitcoin/script/output.dart index 8a25497..8547fd3 100644 --- a/lib/src/bitcoin/script/output.dart +++ b/lib/src/bitcoin/script/output.dart @@ -14,6 +14,14 @@ class TxOutput { final BigInt amount; final Script scriptPubKey; + Map toJson() { + return { + "cashToken": cashToken?.toJson(), + "amount": amount.toString(), + "scriptPubKey": scriptPubKey.script + }; + } + /// creates a copy of the object TxOutput copy() { return TxOutput( @@ -37,27 +45,25 @@ class TxOutput { 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), + static Tuple deserialize( + {required List bytes, required int cursor, bool hasSegwit = false}) { + final value = BigintUtils.fromBytes(bytes.sublist(cursor, cursor + 8), byteOrder: Endian.little) .toSigned(64); cursor += 8; - final vi = IntUtils.decodeVarint(txBytes.sublist(cursor, cursor + 9)); + final vi = IntUtils.decodeVarint(bytes.sublist(cursor)); cursor += vi.item2; - final token = CashToken.fromRaw(txBytes.sublist(cursor)); + final token = CashToken.fromRaw(bytes.sublist(cursor)); List lockScript = - txBytes.sublist(cursor + token.item2, cursor + vi.item1); + bytes.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)), + scriptPubKey: + Script.deserialize(bytes: lockScript, hasSegwit: hasSegwit)), cursor); } diff --git a/lib/src/bitcoin/script/script.dart b/lib/src/bitcoin/script/script.dart index 5228776..8073a9d 100644 --- a/lib/src/bitcoin/script/script.dart +++ b/lib/src/bitcoin/script/script.dart @@ -16,44 +16,54 @@ class Script { script = List.unmodifiable(script); final List script; - static Script fromRaw({required String hexData, bool hasSegwit = false}) { + static Script deserialize( + {required List bytes, bool hasSegwit = false}) { List commands = []; int index = 0; - final scriptBytes = BytesUtils.fromHexString(hexData); - while (index < scriptBytes.length) { - int byte = scriptBytes[index]; + // final scriptBytes = BytesUtils.fromHexString(hexData); + while (index < bytes.length) { + int byte = bytes[index]; if (BitcoinOpCodeConst.CODE_OPS.containsKey(byte)) { - commands.add(BitcoinOpCodeConst.CODE_OPS[byte]!); - index = index + 1; - } else if (!hasSegwit && byte == 0x4c) { - int bytesToRead = scriptBytes[index + 1]; + if (!BitcoinOpCodeConst.isOpPushData(byte)) { + commands.add(BitcoinOpCodeConst.CODE_OPS[byte]!); + } + + /// skip op index = index + 1; - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; - } else if (!hasSegwit && byte == 0x4d) { - int bytesToRead = readUint16LE(scriptBytes, index + 1); + if (byte == BitcoinOpCodeConst.opPushData1) { + // get len + int bytesToRead = bytes[index]; + // skip len + index = index + 1; + commands.add(BytesUtils.toHexString( + bytes.sublist(index, index + bytesToRead))); - index = index + 3; - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; - } else if (!hasSegwit && byte == 0x4e) { - int bytesToRead = readUint32LE(scriptBytes, index + 1); + /// add length + index = index + bytesToRead; + } else if (byte == BitcoinOpCodeConst.opPushData2) { + /// get len + int bytesToRead = readUint16LE(bytes, index); + index = index + 2; + commands.add(BytesUtils.toHexString( + bytes.sublist(index, index + bytesToRead))); + index = index + bytesToRead; + } else if (byte == BitcoinOpCodeConst.opPushData4) { + int bytesToRead = readUint32LE(bytes, index); - index = index + 5; - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index, index + bytesToRead))); - index = index + bytesToRead; + index = index + 4; + commands.add(BytesUtils.toHexString( + bytes.sublist(index, index + bytesToRead))); + index = index + bytesToRead; + } } else { - final viAndSize = IntUtils.decodeVarint(scriptBytes.sublist(index)); + final viAndSize = IntUtils.decodeVarint(bytes.sublist(index)); int dataSize = viAndSize.item1; int size = viAndSize.item2; - final lastIndex = (index + size + dataSize) > scriptBytes.length - ? scriptBytes.length + final lastIndex = (index + size + dataSize) > bytes.length + ? bytes.length : (index + size + dataSize); - commands.add(BytesUtils.toHexString( - scriptBytes.sublist(index + size, lastIndex))); + commands.add( + BytesUtils.toHexString(bytes.sublist(index + size, lastIndex))); index = index + dataSize + size; } } @@ -72,6 +82,9 @@ class Script { if (token is int) { scriptBytes.add(pushInteger(token)); } else { + final pl = opPushData(token); + print("Large ${pl.length}"); + scriptBytes.add(opPushData(token)); } } diff --git a/lib/src/bitcoin/script/transaction.dart b/lib/src/bitcoin/script/transaction.dart index 28e8333..c1b4aa3 100644 --- a/lib/src/bitcoin/script/transaction.dart +++ b/lib/src/bitcoin/script/transaction.dart @@ -49,13 +49,12 @@ class BtcTransaction { List? version, }) { return BtcTransaction( - inputs: inputs ?? this.inputs, - outputs: outputs ?? this.outputs, - witnesses: witnesses ?? this.witnesses, - hasSegwit: hasSegwit ?? this.hasSegwit, - lock: lock ?? List.from(locktime), - version: version ?? List.from(this.version), - ); + inputs: inputs ?? this.inputs, + outputs: outputs ?? this.outputs, + witnesses: witnesses ?? this.witnesses, + hasSegwit: hasSegwit ?? this.hasSegwit, + lock: lock ?? List.from(locktime), + version: version ?? List.from(this.version)); } /// creates a copy of the object (classmethod) @@ -88,19 +87,17 @@ class BtcTransaction { List inputs = []; for (int index = 0; index < vi.item1; index++) { - final inp = - TxInput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); - + final inp = TxInput.deserialize( + bytes: rawtx, hasSegwit: hasSegwit, cursor: cursor); inputs.add(inp.item1); cursor = inp.item2; } - List outputs = []; final viOut = IntUtils.decodeVarint(rawtx.sublist(cursor)); cursor += viOut.item2; for (int index = 0; index < viOut.item1; index++) { - final inp = - TxOutput.fromRaw(raw: raw, hasSegwit: hasSegwit, cursor: cursor); + final inp = TxOutput.deserialize( + bytes: rawtx, hasSegwit: hasSegwit, cursor: cursor); outputs.add(inp.item1); cursor = inp.item2; } @@ -493,6 +490,16 @@ class BtcTransaction { return BytesUtils.toHexString(reversedHash); } + Map toJson() { + return { + "inputs": inputs.map((e) => e.toJson()).toList(), + "outputs": outputs.map((e) => e.toJson()).toList(), + "locktime": BytesUtils.toHexString(locktime), + "version": BytesUtils.toHexString(version), + "witnesses": witnesses.map((e) => e.toJson()).toList() + }; + } + @override String toString() { return "BtcTransaction{inputs: ${inputs.join(", ")}, outputs: ${outputs.join(", ")}, locktime: ${BytesUtils.toHexString(locktime)}}, version: ${BytesUtils.toHexString(version)}, hasSegwit: $hasSegwit, witnesses:${witnesses.join(",")} "; diff --git a/lib/src/bitcoin/script/witness.dart b/lib/src/bitcoin/script/witness.dart index 86b0db5..0a53939 100644 --- a/lib/src/bitcoin/script/witness.dart +++ b/lib/src/bitcoin/script/witness.dart @@ -1,11 +1,11 @@ +import 'package:blockchain_utils/helper/extensions/extensions.dart'; import 'package:blockchain_utils/utils/utils.dart'; /// 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}) : stack = stack.immutable; final List stack; /// creates a copy of the object (classmethod) @@ -26,6 +26,10 @@ class TxWitnessInput { return stackBytes; } + Map toJson() { + return {"stack": stack}; + } + @override String toString() { return "TxWitnessInput{stack: ${stack.join(", ")}}"; diff --git a/lib/src/cash_token/cash_token.dart b/lib/src/cash_token/cash_token.dart index e322257..bcb0fd3 100644 --- a/lib/src/cash_token/cash_token.dart +++ b/lib/src/cash_token/cash_token.dart @@ -261,6 +261,7 @@ class CashToken { /// The commitment contents of the NFT held in this output (0 to 40 bytes). This field is omitted if no NFT is present. final List commitment; final int bitfield; + CashToken.noValidate( {required this.category, required this.amount, @@ -412,6 +413,14 @@ class CashToken { /// Initialized only if the Cash Token has a commitment length. late final String? commitmentInHex = hasCommitment ? BytesUtils.toHexString(commitment) : null; + Map toJson() { + return { + "category": category, + "amount": amount.toString(), + "bitfield": bitfield, + "commitment": BytesUtils.toHexString(commitment), + }; + } @override String toString() { diff --git a/lib/src/crypto/keypair/ec_public.dart b/lib/src/crypto/keypair/ec_public.dart index b33e29f..26eaed4 100644 --- a/lib/src/crypto/keypair/ec_public.dart +++ b/lib/src/crypto/keypair/ec_public.dart @@ -60,8 +60,8 @@ class ECPublic { /// toSegwitAddress 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}) { - final h16 = _toHash160(compressed: compressed); + P2wpkhAddress toSegwitAddress() { + final h16 = _toHash160(); final toHex = BytesUtils.toHexString(h16); return P2wpkhAddress.fromProgram(program: toHex); @@ -124,35 +124,29 @@ class ECPublic { /// toP2wpkhInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2WPKH (Pay-to-Witness-Public-Key-Hash) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2shAddress toP2wpkhInP2sh({bool compressed = true}) { - final addr = toSegwitAddress(compressed: compressed); + P2shAddress toP2wpkhInP2sh() { + final addr = toSegwitAddress(); return P2shAddress.fromScript( 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 toP2wshScript() { + return Script(script: ['OP_1', toHex(), "OP_1", "OP_CHECKMULTISIG"]); } /// toP2wshAddress generates a P2WSH (Pay-to-Witness-Script-Hash) address /// from the ECPublic key. If 'compressed' is true, the key is in compressed format. - P2wshAddress toP2wshAddress({bool compressed = true}) { - return P2wshAddress.fromScript( - script: toP2wshScript(compressed: compressed)); + P2wshAddress toP2wshAddress() { + return P2wshAddress.fromScript(script: toP2wshScript()); } /// toP2wshInP2sh generates a P2SH (Pay-to-Script-Hash) address /// wrapping a P2WSH (Pay-to-Witness-Script-Hash) script derived from the ECPublic key. /// If 'compressed' is true, the key is in compressed format. - P2shAddress toP2wshInP2sh({bool compressed = true}) { - final p2sh = toP2wshAddress(compressed: compressed); + P2shAddress toP2wshInP2sh() { + final p2sh = toP2wshAddress(); return P2shAddress.fromScript( script: p2sh.toScriptPubKey(), type: P2shAddressType.p2wshInP2sh); } diff --git a/lib/src/provider/models/multisig_script.dart b/lib/src/provider/models/multisig_script.dart index 7a8aed6..3ae38fb 100644 --- a/lib/src/provider/models/multisig_script.dart +++ b/lib/src/provider/models/multisig_script.dart @@ -6,7 +6,7 @@ import 'package:blockchain_utils/blockchain_utils.dart'; /// signers in a multi-signature scheme. A multi-signature signer typically includes /// information about their public key and weight within the scheme. class MultiSignatureSigner { - MultiSignatureSigner._(this.publicKey, this.weight); + MultiSignatureSigner._(this.publicKey, this.weight, this.isCompressed); /// PublicKey returns the public key associated with the signer. final String publicKey; @@ -15,12 +15,15 @@ class MultiSignatureSigner { /// The weight is used to determine the number of signatures required for a valid transaction. final int weight; + final bool isCompressed; + /// creates a new instance of a multi-signature signer with the /// specified public key and weight. factory MultiSignatureSigner( {required String publicKey, required int weight}) { ECPublic.fromHex(publicKey); - return MultiSignatureSigner._(publicKey, weight); + return MultiSignatureSigner._( + publicKey, weight, BtcUtils.isCompressedPubKey(publicKey)); } } @@ -34,6 +37,7 @@ class MultiSignatureAddress { P2shAddressType.p2pkhInP2shwt, P2shAddressType.p2pkhInP2sh32wt ]; + final bool canSelectSegwit; /// Signers is a collection of signers participating in the multi-signature scheme. final List signers; @@ -54,6 +58,10 @@ class MultiSignatureAddress { throw DartBitcoinPluginException( "${network.conf.coinName.name} Bitcoin forks that do not support Segwit. use toP2shAddress"); } + if (!canSelectSegwit) { + throw const DartBitcoinPluginException( + "One of the signer's accounts used an uncompressed public key."); + } return P2wshAddress.fromScript(script: multiSigScript); } @@ -79,10 +87,9 @@ class MultiSignatureAddress { return P2shAddress.fromScript(script: multiSigScript, type: addressType); } - BitcoinBaseAddress fromType({ - required BasedUtxoNetwork network, - required BitcoinAddressType addressType, - }) { + BitcoinBaseAddress fromType( + {required BasedUtxoNetwork network, + required BitcoinAddressType addressType}) { switch (addressType) { case SegwitAddresType.p2wsh: return toP2wshAddress(network: network); @@ -102,17 +109,15 @@ class MultiSignatureAddress { MultiSignatureAddress._( {required this.signers, required this.threshold, - // required this.address, - required this.multiSigScript}); + required this.multiSigScript, + required this.canSelectSegwit}); /// CreateMultiSignatureAddress creates a new instance of a MultiSignatureAddress, representing /// a multi-signature Bitcoin address configuration. It allows you to specify the minimum number /// of required signatures (threshold), provide the collection of signers participating in the /// multi-signature scheme, and specify the address type. - factory MultiSignatureAddress({ - required int threshold, - required List signers, - }) { + factory MultiSignatureAddress( + {required int threshold, required List signers}) { final sumWeight = signers.fold(0, (sum, signer) => sum + signer.weight); if (threshold > 16 || threshold < 1) { @@ -136,6 +141,9 @@ class MultiSignatureAddress { multiSigScript.addAll(['OP_$sumWeight', 'OP_CHECKMULTISIG']); final script = Script(script: multiSigScript); return MultiSignatureAddress._( - signers: signers, threshold: threshold, multiSigScript: script); + signers: signers, + threshold: threshold, + multiSigScript: script, + canSelectSegwit: signers.every((e) => e.isCompressed)); } } diff --git a/lib/src/provider/models/utxo_details.dart b/lib/src/provider/models/utxo_details.dart index 4917e98..063da98 100644 --- a/lib/src/provider/models/utxo_details.dart +++ b/lib/src/provider/models/utxo_details.dart @@ -16,11 +16,16 @@ class UtxoAddressDetails { /// associated with the UTXO owner. It may be null if the UTXO owner is not using a multi-signature scheme. final MultiSignatureAddress? _multiSigAddress; - UtxoAddressDetails({ + UtxoAddressDetails._({ required String publicKey, required this.address, }) : _multiSigAddress = null, _publicKey = publicKey; + factory UtxoAddressDetails( + {required String publicKey, required BitcoinBaseAddress address}) { + ECPublic.fromHex(publicKey); + return UtxoAddressDetails._(publicKey: publicKey, address: address); + } UtxoAddressDetails.multiSigAddress( {required MultiSignatureAddress multiSigAddress, required this.address}) @@ -41,10 +46,19 @@ class UtxoWithAddress { /// OwnerDetails is a UtxoAddressDetails instance containing information about the UTXO owner. final UtxoAddressDetails ownerDetails; - UtxoWithAddress({ - required this.utxo, - required this.ownerDetails, - }); + const UtxoWithAddress._( + {required this.utxo, + required this.ownerDetails, + required this.isCompressed}); + factory UtxoWithAddress( + {required BitcoinUtxo utxo, required UtxoAddressDetails ownerDetails}) { + return UtxoWithAddress._( + utxo: utxo, + ownerDetails: ownerDetails, + isCompressed: ownerDetails._publicKey != null && !utxo.isSegwit + ? BtcUtils.isCompressedPubKey(ownerDetails._publicKey!) + : true); + } ECPublic public() { if (isMultiSig()) { @@ -58,6 +72,8 @@ class UtxoWithAddress { return ECPublic.fromHex(ownerDetails._publicKey!); } + final bool isCompressed; + bool isMultiSig() { return ownerDetails._multiSigAddress != null; } @@ -96,10 +112,7 @@ class BitcoinOutput implements BitcoinSpendableBaseOutput { @override final BigInt value; // final CashToken? token; - BitcoinOutput({ - required this.address, - required this.value, - }); + BitcoinOutput({required this.address, required this.value}); @override TxOutput get toOutput => @@ -113,10 +126,7 @@ class BitcoinScriptOutput implements BitcoinBaseOutput { /// The value (amount) of the Bitcoin output. final BigInt value; - const BitcoinScriptOutput({ - required this.script, - required this.value, - }); + const BitcoinScriptOutput({required this.script, required this.value}); /// Convert the custom script output to a standard TxOutput. @override @@ -134,12 +144,11 @@ class BitcoinTokenOutput implements BitcoinSpendableBaseOutput { final BigInt value; final CashToken token; final String? utxoHash; - BitcoinTokenOutput({ - required this.address, - required this.value, - required this.token, - this.utxoHash, - }); + BitcoinTokenOutput( + {required this.address, + required this.value, + required this.token, + this.utxoHash}); /// Convert the custom script output to a standard TxOutput. @override @@ -162,11 +171,7 @@ class BitcoinBurnableOutput extends BitcoinBaseOutput { /// The value (amount) of the burnable output (optional only for token with hasAmount flags). final BigInt? value; - BitcoinBurnableOutput({ - required this.categoryID, - this.utxoHash, - this.value, - }); + BitcoinBurnableOutput({required this.categoryID, this.utxoHash, this.value}); @override TxOutput get toOutput => throw UnimplementedError(); @@ -194,30 +199,45 @@ class BitcoinUtxo { /// BlockHeight represents the block height at which this UTXO was confirmed. final int? blockHeight; - BitcoinUtxo({ - required this.txHash, - required this.value, - required this.vout, - required this.scriptType, - this.blockHeight, - this.token, - }); + BitcoinUtxo._( + {required this.txHash, + required this.value, + required this.vout, + required this.scriptType, + this.blockHeight, + this.token, + required this.isP2tr, + required this.isP2shSegwit, + required this.isSegwit}); + factory BitcoinUtxo( + {required String txHash, + required BigInt value, + required int vout, + required BitcoinAddressType scriptType, + int? blockHeight, + CashToken? token}) { + final bool isP2shSegwit = scriptType == P2shAddressType.p2wpkhInP2sh || + scriptType == P2shAddressType.p2wshInP2sh; + return BitcoinUtxo._( + txHash: txHash, + value: value, + blockHeight: blockHeight, + token: token, + vout: vout, + scriptType: scriptType, + isP2tr: scriptType == SegwitAddresType.p2tr, + isP2shSegwit: isP2shSegwit, + isSegwit: isP2shSegwit || scriptType.isSegwit); + } /// check if utxos is p2tr - bool isP2tr() { - return scriptType == SegwitAddresType.p2tr; - } + final bool isP2tr; /// check if utxos is segwit - bool isSegwit() { - return scriptType.isSegwit || isP2shSegwit(); - } + final bool isSegwit; - /// checl if utxos is p2sh neasted segwit - bool isP2shSegwit() { - return scriptType == P2shAddressType.p2wpkhInP2sh || - scriptType == P2shAddressType.p2wshInP2sh; - } + /// check if utxos is p2sh neasted segwit + final bool isP2shSegwit; /// convert utxos to transaction input with specify sequence like ReplaceByeFee (4Bytes) TxInput toInput([List? sequence]) { diff --git a/lib/src/provider/service/electrum/exception.dart b/lib/src/provider/service/electrum/exception.dart deleted file mode 100644 index 2b1e90e..0000000 --- a/lib/src/provider/service/electrum/exception.dart +++ /dev/null @@ -1,51 +0,0 @@ -// import 'package:blockchain_utils/blockchain_utils.dart'; - -// /// Exception class representing errors encountered during RPC (Remote Procedure Call) requests. -// class ElectrumRPCException implements BlockchainUtilsException { -// /// Constructs an instance of [ElectrumRPCException] with the provided details. -// const ElectrumRPCException( -// {required this.message, -// required this.code, -// required this.data, -// required this.request}); - -// /// The error code associated with the error. -// final int code; - -// /// A human-readable error message describing the issue. -// @override -// final String message; - -// /// Additional data providing more context about the error (nullable). -// final dynamic data; - -// /// The original request that triggered the error. -// final Map request; - -// @override -// String toString() { -// return 'RPC Error: Received code $code with message "$message".'; -// } - -// /// Converts the exception details to a JSON-formatted representation. -// Map toJson() { -// final error = {"message": message, "code": code}; -// if (data != null) { -// error["data"] = data; -// } -// final toJson = { -// "jsonrpc": "2.0", -// "error": error, -// }; -// if (request.isNotEmpty) { -// if (request.containsKey("id")) { -// toJson["id"] = request["id"]; -// } -// if (request.containsKey("params") && request.containsKey("method")) { -// toJson["params"] = request["params"]; -// toJson["method"] = request["method"]; -// } -// } -// return toJson; -// } -// } diff --git a/lib/src/provider/transaction_builder/forked_transaction_builder.dart b/lib/src/provider/transaction_builder/forked_transaction_builder.dart index 542c208..80e1b3c 100644 --- a/lib/src/provider/transaction_builder/forked_transaction_builder.dart +++ b/lib/src/provider/transaction_builder/forked_transaction_builder.dart @@ -49,6 +49,9 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { throw const DartBitcoinPluginException( "invalid network. use ForkedTransactionBuilder for BitcoinCashNetwork and BSVNetwork otherwise use BitcoinTransactionBuilder"); } + + /// validate every address is related to network + /// exception if failed. for (final i in utxosInfo) { i.ownerDetails.address.toAddress(network); } @@ -131,20 +134,21 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { } final senderPub = utxo.public(); + final bool isCompressed = utxo.isCompressed; switch (utxo.utxo.scriptType) { case PubKeyAddressType.p2pk: case P2shAddressType.p2pkInP2sh: case P2shAddressType.p2pkInP2shwt: case P2shAddressType.p2pkInP2sh32: case P2shAddressType.p2pkInP2sh32wt: - return senderPub.toRedeemScript(); + return senderPub.toRedeemScript(compressed: isCompressed); case P2pkhAddressType.p2pkh: case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2pkhInP2sh32: case P2pkhAddressType.p2pkhwt: case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32wt: - return senderPub.toAddress().toScriptPubKey(); + return senderPub.toAddress(compressed: isCompressed).toScriptPubKey(); default: throw DartBitcoinPluginException( "${utxo.utxo.scriptType} does not sudpport on ${network.conf.coinName.name}"); @@ -172,13 +176,14 @@ class ForkedTransactionBuilder implements BasedBitcoinTransacationBuilder { UtxoWithAddress utox, BtcTransaction transaction, ) { + const sighash = + BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED; return transaction.getTransactionSegwitDigit( txInIndex: input, script: scriptPubKeys, amount: utox.utxo.value, token: utox.utxo.token, - sighash: - BitcoinOpCodeConst.SIGHASH_ALL | BitcoinOpCodeConst.SIGHASH_FORKED); + sighash: sighash); } /// buildP2wshOrP2shScriptSig constructs and returns a script signature (represented as a List of strings) @@ -206,23 +211,29 @@ that demonstrate the right to spend the bitcoins associated with the correspondi */ List _buildUnlockingScript(String signedDigest, UtxoWithAddress utx) { final senderPub = utx.public(); + final bool isCompressed = utx.isCompressed; switch (utx.utxo.scriptType) { case PubKeyAddressType.p2pk: return [signedDigest]; case P2pkhAddressType.p2pkh: case P2pkhAddressType.p2pkhwt: - return [signedDigest, senderPub.toHex()]; + return [signedDigest, senderPub.toHex(compressed: isCompressed)]; case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2pkhInP2shwt: case P2shAddressType.p2pkhInP2sh32: case P2shAddressType.p2pkhInP2sh32wt: - final script = senderPub.toAddress().toScriptPubKey(); - return [signedDigest, senderPub.toHex(), script.toHex()]; + final script = + senderPub.toAddress(compressed: isCompressed).toScriptPubKey(); + return [ + signedDigest, + senderPub.toHex(compressed: isCompressed), + script.toHex() + ]; case P2shAddressType.p2pkInP2sh: case P2shAddressType.p2pkInP2shwt: case P2shAddressType.p2pkInP2sh32: case P2shAddressType.p2pkInP2sh32wt: - final script = senderPub.toRedeemScript(); + final script = senderPub.toRedeemScript(compressed: isCompressed); return [signedDigest, script.toHex()]; default: throw DartBitcoinPluginException( @@ -327,7 +338,8 @@ be retrieved by anyone who examines the blockchain's history. required BigInt sumAmountsWithFee, required BigInt sumUtxoAmount, required BigInt sumOutputAmounts}) { - if (!isFakeTransaction && sumAmountsWithFee != sumUtxoAmount) { + if (isFakeTransaction) return; + if (sumAmountsWithFee != sumUtxoAmount) { throw DartBitcoinPluginException('Sum value of utxo not spending', details: { "inputAmount": sumUtxoAmount, @@ -335,56 +347,54 @@ be retrieved by anyone who examines the blockchain's history. "outputAmount": sumOutputAmounts }); } - if (!isFakeTransaction) { - /// sum of token amounts - final sumOfTokenUtxos = utxos.sumOfTokenUtxos(); - - /// sum of token output amounts - final sumTokenOutputAmouts = _sumTokenOutputAmounts(outputs); - for (final i in sumOfTokenUtxos.entries) { - if (sumTokenOutputAmouts[i.key] != i.value) { - BigInt amount = sumTokenOutputAmouts[i.key] ?? BigInt.zero; - amount += outPuts - .whereType() - .where((element) => element.categoryID == i.key) - .fold( - BigInt.zero, - (previousValue, element) => - previousValue + (element.value ?? BigInt.zero)); - - if (amount != i.value) { - throw DartBitcoinPluginException( - 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', - details: { - "token": i.key, - "inputValue": i.value, - "outputValue": amount - }); - } - } - } - for (final i in utxos) { - if (i.utxo.token != null) { - final token = i.utxo.token!; - if (token.hasAmount) continue; - if (!token.hasNFT) continue; - final hasOneoutput = outPuts.whereType().any( - (element) => - element.utxoHash == i.utxo.txHash && - element.token.category == token.category); - if (hasOneoutput) continue; - final hasBurnableOutput = outPuts - .whereType() - .any((element) => - element.utxoHash == i.utxo.txHash && - element.categoryID == token.category); - if (hasBurnableOutput) continue; + + /// sum of token amounts + final sumOfTokenUtxos = utxos.sumOfTokenUtxos(); + + /// sum of token output amounts + final sumTokenOutputAmouts = _sumTokenOutputAmounts(outputs); + for (final i in sumOfTokenUtxos.entries) { + if (sumTokenOutputAmouts[i.key] != i.value) { + BigInt amount = sumTokenOutputAmouts[i.key] ?? BigInt.zero; + amount += outPuts + .whereType() + .where((element) => element.categoryID == i.key) + .fold( + BigInt.zero, + (previousValue, element) => + previousValue + (element.value ?? BigInt.zero)); + + if (amount != i.value) { throw DartBitcoinPluginException( - 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', - details: {"category id": token.category}); + 'Sum token value of UTXOs not spending. use BitcoinBurnableOutput if you want to burn tokens.', + details: { + "token": i.key, + "inputValue": i.value, + "outputValue": amount + }); } } } + for (final i in utxos) { + final token = i.utxo.token; + if (token != null && token.hasNFT) { + if (token.hasAmount) continue; + final hasOneoutput = outPuts.whereType().any( + (element) => + element.utxoHash == i.utxo.txHash && + element.token.category == token.category); + if (hasOneoutput) continue; + final hasBurnableOutput = outPuts + .whereType() + .any((element) => + element.utxoHash == i.utxo.txHash && + element.categoryID == token.category); + if (hasBurnableOutput) continue; + throw DartBitcoinPluginException( + 'Some NFTs in the inputs lack the corresponding spending in the outputs. If you intend to burn tokens, consider utilizing the BitcoinBurnableOutput.', + details: {"category id": token.category}); + } + } } @override @@ -419,7 +429,7 @@ be retrieved by anyone who examines the blockchain's history. break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { + if (sumMultiSigWeight < multiSigAddress.threshold) { throw const DartBitcoinPluginException( "some multisig signature does not exist"); } @@ -502,7 +512,7 @@ be retrieved by anyone who examines the blockchain's history. break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { + if (sumMultiSigWeight < multiSigAddress.threshold) { throw const DartBitcoinPluginException( "some multisig signature does not exist"); } @@ -516,7 +526,8 @@ be retrieved by anyone who examines the blockchain's history. final sig = sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } - + final ea = BtcTransaction.fromRaw(transaction.serialize()); + assert(ea.serialize() == transaction.serialize(), transaction.serialize()); return transaction; } @@ -578,6 +589,7 @@ be retrieved by anyone who examines the blockchain's history. /// now we need sign the transaction digest final sig = await sign(digest, indexUtxo, multiSigAddress.signers[ownerIndex].publicKey, sighash); + if (sig.isEmpty) continue; for (int weight = 0; weight < multiSigAddress.signers[ownerIndex].weight; @@ -587,12 +599,13 @@ be retrieved by anyone who examines the blockchain's history. } mutlsiSigSignatures.add(sig); } + sumMultiSigWeight += multiSigAddress.signers[ownerIndex].weight; if (sumMultiSigWeight >= multiSigAddress.threshold) { break; } } - if (sumMultiSigWeight != multiSigAddress.threshold) { + if (sumMultiSigWeight < multiSigAddress.threshold) { throw const DartBitcoinPluginException( "some multisig signature does not exist"); } @@ -607,20 +620,17 @@ be retrieved by anyone who examines the blockchain's history. await sign(digest, indexUtxo, indexUtxo.public().toHex(), sighash); _addScripts(input: inputs[i], signatures: [sig], utxo: indexUtxo); } - return transaction; } - void _addScripts({ - required UtxoWithAddress utxo, - required TxInput input, - required List signatures, - }) { + void _addScripts( + {required UtxoWithAddress utxo, + required TxInput input, + required List signatures}) { /// ok we signed, now we need unlocking script for this input final scriptSig = utxo.isMultiSig() ? _buildMiltisigUnlockingScript(signatures, utxo) : _buildUnlockingScript(signatures.first, utxo); - input.scriptSig = Script(script: scriptSig); } } diff --git a/lib/src/provider/transaction_builder/transaction_builder.dart b/lib/src/provider/transaction_builder/transaction_builder.dart index 07587c6..8003fd2 100644 --- a/lib/src/provider/transaction_builder/transaction_builder.dart +++ b/lib/src/provider/transaction_builder/transaction_builder.dart @@ -122,7 +122,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final transaction = transactionBuilder .buildTransaction((trDigest, utxo, multiSigPublicKey, int sighash) { - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return fakeSchnorSignaturBytes; } else { return fakeECDSASignatureBytes; @@ -145,7 +145,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - bool: True if at least one UTXO in the list is a SegWit UTXO, false otherwise. bool _hasSegwit() { for (final element in utxosInfo) { - if (element.utxo.isSegwit()) { + if (element.utxo.isSegwit) { return true; } } @@ -160,7 +160,7 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { /// - bool: True if at least one UTXO in the list is a P2TR UTXO, false otherwise. bool _hasTaproot() { for (final element in utxosInfo) { - if (element.utxo.isP2tr()) { + if (element.utxo.isP2tr) { return true; } } @@ -201,14 +201,16 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { final senderPub = utxo.public(); switch (utxo.utxo.scriptType) { case PubKeyAddressType.p2pk: - return senderPub.toRedeemScript(); + return senderPub.toRedeemScript(compressed: utxo.isCompressed); case SegwitAddresType.p2wsh: if (isTaproot) { return senderPub.toP2wshAddress().toScriptPubKey(); } return senderPub.toP2wshScript(); case P2pkhAddressType.p2pkh: - return senderPub.toAddress().toScriptPubKey(); + return senderPub + .toAddress(compressed: utxo.isCompressed) + .toScriptPubKey(); case SegwitAddresType.p2wpkh: if (isTaproot) { return senderPub.toSegwitAddress().toScriptPubKey(); @@ -218,9 +220,13 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return senderPub.toTaprootAddress().toScriptPubKey(); case P2shAddressType.p2pkhInP2sh: if (isTaproot) { - return senderPub.toP2pkhInP2sh().toScriptPubKey(); + return senderPub + .toP2pkhInP2sh(compressed: utxo.isCompressed) + .toScriptPubKey(); } - return senderPub.toAddress().toScriptPubKey(); + return senderPub + .toAddress(compressed: utxo.isCompressed) + .toScriptPubKey(); case P2shAddressType.p2wpkhInP2sh: if (isTaproot) { return senderPub.toP2wpkhInP2sh().toScriptPubKey(); @@ -233,9 +239,11 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { return senderPub.toP2wshScript(); case P2shAddressType.p2pkInP2sh: if (isTaproot) { - return senderPub.toP2pkInP2sh().toScriptPubKey(); + return senderPub + .toP2pkInP2sh(compressed: utxo.isCompressed) + .toScriptPubKey(); } - return senderPub.toRedeemScript(); + return senderPub.toRedeemScript(compressed: utxo.isCompressed); } throw const DartBitcoinPluginException("invalid bitcoin address type"); } @@ -262,8 +270,8 @@ class BitcoinTransactionBuilder implements BasedBitcoinTransacationBuilder { BtcTransaction transaction, List taprootAmounts, List