diff --git a/.gitignore b/.gitignore index 9d7edcf..dc1e7b5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ .vscode/ # Flutter/Dart/Pub related -**/doc/api/ .dart_tool/ .flutter-plugins .packages diff --git a/README.md b/README.md index 9a77bcd..8e4e4bb 100644 --- a/README.md +++ b/README.md @@ -6,30 +6,32 @@ Works with mainnet and testnet. ## Getting Started ### 1) Depend on it -After you download the repository, add a local dependency into the pubspec.yaml of your testing or development projet: ``` dependencies: - bitbox_plugin: - path: / + bitbox: + git: + url: https://github.com/RomitRadical/bitbox-flutter + ref: master ``` - + ### 2) Import it ``` -// There's a good chance your own project will use similar names as some of the -// classes in this library. A simple way to create some order is to import the +// There's a good chance your own project will use similar names as some of the +// classes in this library. A simple way to create some order is to import the // library with Bitbox prefix: -import 'package:bitbox/bitbox.dart' as Bitbox; +import 'package:bitbox/bitbox.dart' as bitbox; ``` ### 2) Use it + ``` // set this to true to use testnet final testnet = true; -// After running the code for the first time, depositing an amount to the address -// displayed in the console, and waiting for confirmation, paste the generated +// After running the code for the first time, depositing an amount to the address +// displayed in the console, and waiting for confirmation, paste the generated // mnemonic here, so the code continues below with address withdrawal String mnemonic = ""; @@ -86,7 +88,7 @@ if (addressDetails["balance"] > 0) { // placeholder for total input balance int totalBalance = 0; - // iterate through the list of address utxos and use them as inputs for the + // iterate through the list of address utxos and use them as inputs for the // withdrawal transaction utxos.forEach((Bitbox.Utxo utxo) { // add the utxo as an input for the transaction @@ -118,7 +120,7 @@ if (addressDetails["balance"] > 0) { // sign all inputs signatures.forEach((signature) { - builder.sign(signature["vin"], signature["key_pair"], + builder.sign(signature["vin"], signature["key_pair"], signature["original_amount"]); }); @@ -140,8 +142,9 @@ For further documentation, refer to apidoc of this repository ## Testing -There are some unit tests in test/bitbox_test.dart. They use data generated from the original [Bitbox for JS](https://developer.bitcoin.com/bitbox/) and compare them with the output of this library. +There are some unit tests in test/bitbox_test.dart. They use data generated from the original [Bitbox for JS](https://developer.bitcoin.com/bitbox/) and compare them with the output of this library. The following is tested for both testnet and mainnet: + - Generating the master node from mnemonic and comparing both its XPub and XPriv - Generating an account node and comparing XPub and XPriv - Generating 10 test childs and comparing their private keys and addresses @@ -156,12 +159,11 @@ To run the test: 1. Copy create_test_data.js to a separate directory and download the original Bitbox JS into the directory 2. Generate the testing data by runing create_test_data.js with your local nodeJS engine 3. Update bitbox_test.dart with the path to the generated test_data.json file -4. Run bitbox_test.dart -Optionally between step 1) and 2), send some balance to either testnet or mainnet addresses (or both), wait for confirmations and run create_test_data.js again to update the data and generate testing transactions - +4. Run bitbox_test.dart + Optionally between step 1) and 2), send some balance to either testnet or mainnet addresses (or both), wait for confirmations and run create_test_data.js again to update the data and generate testing transactions ## Acknowledgments -This is a port of the original JS-based Bitbox library by Gabriel Cordana and Bitcoin.com, so first of all huge thanks to Gabriel and the whole Bitcoin.com team for doing so much for the BCH ecosystem. +This is a port of the original JS-based Bitbox library by Gabriel Cardona and Bitcoin.com, so first of all huge thanks to Gabriel and the whole Bitcoin.com team for doing so much for the BCH ecosystem. Also I either re-used a lot of code originally wrote for Bitcoin or called some libraries (bip39 and bip32) by [anicdh](https://github.com/anicdh), so Thanks big time to him. Without that it would take me many more weeks! diff --git a/create_test_data.js b/create_test_data.js index dbac9a1..4f9e3d9 100644 --- a/create_test_data.js +++ b/create_test_data.js @@ -27,7 +27,7 @@ function createTestData(mnemonic, testnet) { "master_xpriv": bitbox.HDNode.toXPriv(masterNode), "master_xpub": bitbox.HDNode.toXPub(masterNode), "account_xpriv": bitbox.HDNode.toXPriv(accountNode), - "account_xpub": bitbox.HDNode.toXPub(accountNode), + "account_xpub": bitbox.HDNode.tdoXPub(accountNode), "child_nodes" : [] }; diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..757d3a3 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,114 @@ +import 'package:bitbox/bitbox.dart' as Bitbox; + +void main() async { + // set this to false to use mainnet + final testnet = false; + + // After running the code for the first time, depositing an amount to the address displayed in the console, + // and waiting for confirmation, paste the generated mnemonic here, + // so the code continues below with address withdrawal + String mnemonic = + "leaf tackle snap liar core motion material live camp quote mercy void"; + + if (mnemonic == "") { + // generate 12-word (128bit) mnemonic + mnemonic = Bitbox.Mnemonic.generate(); + + print(mnemonic); + } + + // generate a seed from mnemonic + final seed = Bitbox.Mnemonic.toSeed(mnemonic); + + // create an instance of Bitbox.HDNode for mainnet + final masterNode = Bitbox.HDNode.fromSeed(seed, testnet); + + // This format is compatible with Bitcoin.com wallet. + // Other wallets use Change to m/44'/145'/0'/0 + final accountDerivationPath = "m/44'/0'/0'/0"; + + // create an account node using the provided derivation path + final accountNode = masterNode.derivePath(accountDerivationPath); + + // get account's extended private key + final accountXPriv = accountNode.toXPriv(); + + // create a Bitbox.HDNode instance of the first child in this account + final childNode = accountNode.derive(0); + + // get an address of the child + final address = childNode.toCashAddress(); + + // if you are using testnet, set the appropriate rest api url before making + // any API calls (like getting address or transaction details or broadcasting a transaction + if (testnet) { + Bitbox.Bitbox.setRestUrl(restUrl: Bitbox.Bitbox.trestUrl); + } + + // get address details + final addressDetails = await Bitbox.Address.details(address); + + print(addressDetails); + + // If there is a confirmed balance, attempt to withdraw it to the address defined below + if (addressDetails["balance"] > 0) { + final builder = Bitbox.Bitbox.transactionBuilder(testnet: testnet); + + // retrieve address' utxos from the rest api + final utxos = await Bitbox.Address.utxo(address) as List; + + // placeholder for input signatures + final signatures = []; + + // placeholder for total input balance + int totalBalance = 0; + + // iterate through the list of address utxos and use them as inputs for the withdrawal transaction + utxos.forEach((Bitbox.Utxo utxo) { + // add the utxo as an input for the transaction + builder.addInput(utxo.txid, utxo.vout); + + // add a signature to the list to be used later + signatures.add({ + "vin": signatures.length, + "key_pair": childNode.keyPair, + "original_amount": utxo.satoshis + }); + + totalBalance += utxo.satoshis!; + }); + + // set an address to send the remaining balance to + final outputAddress = + "bitcoincash:qq4vzza5uhgr42ntkl28x67qzda4af5hpgap6z0ntx"; + + // if there is an unspent balance, create a spending transaction + if (totalBalance > 0 && outputAddress != "") { + // calculate the fee based on number of inputs and one expected output + final fee = Bitbox.BitcoinCash.getByteCount(signatures.length, 1); + + // calculate how much balance will be left over to spend after the fee + final sendAmount = totalBalance - fee; + + // add the output based on the address provided in the testing data + builder.addOutput(outputAddress, sendAmount); + + // sign all inputs + signatures.forEach((signature) { + builder.sign(signature["vin"], signature["key_pair"], + signature["original_amount"]); + }); + + // build the transaction + final tx = builder.build(); + + // broadcast the transaction + final txid = await Bitbox.RawTransactions.sendRawTransaction(tx.toHex()); + + // Yatta! + print("Transaction broadcasted: $txid"); + } else if (totalBalance > 0) { + print("Enter an output address to test withdrawal transaction"); + } + } +} diff --git a/lib/bitbox.dart b/lib/bitbox.dart index b8704b1..034cbff 100644 --- a/lib/bitbox.dart +++ b/lib/bitbox.dart @@ -4,10 +4,19 @@ export 'src/account.dart'; export 'src/address.dart'; export 'src/bitbox.dart'; export 'src/bitcoincash.dart'; +export 'src/block.dart'; +export 'src/blockchain.dart'; +export 'src/cashaccounts.dart'; +export 'src/crypto/crypto.dart'; +export 'src/crypto/ecies.dart'; export 'src/ecpair.dart'; export 'src/hdnode.dart'; export 'src/mnemonic.dart'; +export 'src/networks.dart'; +export 'src/slp.dart'; export 'src/rawtransactions.dart'; export 'src/transaction.dart'; export 'src/transactionbuilder.dart'; -export 'src/varuint.dart'; \ No newline at end of file +export 'src/varuint.dart'; +export 'src/utils/opcodes.dart'; +export 'src/utils/script.dart'; diff --git a/lib/src/account.dart b/lib/src/account.dart index ed20836..c8d77c4 100644 --- a/lib/src/account.dart +++ b/lib/src/account.dart @@ -4,25 +4,31 @@ import 'hdnode.dart'; class Account { final HDNode accountNode; - int currentChild = 0; + int? currentChild = 0; Account(this.accountNode, [this.currentChild]); /// Returns address at the current position - String getCurrentAddress([legacyFormat = true]) { + String? getCurrentAddress([legacyFormat = true]) { if (legacyFormat) { - return accountNode.derive(currentChild).toLegacyAddress(); + return accountNode.derive(currentChild!).toLegacyAddress(); } else { - return accountNode.derive(currentChild).toCashAddress(); + return accountNode.derive(currentChild!).toCashAddress(); } } /// moves the position forward and returns an address from the new position - String getNextAddress([legacyFormat = true]) { + String? getNextAddress([legacyFormat = true]) { if (legacyFormat) { - return accountNode.derive(++currentChild).toLegacyAddress(); + if (currentChild != null) { + currentChild = currentChild! + 1; + return accountNode.derive(currentChild!).toLegacyAddress(); + } } else { - return accountNode.derive(++currentChild).toCashAddress(); + if (currentChild != null) { + currentChild = currentChild! + 1; + return accountNode.derive(currentChild!).toCashAddress(); + } } } -} \ No newline at end of file +} diff --git a/lib/src/address.dart b/lib/src/address.dart index 7c4d162..e019a75 100644 --- a/lib/src/address.dart +++ b/lib/src/address.dart @@ -1,36 +1,63 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'utils/rest_api.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'utils/network.dart'; -import 'utils/opcodes.dart'; -import 'utils/script.dart' as bscript; import 'package:fixnum/fixnum.dart'; -import 'hdnode.dart'; - /// Works with both legacy and cashAddr formats of the address /// /// There is no reason to instanciate this class. All constants, functions, and methods are static. /// It is assumed that all necessary data to work with addresses are kept in the instance of [ECPair] or [Transaction] class Address { - static const formatCashAddr = 0; - static const formatLegacy = 1; + static const formatCashAddr = 'cashaddr'; + static const formatLegacy = 'legacy'; + static const formatSlp = 'slpaddr'; static const _CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; static const _CHARSET_INVERSE_INDEX = { - 'q': 0, 'p': 1, 'z': 2, 'r': 3, 'y': 4, '9': 5, 'x': 6, '8': 7, - 'g': 8, 'f': 9, '2': 10, 't': 11, 'v': 12, 'd': 13, 'w': 14, '0': 15, - 's': 16, '3': 17, 'j': 18, 'n': 19, '5': 20, '4': 21, 'k': 22, 'h': 23, - 'c': 24, 'e': 25, '6': 26, 'm': 27, 'u': 28, 'a': 29, '7': 30, 'l': 31, + 'q': 0, + 'p': 1, + 'z': 2, + 'r': 3, + 'y': 4, + '9': 5, + 'x': 6, + '8': 7, + 'g': 8, + 'f': 9, + '2': 10, + 't': 11, + 'v': 12, + 'd': 13, + 'w': 14, + '0': 15, + 's': 16, + '3': 17, + 'j': 18, + 'n': 19, + '5': 20, + '4': 21, + 'k': 22, + 'h': 23, + 'c': 24, + 'e': 25, + '6': 26, + 'm': 27, + 'u': 28, + 'a': 29, + '7': 30, + 'l': 31, }; /// Returns information about the given Bitcoin Cash address. /// /// See https://developer.bitcoin.com/bitbox/docs/util for details about returned format - static Future> validateAddress(String address) async => - await RestApi.sendGetRequest("util/validateAddress", address); + static Future?> validateAddress(String address) async => + await (RestApi.sendGetRequest("util/validateAddress", address) + as FutureOr?>); /// Returns details of the provided address or addresses /// @@ -44,7 +71,7 @@ class Address { /// See https://developer.bitcoin.com/bitbox/docs/address#details for details about returned format. However /// note, that processing from array to map is done on the library side static Future details(addresses, [returnAsMap = false]) async => - await _sendRequest("details", addresses, returnAsMap); + await _sendRequest("details", addresses, returnAsMap); /// Returns list of unconfirmed transactions /// @@ -57,20 +84,23 @@ class Address { /// /// See https://developer.bitcoin.com/bitbox/docs/address#unconfirmed for details about the returned format. However /// note, that processing from array to map is done on the library side - static Future getUnconfirmed(addresses, [returnAsMap = false]) async { + static Future getUnconfirmed(addresses, + [returnAsMap = false]) async { final result = await _sendRequest("unconfirmed", addresses); if (result is Map) { return Utxo.convertMapListToUtxos(result["utxos"]); } else if (result is List) { final returnList = []; - final returnMap = {}; + final returnMap = {}; result.forEach((addressUtxoMap) { if (returnAsMap) { - returnMap[addressUtxoMap["cashAddr"]] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); + returnMap[addressUtxoMap["cashAddr"]] = + Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); } else { - addressUtxoMap["utxos"] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); + addressUtxoMap["utxos"] = + Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); returnList.add(addressUtxoMap); } }); @@ -95,13 +125,15 @@ class Address { return Utxo.convertMapListToUtxos(result["utxos"]); } else if (result is List) { final returnList = []; - final returnMap = {}; + final returnMap = {}; result.forEach((addressUtxoMap) { if (returnAsMap) { - returnMap[addressUtxoMap["cashAddress"]] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); + returnMap[addressUtxoMap["cashAddress"]] = + Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); } else { - addressUtxoMap["utxos"] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); + addressUtxoMap["utxos"] = + Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]); returnList.add(addressUtxoMap); } }); @@ -112,38 +144,122 @@ class Address { } } - /// Converts legacy address to cash address - static String toCashAddress(String legacyAddress, [bool includePrefix = true]) { - final decoded = Address._decodeLegacyAddress(legacyAddress); - String prefix = ""; - if (includePrefix) { - switch (decoded["version"]) { - case Network.bchPublic : - prefix = "bitcoincash"; + /// Converts cashAddr format to legacy address + static String toLegacyAddress(String address) { + final decoded = _decode(address); + final testnet = + decoded['prefix'] == "bchtest" || decoded['prefix'] == "slptest"; + late var version; + if (testnet) { + switch (decoded['type']) { + case "P2PKH": + version = Network.bchTestnetPublic; + break; + case "P2SH": + version = Network.bchTestnetscriptHash; + break; + } + } else { + switch (decoded['type']) { + case "P2PKH": + version = Network.bchPublic; break; - case Network.bchTestnetPublic : - prefix = "bchtest"; + case "P2SH": + version = Network.bchPublicscriptHash; break; - default: - throw FormatException("Unsupported address format: $legacyAddress"); } } + return toBase58Check(decoded["hash"], version); + } - final cashAddress = Address._encode(prefix, "P2PKH", decoded["hash"]); - return cashAddress; + /// Converts legacy address to cash address + static String? toCashAddress(String address, [bool includePrefix = true]) { + final decoded = _decode(address); + switch (decoded["prefix"]) { + case 'bitcoincash': + case 'simpleledger': + decoded['prefix'] = "bitcoincash"; + break; + case 'bchtest': + case 'slptest': + decoded['prefix'] = "bchtest"; + break; + default: + throw FormatException("Unsupported address format: $address"); + } + final cashAddress = + _encode(decoded['prefix'], decoded['type'], decoded["hash"]); + if (!includePrefix) { + return cashAddress.split(":")[1]; + } else { + return cashAddress; + } } - /// Converts cashAddr format to legacy address - static String toLegacyAddress(String cashAddress) { - final decoded = _decodeCashAddress(cashAddress); - final testnet = decoded['prefix'] == "bchtest"; + /// Converts legacy address to cash address + static String? toTokenAddress(String address, [bool includePrefix = true]) { + final decoded = _decode(address); + switch (decoded["prefix"]) { + case 'bitcoincash': + case 'simpleledger': + decoded['prefix'] = "bitcoincash"; + break; + case 'bchtest': + case 'slptest': + decoded['prefix'] = "bchtest"; + break; + default: + throw FormatException("Unsupported address format: $address"); + } - final version = !testnet ? Network.bchPublic : Network.bchTestnetPublic; - return toBase58Check(decoded["hash"], version); + final tokenAddress = + _encode(decoded['prefix'], "P2PKHWITHTOKENS", decoded["hash"]); + + if (!includePrefix) { + return tokenAddress.split(":")[1]; + } else { + return tokenAddress; + } + } + + /// Converts legacy or cash address to SLP address + static String? toSLPAddress(String address, [bool includePrefix = true]) { + final decoded = Address._decode(address); + switch (decoded["prefix"]) { + case 'bitcoincash': + case 'simpleledger': + decoded['prefix'] = "simpleledger"; + break; + case 'bchtest': + case 'slptest': + decoded['prefix'] = "slptest"; + break; + default: + throw FormatException("Unsupported address format: $address"); + } + final slpAddress = + Address._encode(decoded['prefix'], decoded['type'], decoded["hash"]); + if (!includePrefix) { + return slpAddress.split(":")[1]; + } else { + return slpAddress; + } + } + + static bool isLegacyAddress(String address) { + return detectAddressFormat(address) == formatLegacy; + } + + static bool isCashAddress(String address) { + return detectAddressFormat(address) == formatCashAddr; } - /// Detects type of the address and returns [formatCashAddr] or [formatLegacy] - static int detectFormat(String address) { + static bool isSlpAddress(String address) { + return detectAddressFormat(address) == formatSlp; + } + + /// Detects type of the address and returns [legacy], [cashaddr] or [slpaddr] + static String? detectAddressFormat(String address) { // decode the address to determine the format final decoded = _decode(address); // return the format @@ -158,6 +274,7 @@ class Address { return bs58check.encode(payload); } + /* static Uint8List _toOutputScript(address, network) { return bscript.compile([ Opcodes.OP_DUP, @@ -166,16 +283,17 @@ class Address { Opcodes.OP_EQUALVERIFY, Opcodes.OP_CHECKSIG ]); - } + }*/ /// Encodes a hash from a given type into a Bitcoin Cash address with the given prefix. /// [prefix] - Network prefix. E.g.: 'bitcoincash'. - /// [type] is currently unused - the library works only with _P2PKH_ + /// [type] Type of address to generate. Either 'P2PKH' or 'P2SH'. /// [hash] is the address hash, which can be decode either using [_decodeCashAddress()] or [_decodeLegacyAddress()] - static _encode(String prefix, String type, Uint8List hash) { + static _encode(String prefix, String? type, Uint8List hash) { final prefixData = _prefixToUint5List(prefix) + Uint8List(1); - final versionByte = _getHashSizeBits(hash); - final payloadData = _convertBits(Uint8List.fromList([versionByte] + hash), 8, 5); + final versionByte = _getTypeBits(type) + _getHashSizeBits(hash); + final payloadData = + _convertBits(Uint8List.fromList([versionByte] + hash), 8, 5); final checksumData = prefixData + payloadData + Uint8List(8); final payload = payloadData + _checksumToUint5Array(_polymod(checksumData)); return "$prefix:" + _base32Encode(payload); @@ -183,14 +301,16 @@ class Address { /// Helper method for sending generic requests to Bitbox API. Accepts [String] or [List] of Strings and optionally /// converts the List returned by Bitbox into [Map], which uses cashAddress as a key - static Future _sendRequest(String path, dynamic addresses, [bool returnAsMap = false]) async { + static Future _sendRequest(String path, dynamic addresses, + [bool returnAsMap = false]) async { assert(addresses is String || addresses is List); if (addresses is String) { - return await RestApi.sendGetRequest("address/$path", addresses) as Map; + return await RestApi.sendGetRequest("address/$path", addresses) as Map?; } else if (addresses is List) { - return await RestApi.sendPostRequest("address/$path", "addresses", addresses, - returnKey: returnAsMap ? "cashAddress" : null); + return await RestApi.sendPostRequest( + "address/$path", "addresses", addresses, + returnKey: returnAsMap ? "cashAddress" : null); } else { throw TypeError(); } @@ -249,11 +369,41 @@ class Address { case 7: return 512; } + + return -1; + } + + static String _getType(versionByte) { + switch (versionByte & 120) { + case 0: + return 'P2PKH'; + case 8: + return 'P2SH'; + case 16: + return 'P2PKHWITHTOKENS'; + default: + throw FormatException( + 'Invalid address type in version byte: ' + versionByte + '.'); + } + } + + static int _getTypeBits(type) { + switch (type) { + case 'P2PKH': + return 0; + case 'P2SH': + return 8; + case 'P2PKHWITHTOKENS': + return 16; + default: + throw new FormatException('Invalid type: ' + type + '.'); + } } /// Decodes the given address into: - /// * (for cashAddr): constituting prefix (e.g. _bitcoincash_) /// * (for legacy): version + /// * (for cashAddr): constituting prefix (e.g. _bitcoincash_) + ///* (for slpAddr): constituting prefix (e.g. _simpleledger_) /// * hash /// * format static Map _decode(String address) { @@ -265,6 +415,10 @@ class Address { return _decodeCashAddress(address); } catch (e) {} + try { + return _decodeSlpAddress(address); + } catch (e) {} + throw FormatException("Invalid address format : $address"); } @@ -272,11 +426,35 @@ class Address { static Map _decodeLegacyAddress(String address) { Uint8List buffer = bs58check.decode(address); - return { - "version": buffer.first, - "hash": buffer.sublist(1), - "format" : formatLegacy, + var decoded = { + 'prefix': "", + 'type': "", + 'hash': buffer.sublist(1), + 'format': formatLegacy }; + + switch (buffer.first) { + case Network.bchPublic: + decoded['prefix'] = "bitcoincash"; + decoded['type'] = "P2PKH"; + break; + + case Network.bchPublicscriptHash: + decoded['prefix'] = "bitcoincash"; + decoded['type'] = "P2SH"; + break; + + case Network.bchTestnetPublic: + decoded['prefix'] = "bchtest"; + decoded['type'] = "P2PKH"; + break; + + case Network.bchTestnetscriptHash: + decoded['prefix'] = "bchtest"; + decoded['type'] = "P2SH"; + break; + } + return decoded; } /// Decodes the given address into its constituting prefix, type and hash @@ -307,7 +485,7 @@ class Address { throw FormatException("Invalid Address Format: $address"); } - String exception; + late String exception; // try to decode the address with either one or all three possible prefixes for (int i = 0; i < prefixes.length; i++) { final payload = _base32Decode(address); @@ -317,7 +495,9 @@ class Address { continue; } - final payloadData = _fromUint5Array(payload.sublist(0, payload.length - 8)); + final payloadData = + _fromUint5Array(payload.sublist(0, payload.length - 8)); + var versionByte = payloadData[0]; final hash = payloadData.sublist(1); if (_getHashSize(payloadData[0]) != hash.length * 8) { @@ -325,12 +505,78 @@ class Address { continue; } + var type = _getType(versionByte); + + // If the loop got all the way here, it means validations went through and the address was decoded. + // Return the decoded data + return { + "prefix": prefixes[i], + "type": type, + "hash": hash, + "format": formatCashAddr + }; + } + + // if the loop went through all possible formats and didn't return data from the function, it means there were + // validation issues. Throw a format exception + throw FormatException(exception); + } + + static Map _decodeSlpAddress(String address) { + if (!_hasSingleCase(address)) { + throw FormatException("Address has both lower and upper case: $address"); + } + + // split the address with : separator to find out it if contains prefix + final pieces = address.toLowerCase().split(":"); + + // placeholder for different prefixes to be tested later + List prefixes; + + // check if the address contained : separator by looking at number of splitted pieces + if (pieces.length == 2) { + // if it contained the separator, use the first piece as a single prefix + prefixes = [pieces.first]; + address = pieces.last; + } else if (pieces.length == 1) { + // if it came without separator, try all three possible formats + prefixes = ["simpleledger", "slptest", "slpreg"]; + } else { + // if it came with more than one separator, throw a format exception + throw FormatException("Invalid Address Format: $address"); + } + + late String exception; + + // try to decode the address with either one or all three possible prefixes + for (int i = 0; i < prefixes.length; i++) { + final payload = _base32Decode(address); + + if (!_validChecksum(prefixes[i], payload)) { + exception = "Invalid checksum: $address"; + continue; + } + + final payloadData = + _fromUint5Array(payload.sublist(0, payload.length - 8)); + + var versionByte = payloadData[0]; + final hash = payloadData.sublist(1); + + if (_getHashSize(payloadData[0]) != hash.length * 8) { + exception = "Invalid hash size: $address"; + continue; + } + + var type = _getType(versionByte); + // If the loop got all the way here, it means validations went through and the address was decoded. // Return the decoded data return { - "prefix" : prefixes[i], - "hash" : hash, - "format" : formatCashAddr + "prefix": prefixes[i], + "type": type, + "hash": hash, + "format": formatSlp }; } @@ -358,8 +604,11 @@ class Address { /// Converts a list of integers made up of 'from' bits into an array of integers made up of 'to' bits. /// The output array is zero-padded if necessary, unless strict mode is true. - static Uint8List _convertBits(List data, int from, int to, [bool strictMode = false]) { - final length = strictMode ? (data.length * from / to).floor() : (data.length * from / to).ceil(); + static Uint8List _convertBits(List data, int from, int to, + [bool strictMode = false]) { + final length = strictMode + ? (data.length * from / to).floor() + : (data.length * from / to).ceil(); int mask = (1 << to) - 1; var result = Uint8List(length); int index = 0; @@ -383,7 +632,8 @@ class Address { } } else { if (bits < from && ((accumulator << (to - bits)) & mask).toInt() != 0) { - throw FormatException("Input cannot be converted to $to bits without padding, but strict mode was used."); + throw FormatException( + "Input cannot be converted to $to bits without padding, but strict mode was used."); } } return result; @@ -392,13 +642,20 @@ class Address { /// Computes a checksum from the given input data as specified for the CashAddr format: // https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md. static int _polymod(List data) { - const GENERATOR = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470]; + const GENERATOR = [ + 0x98f2bc8e61, + 0x79b76d99e2, + 0xf33e5fb3c4, + 0xae2eabe2a8, + 0x1e4f43e470 + ]; int checksum = 1; for (int i = 0; i < data.length; ++i) { final value = data[i]; final topBits = checksum >> 35; + checksum = ((checksum & 0x07ffffffff) << 5) ^ value; for (int j = 0; j < GENERATOR.length; ++j) { @@ -415,8 +672,9 @@ class Address { final data = Uint8List(string.length); for (int i = 0; i < string.length; i++) { final value = string[i]; - if (!_CHARSET_INVERSE_INDEX.containsKey(value)) throw FormatException("Invalid character '$value'"); - data[i] = _CHARSET_INVERSE_INDEX[string[i]]; + if (!_CHARSET_INVERSE_INDEX.containsKey(value)) + throw FormatException("Invalid character '$value'"); + data[i] = _CHARSET_INVERSE_INDEX[string[i]]!; } return data; @@ -446,23 +704,24 @@ class Address { /// Container for to make it easier to work with Utxos class Utxo { - final String txid; - final int vout; - final double amount; - final int satoshis; - final int height; - final int confirmations; + final String? txid; + final int? vout; + final double? amount; + final int? satoshis; + final int? height; + final int? confirmations; - Utxo(this.txid, this.vout, this.amount, this.satoshis, this.height, this.confirmations); + Utxo(this.txid, this.vout, this.amount, this.satoshis, this.height, + this.confirmations); /// Create [Utxo] instance from utxo [Map] - Utxo.fromMap(Map utxoMap) : - this.txid = utxoMap['txid'], - this.vout = utxoMap['vout'], - this.amount = utxoMap['amount'], - this.satoshis = utxoMap['satoshis'], - this.height = utxoMap.containsKey('height') ? utxoMap['height'] : null, - this.confirmations = utxoMap['confirmations']; + Utxo.fromMap(Map utxoMap) + : this.txid = utxoMap['txid'], + this.vout = utxoMap['vout'], + this.amount = utxoMap['amount'], + this.satoshis = utxoMap['satoshis'], + this.height = utxoMap.containsKey('height') ? utxoMap['height'] : null, + this.confirmations = utxoMap['confirmations']; /// Converts List of utxo maps into a list of [Utxo] objects static List convertMapListToUtxos(List utxoMapList) { @@ -476,11 +735,11 @@ class Utxo { @override String toString() => jsonEncode({ - "txid": txid, - "vout": vout, - "amount": amount, - "satoshis": satoshis, - "height": height, - "confirmations" : confirmations - }); -} \ No newline at end of file + "txid": txid, + "vout": vout, + "amount": amount, + "satoshis": satoshis, + "height": height, + "confirmations": confirmations + }); +} diff --git a/lib/src/bitbox.dart b/lib/src/bitbox.dart index 61d185a..f7caf62 100644 --- a/lib/src/bitbox.dart +++ b/lib/src/bitbox.dart @@ -16,6 +16,6 @@ class Bitbox { /// /// It is possible to call [TransactionBuilder] directly and pass [Network] parameter, this just makes it easier static TransactionBuilder transactionBuilder({testnet: false}) => - TransactionBuilder(network: testnet ? Network.bitcoinCashTest() : Network.bitcoinCash()); + TransactionBuilder( + network: testnet ? Network.bitcoinCashTest() : Network.bitcoinCash()); } - diff --git a/lib/src/bitcoincash.dart b/lib/src/bitcoincash.dart index 4d040c6..0249a63 100644 --- a/lib/src/bitcoincash.dart +++ b/lib/src/bitcoincash.dart @@ -1,3 +1,11 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bitbox/bitbox.dart'; +import 'package:bitbox/src/utils/magic_hash.dart'; + +import 'utils/bip21.dart'; + /// Bitcoin Cash specific utilities class BitcoinCash { /// Converts Bitcoin Cash units to satoshi units @@ -14,4 +22,25 @@ class BitcoinCash { static int getByteCount(int inputs, int outputs) { return ((inputs * 148 * 4 + 34 * 4 * outputs + 10 * 4) / 4).ceil(); } -} \ No newline at end of file + + // Converts a [String] bch address and its [Map] options into [String] bip-21 uri + static String encodeBIP21(String address, Map options) { + return Bip21.encode(address, options); + } + + // Converts [String] bip-21 uri into a [Map] of bch address and its options + static Map decodeBIP21(String uri) { + return Bip21.decode(uri); + } + + // Sign a string message with privateKey in Bitcoin Signature format + static Uint8List signMessage(String message, [returnString = false]) { + Uint8List signatureBuffer = magicHash(message); + //return utf8.decode(signatureBuffer); + return signatureBuffer; + } + + static Uint8List? getOpReturnScript(String data) { + return compile([Opcodes.OP_RETURN, utf8.encode(data)]); + } +} diff --git a/lib/src/block.dart b/lib/src/block.dart new file mode 100644 index 0000000..c200fc1 --- /dev/null +++ b/lib/src/block.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; +import 'utils/rest_api.dart'; +import 'utils/rest_api.dart'; + +// Return details about a Block + +class Block { + // Lookup the block with a block height. + static Future detailsByHeight(number) async { + if (number is String) { + // Single Block + return await RestApi.sendGetRequest( + "block/detailsByHeight", number.toString()); + } else if (number is List) { + // Array of Blocks + return await RestApi.sendPostRequest( + "block/detailsByHeight", "heights", number); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } + +// Lookup the block with a block hash. + static Future detailsByHash(hash) async { + if (hash is String) { + // Single Block + return await RestApi.sendGetRequest("block/detailsByHash", hash); + } else if (hash is List) { + // Array of Blocks + return await RestApi.sendPostRequest( + "block/detailsByHash", "hashes", hash); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } +} diff --git a/lib/src/blockchain.dart b/lib/src/blockchain.dart new file mode 100644 index 0000000..78c1f10 --- /dev/null +++ b/lib/src/blockchain.dart @@ -0,0 +1,98 @@ +import 'utils/rest_api.dart'; + +class Blockchain { + // Hash of the best block in the longest blockchain. + static Future getBestBlockHash() async { + // Returns the hash of the best (tip) block in the longest blockchain. + return await RestApi.sendGetRequest("blockchain/getBestBlockHash"); + } + + // Info regarding blockchain processing + static Future getBlockchainInfo() async { + // Returns an object containing various state info regarding blockchain processing. + return await RestApi.sendGetRequest("blockchain/getBlockchainInfo"); + } + + // Number of blocks in the longest blockchain. + static Future getBlockCount() async { + // Returns the number of blocks in the longest blockchain. + return await RestApi.sendGetRequest("blockchain/getBlockCount"); + } + + // Information about blockheader hash + static Future getBlockHeader(hash) async { + if (hash is String) { + // If verbose is false, returns a string that is serialized, hex-encoded data for blockheader 'hash'. If verbose is true, returns an Object with information about blockheader hash. + return await RestApi.sendGetRequest( + "blockchain/getBlockHeader", '$hash?verbose=true'); + } else if (hash is List) { + // Bulk information about blockheader hash + return await RestApi.sendPostRequest( + "blockchain/getBlockHeader", "hashes", hash); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } + +// Information about all known tips in the block tree + static Future getChainTips() async { + // Return information about all known tips in the block tree, including the main chain as well as orphaned branches. + return await RestApi.sendGetRequest("blockchain/getChainTips"); + } + + // Proof-of-work difficulty + static Future getDifficulty() async { + // Returns the proof-of-work difficulty as a multiple of the minimum difficulty. + return await RestApi.sendGetRequest("blockchain/getDifficulty"); + } + + // Mempool data for transaction + static Future getMempoolEntry(txid) async { + if (txid is String) { + // Returns mempool data for given transaction + return await RestApi.sendGetRequest("blockchain/getMempoolEntry", txid); + } else if (txid is List) { + // Returns mempool data for given transaction + return await RestApi.sendPostRequest( + "block/getMempoolEntry", "txids", txid); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } + + // All transaction ids in memory pool. + static Future getRawMempool() async { + // Returns all transaction ids in memory pool as a json array of string transaction ids. + return await RestApi.sendGetRequest("blockchain/getRawMempool"); + } + + // Details about unspent transaction output. + static Future getTxOut(txid, n) async { + // Returns mempool data for given transaction + return await RestApi.sendGetRequest("blockchain/getTxOut", '$txid/$n'); + } + + // Hex-encoded proof that single txid was included. + static Future getTxOutProof(txid) async { + if (txid is String) { + // Returns a hex-encoded proof that 'txid' was included in a block. + return await RestApi.sendGetRequest("blockchain/getTxOutProof", txid); + } else if (txid is List) { + // Returns a hex-encoded proof that multiple txids were included in a block. + return await RestApi.sendPostRequest( + "block/getTxOutProof", "txids", txid); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } + + // Verify that a single proof points to a transaction in a block + static Future verifyTxOutProof(proof) async { + if (proof is String) { + // Returns a hex-encoded proof that 'txid' was included in a block. + return await RestApi.sendGetRequest("blockchain/verifyTxOutProof", proof); + } else if (proof is List) { + // Returns a hex-encoded proof that multiple txids were included in a block. + return await RestApi.sendPostRequest( + "blockchain/verifyTxOutProof", "proofs", proof); + } else + return throw ("Function parameter must be String for single block and List for multiple blocks"); + } +} diff --git a/lib/src/cashaccounts.dart b/lib/src/cashaccounts.dart new file mode 100644 index 0000000..0fc7e4c --- /dev/null +++ b/lib/src/cashaccounts.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'utils/rest_api.dart'; +import 'package:http/http.dart' as http; + +class CashAccounts { + static Future lookup(String account, int number, {int? collision}) async { + String col = ""; + if (collision != null) { + col = collision.toString(); + } + final response = await http.get(Uri.parse( + "https://rest.bitcoin.com/v2/cashAccounts/lookup/$account/$number/$col")); + return json.decode(response.body); + } + + static Future check(String account, int number) async { + final response = await http.get(Uri.parse( + "https://rest.bitcoin.com/v2/cashAccounts/check/$account/$number")); + return json.decode(response.body); + } + + static Future reverseLookup(String cashAddress) async { + final response = await http.get(Uri.parse( + "https://rest.bitcoin.com/v2/cashAccounts/reverseLookup/$cashAddress")); + return json.decode(response.body); + } + + static Future register(String name, String address) async { + Map register = { + 'name': name, + 'payments': [address] + }; + final response = await http.post( + Uri.parse('https://api.cashaccount.info/register'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(register)); + Map? data = jsonDecode(response.body); + return data; + } +} diff --git a/lib/src/crypto/crypto.dart b/lib/src/crypto/crypto.dart index 9ea97c3..908905e 100644 --- a/lib/src/crypto/crypto.dart +++ b/lib/src/crypto/crypto.dart @@ -5,17 +5,23 @@ import "package:pointycastle/macs/hmac.dart"; import "package:pointycastle/digests/ripemd160.dart"; import "package:pointycastle/digests/sha256.dart"; -Uint8List hash160(Uint8List buffer) { - Uint8List _tmp = new SHA256Digest().process(buffer); - return new RIPEMD160Digest().process(_tmp); -} +class Crypto { + static Uint8List hash160(Uint8List buffer) { + Uint8List _tmp = new SHA256Digest().process(buffer); + return new RIPEMD160Digest().process(_tmp); + } -Uint8List hmacSHA512(Uint8List key, Uint8List data) { - final _tmp = new HMac(new SHA512Digest(), 128)..init(new KeyParameter(key)); - return _tmp.process(data); -} + static Uint8List hmacSHA512(Uint8List key, Uint8List data) { + final _tmp = new HMac(new SHA512Digest(), 128)..init(new KeyParameter(key)); + return _tmp.process(data); + } -Uint8List hash256(Uint8List buffer) { - Uint8List _tmp = new SHA256Digest().process(buffer); - return new SHA256Digest().process(_tmp); -} \ No newline at end of file + static Uint8List hash256(Uint8List buffer) { + Uint8List _tmp = new SHA256Digest().process(buffer); + return new SHA256Digest().process(_tmp); + } + + static Uint8List sha256(Uint8List buffer) { + return new SHA256Digest().process(buffer); + } +} diff --git a/lib/src/crypto/ecies.dart b/lib/src/crypto/ecies.dart new file mode 100644 index 0000000..8b69372 --- /dev/null +++ b/lib/src/crypto/ecies.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:bitbox/src/privatekey.dart'; +import 'package:bitbox/src/publickey.dart'; +import 'package:collection/collection.dart'; +import 'package:hex/hex.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'package:pointycastle/digests/sha512.dart'; +import 'package:pointycastle/macs/hmac.dart'; +import 'package:pointycastle/pointycastle.dart'; + +/// A Class for performing Elliptic Curve Integrated Encryption Scheme operations. +/// +/// This class only makes provision for the "Electrum ECIES" aka "BIE1" serialization +/// format for the cipherText. +class Ecies { + static final ECDomainParameters _domainParams = + ECDomainParameters('secp256k1'); + static final SHA256Digest _sha256Digest = SHA256Digest(); + static final _tagLength = 32; //size of hmac + + /// Perform an ECIES encryption using AES for the symmetric cipher. + /// + /// [messageBuffer] - The buffer to encrypt. Note that the buffer in this instance has a very specific + /// encoding format called "BIE1" or "Electrum ECIES". It is in essence a serialization format with a + /// built-in checksum. + /// - bytes [0 - 4] : Magic value. Literally "BIE1". + /// - bytes [4 - 37] : Compressed Public Key + /// - bytes [37 - (length - 32) ] : Actual cipherText + /// - bytes [ length - 32 ] : (last 32 bytes) Checksum value + /// + /// [senderPrivateKey] - Private Key of the sending party + /// + /// [recipientPublicKey] - Public Key of the party who can decrypt the message + /// + static String encryptData( + {required String message, + required String senderPrivateKeyHex, + required String recipientPublicKeyHex, + String magicValue = "BIE1"}) { + //Encryption requires derivation of a cipher using the other party's Public Key + // Bob is sender, Alice is recipient of encrypted message + // Qb = k o Qa, where + // Qb = Bob's Public Key; + // k = Bob's private key; + // Qa = Alice's public key; + + BCHPrivateKey senderPrivateKey = BCHPrivateKey.fromHex(senderPrivateKeyHex); + BCHPublicKey recipientPublicKey = + BCHPublicKey.fromHex(recipientPublicKeyHex); + + List messageBuffer = Uint8List.fromList(message.codeUnits); + + final ECPoint S = (recipientPublicKey.point! * + senderPrivateKey.privateKey)!; //point multiplication + + final pubkeyS = BCHPublicKey.fromXY(S.x!.toBigInteger()!, S.y!.toBigInteger()!); + final pubkeyBuffer = HEX.decode(pubkeyS.getEncoded(true)); + final pubkeyHash = SHA512Digest().process(pubkeyBuffer as Uint8List); + + //initialization vector parameters + final iv = pubkeyHash.sublist(0, 16); + final kE = pubkeyHash.sublist(16, 32); + final kM = pubkeyHash.sublist(32, 64); + + CipherParameters params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(kE), iv), null); + BlockCipher encryptionCipher = PaddedBlockCipher('AES/CBC/PKCS7'); + encryptionCipher.init(true, params); + + final cipherText = encryptionCipher.process(messageBuffer as Uint8List); + + final magic = utf8.encode(magicValue); + + final encodedBuffer = Uint8List.fromList( + magic + HEX.decode(senderPrivateKey.publicKey!.toHex()) + cipherText); + + //calc checksum + final hmac = _calculateHmac(kM, encodedBuffer); + + return HEX.encode(encodedBuffer + hmac); + } + + static Uint8List _calculateHmac(Uint8List kM, Uint8List encodedBuffer) { + final sha256Hmac = HMac(_sha256Digest, 64); + sha256Hmac.init(KeyParameter(kM)); + final calculatedChecksum = sha256Hmac.process(encodedBuffer); + return calculatedChecksum; + } + + /// Perform an ECIES decryption using AES for the symmetric cipher. + /// + /// [cipherText] - The buffer to decrypt. Note that the buffer in this instance has a very specific + /// encoding format called "BIE1" or "Electrum ECIES". It is in essence a serialization format with a + /// built-in checksum. + /// - bytes [0 - 4] : Magic value. Literally "BIE1". + /// - bytes [4 - 37] : Compressed Public Key + /// - bytes [37 - (length - 32) ] : Actual cipherText + /// - bytes [ length - 32 ] : (last 32 bytes) Checksum valu + /// + /// [recipientPrivateKey] - Private Key of the receiving party + /// + static String decryptData( + {required String cipherTextStr, + required String recipientPrivateKeyHex, + String magicValue = "BIE1"}) { + //AES Cipher is calculated as + //1) S = recipientPrivateKey o senderPublicKey + //2) cipher = S.x + + List cipherText = HEX.decode(cipherTextStr); + + BCHPrivateKey recipientPrivateKey = + BCHPrivateKey.fromHex(recipientPrivateKeyHex); + + if (cipherText.length < 37) { + throw Exception('Buffer is too small '); + } + + final magic = utf8.decode(cipherText.sublist(0, 4)); + + if (magic != magicValue) { + throw Exception('Not a $magicValue-encoded buffer'); + } + + final senderPubkeyBuffer = cipherText.sublist(4, 37); + final senderPublicKey = + BCHPublicKey.fromHex(HEX.encode(senderPubkeyBuffer)); + + //calculate S = recipientPrivateKey o senderPublicKey + final S = (senderPublicKey.point! * + recipientPrivateKey.privateKey)!; //point multiplication + final cipher = S.x; + + if (cipherText.length - _tagLength <= 37) { + throw Exception( + 'Invalid Checksum detected. Combined sum of Checksum and Message makes no sense'); + } + + //validate the checksum bytes + final pubkeyS = BCHPublicKey.fromXY(S.x!.toBigInteger()!, S.y!.toBigInteger()!); + final pubkeyBuffer = HEX.decode(pubkeyS.getEncoded(true)); + final pubkeyHash = SHA512Digest().process(pubkeyBuffer as Uint8List); + + //initialization vector parameters + final iv = pubkeyHash.sublist(0, 16); + final kE = pubkeyHash.sublist(16, 32); + final kM = pubkeyHash.sublist(32, 64); + + final message = Uint8List.fromList( + cipherText.sublist(0, cipherText.length - _tagLength)); + + final Uint8List hmac = _calculateHmac(kM, message); + + final Uint8List messageChecksum = + cipherText.sublist(cipherText.length - _tagLength, cipherText.length) as Uint8List; + + // ignore: prefer_const_constructors + if (!ListEquality().equals(messageChecksum, hmac)) { + throw Exception('HMAC checksum failed to validate'); + } + + //decrypt! + CipherParameters params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(kE), iv), null); + BlockCipher decryptionCipher = PaddedBlockCipher("AES/CBC/PKCS7"); + decryptionCipher.init(false, params); + + final decrypted = decryptionCipher.process( + cipherText.sublist(37, cipherText.length - _tagLength) as Uint8List); + return utf8.decode(decrypted); + } +} diff --git a/lib/src/crypto/ecurve.dart b/lib/src/crypto/ecurve.dart index 5c66169..56e2912 100644 --- a/lib/src/crypto/ecurve.dart +++ b/lib/src/crypto/ecurve.dart @@ -2,18 +2,40 @@ import 'dart:typed_data'; import 'package:hex/hex.dart'; import 'package:pointycastle/ecc/api.dart' show ECPoint; import 'package:pointycastle/ecc/curves/secp256k1.dart'; -import "package:pointycastle/src/utils.dart"; class ECurve { static final secp256k1 = new ECCurve_secp256k1(); static final n = secp256k1.n; static final G = secp256k1.G; - static final ZERO32 = Uint8List.fromList(List.generate(32, (index) => 0)); - static final EC_GROUP_ORDER = HEX.decode("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); - static final EC_P = HEX.decode("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + static final zero32 = Uint8List.fromList(List.generate(32, (index) => 0)); + static final ecGroupOrder = HEX.decode( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + static final ecP = HEX.decode( + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + /// Decode a BigInt from bytes in big-endian encoding. + static BigInt decodeBigInt(List bytes) { + var result = BigInt.from(0); + for (var i = 0; i < bytes.length; i++) { + result += BigInt.from(bytes[bytes.length - i - 1]) << (8 * i); + } + return result; + } + + /// Encode a BigInt into bytes using big-endian encoding. + static Uint8List encodeBigInt(BigInt number) { + var _byteMask = BigInt.from(0xff); + // Not handling negative numbers. Decide how you want to do that. + var size = (number.bitLength + 7) >> 3; + var result = Uint8List(size); + for (var i = 0; i < size; i++) { + result[size - i - 1] = (number & _byteMask).toInt(); + number = number >> 8; + } + return result; + } - static Uint8List privateAdd (Uint8List d,Uint8List tweak) { + static Uint8List? privateAdd(Uint8List d, Uint8List tweak) { // if (!isPrivate(d)) throw new ArgumentError(THROW_BAD_PRIVATE); // if (!isOrderScalar(tweak)) throw new ArgumentError(THROW_BAD_TWEAK); BigInt dd = decodeBigInt(d); @@ -23,33 +45,34 @@ class ECurve { return dt; } - static bool isPrivate (Uint8List x) { + static bool isPrivate(Uint8List x) { if (!isScalar(x)) return false; - return _compare(x, ZERO32) > 0 && // > 0 - _compare(x, EC_GROUP_ORDER) < 0; // < G + return _compare(x, zero32) > 0 && // > 0 + _compare(x, ecGroupOrder as Uint8List) < 0; // < G } - static bool isScalar (Uint8List x) { + static bool isScalar(Uint8List x) { return x.length == 32; } - static Uint8List pointFromScalar(Uint8List d, bool _compressed) { + static Uint8List? pointFromScalar(Uint8List d, bool _compressed) { // if (!isPrivate(d)) throw new ArgumentError(THROW_BAD_PRIVATE); BigInt dd = decodeBigInt(d); - ECPoint pp = G * dd; + ECPoint pp = (G * dd)!; if (pp.isInfinity) return null; return pp.getEncoded(_compressed); } - static Uint8List pointAddScalar(Uint8List p,Uint8List tweak, bool _compressed) { + static Uint8List? pointAddScalar( + Uint8List p, Uint8List tweak, bool _compressed) { // if (!isPoint(p)) throw new ArgumentError(THROW_BAD_POINT); // if (!isOrderScalar(tweak)) throw new ArgumentError(THROW_BAD_TWEAK); bool compressed = assumeCompression(_compressed, p); - ECPoint pp = decodeFrom(p); - if (_compare(tweak, ZERO32) == 0) return pp.getEncoded(compressed); + ECPoint? pp = decodeFrom(p); + if (_compare(tweak, zero32) == 0) return pp!.getEncoded(compressed); BigInt tt = decodeBigInt(tweak); - ECPoint qq = G * tt; - ECPoint uu = pp + qq; + ECPoint? qq = G * tt; + ECPoint uu = (pp! + qq)!; if (uu.isInfinity) return null; return uu.getEncoded(compressed); } @@ -75,25 +98,25 @@ class ECurve { var t = p[0]; var x = p.sublist(1, 33); - if (_compare(x, ZERO32) == 0) { + if (_compare(x, zero32) == 0) { return false; } - if (_compare(x, EC_P) == 1) { + if (_compare(x, ecP as Uint8List) == 1) { return false; } try { decodeFrom(p); - } catch(err) { + } catch (err) { return false; } if ((t == 0x02 || t == 0x03) && p.length == 33) { return true; } var y = p.sublist(33); - if (_compare(y, ZERO32) == 0) { + if (_compare(y, zero32) == 0) { return false; } - if (_compare(y, EC_P) == 1) { + if (_compare(y, ecP as Uint8List) == 1) { return false; } if (t == 0x04 && p.length == 65) { @@ -102,9 +125,9 @@ class ECurve { return false; } - static bool _isPointCompressed (Uint8List p) { + static bool _isPointCompressed(Uint8List p) { return p[0] != 0x04; } - static ECPoint decodeFrom(Uint8List P) => secp256k1.curve.decodePoint(P); -} \ No newline at end of file + static ECPoint? decodeFrom(Uint8List P) => secp256k1.curve.decodePoint(P); +} diff --git a/lib/src/ecpair.dart b/lib/src/ecpair.dart index fc6ee82..06d4e32 100644 --- a/lib/src/ecpair.dart +++ b/lib/src/ecpair.dart @@ -9,17 +9,17 @@ import 'utils/network.dart'; /// Stores a keypair and provides various methods and factories for creating it and working with it class ECPair { - final Uint8List _d; - final Uint8List _Q; + final Uint8List? _d; + final Uint8List? _q; final Network network; - final bool compressed; + final bool? compressed; /// Default constructor. If [network] is not provided, it will assume Bitcoin Cash mainnet - ECPair(this._d, this._Q, {network, this.compressed = true}): - this.network = network ?? Network.bitcoinCash(); + ECPair(this._d, this._q, {network, this.compressed = true}) + : this.network = network ?? Network.bitcoinCash(); /// Creates a keypair from the private key provided in WIF format - factory ECPair.fromWIF(String wifPrivateKey, {Network network}) { + factory ECPair.fromWIF(String wifPrivateKey, {Network? network}) { wif.WIF decoded = wif.decode(wifPrivateKey); final version = decoded.version; // TODO support multi networks @@ -37,47 +37,48 @@ class ECPair { } } return ECPair.fromPrivateKey(decoded.privateKey, - compressed: decoded.compressed, network: nw); + compressed: decoded.compressed, network: nw); } /// Creates a keypair from [publicKey. The returned keypair will contain [null] private key - factory ECPair.fromPublicKey(Uint8List publicKey, {Network network, bool compressed}) { + factory ECPair.fromPublicKey(Uint8List publicKey, + {Network? network, bool? compressed}) { if (!ecc.isPoint(publicKey)) { throw ArgumentError("Point is not on the curve"); } - return ECPair(null, publicKey, - network: network, compressed: compressed); + return ECPair(null, publicKey, network: network, compressed: compressed); } /// Creates a keypair from [privateKey] - factory ECPair.fromPrivateKey(Uint8List privateKey, {Network network, bool compressed}) { + factory ECPair.fromPrivateKey(Uint8List privateKey, + {Network? network, bool? compressed}) { if (privateKey.length != 32) throw ArgumentError( - "Expected property privateKey of type Buffer(Length: 32)"); + "Expected property privateKey of type Buffer(Length: 32)"); if (!ecc.isPrivate(privateKey)) throw ArgumentError("Private key not in range [1, n)"); - return ECPair(privateKey, null, - network: network, compressed: compressed); + return ECPair(privateKey, null, network: network, compressed: compressed); } /// Creates a random keypair - factory ECPair.makeRandom({Network network, bool compressed, Function rng}) { + factory ECPair.makeRandom({Network? network, bool? compressed, Function? rng}) { final rfunc = rng ?? _randomBytes; - Uint8List d; + Uint8List? d; // int beginTime = DateTime.now().millisecondsSinceEpoch; do { d = rfunc(32); - if (d.length != 32) throw ArgumentError("Expected Buffer(Length: 32)"); + if (d!.length != 32) throw ArgumentError("Expected Buffer(Length: 32)"); // if (DateTime.now().millisecondsSinceEpoch - beginTime > 5000) throw ArgumentError("Timeout"); } while (!ecc.isPrivate(d)); return ECPair.fromPrivateKey(d, network: network, compressed: compressed); } - Uint8List get publicKey => _Q ?? ecc.pointFromScalar(_d, compressed); + Uint8List? get publicKey => _q ?? ecc.pointFromScalar(_d!, compressed!); - Uint8List get privateKey => _d; + Uint8List? get privateKey => _d; - String get address => Address.toBase58Check(hash160(publicKey), network.pubKeyHash); + String get address => + Address.toBase58Check(Crypto.hash160(publicKey!), network.pubKeyHash); /// Returns the private key in WIF format String toWIF() { @@ -85,17 +86,19 @@ class ECPair { throw ArgumentError("Missing private key"); } return wif.encode(wif.WIF( - version: network.private, privateKey: privateKey, compressed: compressed)); + version: network.private, + privateKey: privateKey!, + compressed: compressed!)); } /// Signs the provided [hash] with the private key Uint8List sign(Uint8List hash) { - return ecc.sign(hash, privateKey); + return ecc.sign(hash, privateKey!); } /// Verifies whether the provided [signature] matches the [hash] using the keypair's [publicKey] bool verify(Uint8List hash, Uint8List signature) { - return ecc.verify(hash, publicKey, signature); + return ecc.verify(hash, publicKey!, signature); } } @@ -107,4 +110,4 @@ Uint8List _randomBytes(int size) { bytes[i] = rng.nextInt(_SIZE_BYTE); } return bytes; -} \ No newline at end of file +} diff --git a/lib/src/encoding/base58check.dart b/lib/src/encoding/base58check.dart new file mode 100644 index 0000000..ca8a272 --- /dev/null +++ b/lib/src/encoding/base58check.dart @@ -0,0 +1,136 @@ +import 'dart:typed_data'; +import 'dart:convert'; +import 'utils.dart'; +import 'package:collection/collection.dart'; +import '../exceptions.dart'; + +var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +List decode(String input) { + if (input.isEmpty) { + return []; + } + + var encodedInput = utf8.encode(input); + var uintAlphabet = utf8.encode(ALPHABET); + + List INDEXES = List.filled(128, -1); + for (int i = 0; i < ALPHABET.length; i++) { + INDEXES[uintAlphabet[i]] = i; + } + + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + List input58 = List.generate(encodedInput.length, ((index) => index)); + input58.fillRange(0, input58.length, 0); + for (int i = 0; i < encodedInput.length; ++i) { + var c = encodedInput[i]; + var digit = c < 128 ? INDEXES[c]! : -1; + if (digit < 0) { + var buff = [c]; + var invalidChar = utf8.decode(buff as List); + throw new AddressFormatException( + "Illegal character " + invalidChar + " at position " + i.toString()); + } + input58[i] = digit; + } + + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + + // Convert base-58 digits to base-256 digits. + var decoded = List.filled(encodedInput.length, 0); + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length;) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + + // Return decoded data (including original number of leading zeros). + return decoded.sublist(outputStart - zeros, decoded.length); +} + +/** + * Divides a number, represented as an array of bytes each containing a single digit + * in the specified base, by the given divisor. The given number is modified in-place + * to contain the quotient, and the return value is the remainder. + */ +divmod(List number, int firstDigit, int base, int divisor) { +// this is just long division which accounts for the base of the input digits + int remainder = 0; + for (int i = firstDigit; i < number.length; i++) { + int digit = number[i]! & 0xFF; + int temp = remainder * base + digit; + number[i] = (temp / divisor).toInt(); + remainder = temp % divisor; + } + + return remainder.toSigned(8); +} + +/** + * Encodes the given bytes as a base58 string (no checksum is appended). + */ +Uint8List encode(List encodedInput) { + var uintAlphabet = utf8.encode(ALPHABET); + var ENCODED_ZERO = uintAlphabet[0]; + +// var encodedInput = utf8.encode(input); + + if (encodedInput.isEmpty) { + return [] as Uint8List; + } + + // Count leading zeros. + int zeros = 0; + while (zeros < encodedInput.length && encodedInput[zeros] == 0) { + ++zeros; + } + + // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) + //input = Arrays.copyOf(input, input.length); // since we modify it in-place + Uint8List encoded = + Uint8List(encodedInput.length * 2); // upper bound <----- ??? + int outputStart = encoded.length; + for (int inputStart = zeros; inputStart < encodedInput.length;) { + encoded[--outputStart] = + uintAlphabet[divmod(encodedInput, inputStart, 256, 58)]; + if (encodedInput[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) { + ++outputStart; + } + while (--zeros >= 0) { + encoded[--outputStart] = ENCODED_ZERO; + } + // Return encoded string (including encoded leading zeros). + return encoded.sublist(outputStart, encoded.length); +} + +List decodeChecked(String input) { + List decoded = decode(input); + if (decoded.length < 4) throw new AddressFormatException("Input too short"); + + List data = decoded.sublist(0, decoded.length - 4); + List checksum = decoded.sublist(decoded.length - 4, decoded.length); + List actualChecksum = sha256Twice(data).sublist(0, 4); + + var byteConverted = actualChecksum + .map((elem) => elem.toSigned(8)); //convert unsigned list back to signed + if (!IterableEquality().equals(checksum, byteConverted)) + throw new BadChecksumException("Checksum does not validate"); + + return data; +} diff --git a/lib/src/encoding/utils.dart b/lib/src/encoding/utils.dart new file mode 100644 index 0000000..7c37ca3 --- /dev/null +++ b/lib/src/encoding/utils.dart @@ -0,0 +1,327 @@ +import 'package:bitbox/src/exceptions.dart'; +import 'package:hex/hex.dart'; +import 'package:pointycastle/digests/ripemd160.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'dart:typed_data'; +import 'package:buffer/buffer.dart'; +import 'dart:math'; + +//import 'package:pointycastle/src/utils.dart'; +import 'package:pointycastle/export.dart'; + +List sha256Twice(List bytes) { + var first = new SHA256Digest().process(Uint8List.fromList(bytes as List)); + var second = new SHA256Digest().process(first); + return second.toList(); +} + +List sha256(List bytes) { + return new SHA256Digest().process(Uint8List.fromList(bytes)).toList(); +} + +List sha1(List bytes) { + return new SHA1Digest().process(Uint8List.fromList(bytes)).toList(); +} + +List hash160(List bytes) { + List shaHash = new SHA256Digest().process(Uint8List.fromList(bytes)); + var ripeHash = new RIPEMD160Digest().process(shaHash as Uint8List); + return ripeHash.toList(); +} + +List ripemd160(List bytes) { + var ripeHash = new RIPEMD160Digest().process(Uint8List.fromList(bytes)); + return ripeHash.toList(); +} + +int hexToUint16(List hexBuffer) { + return int.parse(HEX.encode(hexBuffer), radix: 16).toUnsigned(16); +} + +int hexToInt32(List hexBuffer) { + return int.parse(HEX.encode(hexBuffer), radix: 16).toSigned(32); +} + +int hexToUint32(List hexBuffer) { + return int.parse(HEX.encode(hexBuffer), radix: 16).toUnsigned(32); +} + +int hexToInt64(List hexBuffer) { + return int.parse(HEX.encode(hexBuffer), radix: 16).toSigned(64); +} + +BigInt hexToUint64(List hexBuffer) { + return BigInt.parse(HEX.encode(hexBuffer), radix: 16).toUnsigned(64); +} + +List varintBufNum(n) { +// List buf ; + ByteDataWriter writer = ByteDataWriter(); + if (n < 253) { + writer.writeUint8(n); + } else if (n < 0x10000) { + writer.writeUint8(253); + writer.writeUint16(n, Endian.little); + } else if (n < 0x100000000) { + writer.writeUint8(254); + writer.writeUint32(n, Endian.little); + } else { + writer.writeUint8(255); + writer.writeInt32(n & -1, Endian.little); + writer.writeUint32((n / 0x100000000).floor(), Endian.little); + } + return writer.toBytes().toList(); +} + +Uint8List varIntWriter(int length) { + ByteDataWriter writer = ByteDataWriter(); + + if (length == null) { + return writer.toBytes(); + } + + if (length < 0xFD) { + writer.writeUint8(length); + return writer.toBytes(); + } + + if (length < 0xFFFF) { +// return HEX.decode("FD" + length.toRadixString(16)); + writer.writeUint8(253); + writer.writeUint16(length, Endian.little); + return writer.toBytes(); + } + + if (length < 0xFFFFFFFF) { +// return HEX.decode("FE" + length.toRadixString(16)); + + writer.writeUint8(254); + writer.writeUint32(length, Endian.little); + return writer.toBytes(); + } + + if (length < 0xFFFFFFFFFFFFFFFF) { +// return HEX.decode("FF" + length.toRadixString(16)); + + writer.writeUint8(255); + writer.writeInt32(length & -1, Endian.little); + writer.writeUint32((length / 0x100000000).floor(), Endian.little); + return writer.toBytes(); + } + + return writer.toBytes(); +} + +List calcVarInt(int length) { + if (length == null) return Uint8List(0); + + if (length < 0xFD) return HEX.decode(length.toRadixString(16)); + + if (length < 0xFFFF) return HEX.decode("FD" + length.toRadixString(16)); + + if (length < 0xFFFFFFFF) return HEX.decode("FE" + length.toRadixString(16)); + + if (length < 0xFFFFFFFFFFFFFFFF) + return HEX.decode("FF" + length.toRadixString(16)); + + return Uint8List(0); +} + +int readVarIntNum(ByteDataReader reader) { + var first = reader.readUint8(); + switch (first) { + case 0xFD: + return reader.readUint16(Endian.little); + break; + case 0xFE: + return reader.readUint32(Endian.little); + break; + case 0xFF: + var bn = BigInt.from(reader.readUint64(Endian.little)); + var n = bn.toInt(); + if (n <= pow(2, 53)) { + return n; + } else { + throw new Exception( + 'number too large to retain precision - use readVarintBN'); + } + break; + default: + return first; + } +} + +//FIXME: Should probably have two versions of this function. One for BigInt, one for Int +BigInt readVarInt(Uint8List buffer) { + var first = + int.parse(HEX.encode(buffer.sublist(0, 1)), radix: 16).toUnsigned(8); + + switch (first) { + case 0xFD: + return BigInt.from( + hexToUint16(buffer.sublist(1, 3))); //2 bytes == Uint16 + + case 0xFE: + return BigInt.from(hexToUint32(buffer.sublist(1, 5))); //4 bytes == Uint32 + + case 0xFF: + return hexToUint64(buffer.sublist(1, 9)); //8 bytes == Uint64 + + default: + return BigInt.from(first); + } +} + +int? getBufferOffset(int count) { + if (count < 0xFD) return 1; + + if (count == 0xFD) return 3; //2 bytes == Uint16 + + if (count == 0xFE) return 5; //4 bytes == Uint32 + + if (count == 0xFF) return 9; +} + +/// Decode a BigInt from bytes in big-endian encoding. +BigInt decodeBigInt(List bytes) { + BigInt result = new BigInt.from(0); + + for (int i = 0; i < bytes.length; i++) { + result += new BigInt.from(bytes[bytes.length - i - 1]) << (8 * i); + } + + return result; +} + +var _byteMask = new BigInt.from(0xff); + +/// Encode a BigInt into bytes using big-endian encoding. +/// Provide a minimum byte length `minByteLength` +Uint8List encodeBigInt(BigInt number, {int minByteLength = 0}) { + int size = (number.bitLength + 7) >> 3; + size = minByteLength > size ? minByteLength : size; + + var result = Uint8List(size); + for (int i = 0; i < size; i++) { + result[size - i - 1] = (number & _byteMask).toInt(); + number = number >> 8; + } + + return result; +} + +toScriptNumBuffer(BigInt value) { + return toSM(value, endian: Endian.little); +} + +BigInt fromScriptNumBuffer(Uint8List buf, bool fRequireMinimal, + {int nMaxNumSize = 4}) { + if (!(buf.length <= nMaxNumSize)) { + throw new ScriptException('script number overflow'); + } + + if (fRequireMinimal && buf.length > 0) { + // Check that the number is encoded with the minimum possible + // number of bytes. + // + // If the most-significant-byte - excluding the sign bit - is zero + // then we're not minimal. Note how this test also rejects the + // negative-zero encoding, 0x80. + if ((buf[buf.length - 1] & 0x7f) == 0) { + // One exception: if there's more than one byte and the most + // significant bit of the second-most-significant-byte is set + // it would conflict with the sign bit. An example of this case + // is +-255, which encode to 0xff00 and 0xff80 respectively. + // (big-endian). + if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) == 0) { + throw new Exception('non-minimally encoded script number'); + } + } + } + return fromSM(buf, endian: Endian.little); +} + +toSM(BigInt value, {Endian endian = Endian.big}) { + var buf = toSMBigEndian(value); + + if (endian == Endian.little) { + buf = buf.reversed.toList(); + } + return buf; +} + +List toSMBigEndian(BigInt value) { + List buf = []; + if (value.compareTo(BigInt.zero) == -1) { + buf = toBuffer(-value); + if (buf[0] & 0x80 != 0) { + buf = [0x80] + buf; + } else { + buf[0] = buf[0] | 0x80; + } + } else { + buf = toBuffer(value); + if (buf[0] & 0x80 != 0) { + buf = [0x00] + buf; + } + } + + if (buf.length == 1 && buf[0] == 0) { + buf = []; + } + return buf; +} + +BigInt fromSM(Uint8List buf, {Endian endian = Endian.big}) { + BigInt ret; + List localBuffer = buf.toList(); + if (localBuffer.length == 0) { + return decodeBigInt([0]); + } + + if (endian == Endian.little) { + localBuffer = buf.reversed.toList(); + } + + if (localBuffer[0] & 0x80 != 0) { + localBuffer[0] = localBuffer[0] & 0x7f; + ret = decodeBigInt(localBuffer); + ret = (-ret); + } else { + ret = decodeBigInt(localBuffer); + } + + return ret; +} + +//FIXME: New implementation. Untested +List toBuffer(BigInt value, {int size = 0, Endian endian = Endian.big}) { + String hex; + List buf = []; + if (size != 0) { + hex = value.toRadixString(16); + int natlen = (hex.length / 2) as int; + buf = HEX.decode(hex); + + if (natlen == size) { + // buf = buf + } else if (natlen > size) { + buf = buf.sublist(natlen - buf.length, buf.length); +// buf = BN.trim(buf, natlen); + } else if (natlen < size) { + List padding = [size]; + padding.fillRange(0, size, 0); + buf.insertAll(0, padding); +// buf = BN.pad(buf, natlen, opts.size) + } + } else { + hex = value.toRadixString(16); + buf = HEX.decode(hex); + } + + if (endian == Endian.little) { + buf = buf.reversed.toList(); + } + + return buf; +} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart new file mode 100644 index 0000000..aaa981a --- /dev/null +++ b/lib/src/exceptions.dart @@ -0,0 +1,119 @@ +class AddressFormatException implements Exception { + String cause; + + AddressFormatException(this.cause); +} + +class BadChecksumException implements AddressFormatException { + String cause; + + BadChecksumException(this.cause); +} + +class BadParameterException implements Exception { + String cause; + + BadParameterException(this.cause); +} + +class InvalidPointException implements Exception { + String cause; + + InvalidPointException(this.cause); +} + +class InvalidNetworkException implements Exception { + String cause; + + InvalidNetworkException(this.cause); +} + +class InvalidKeyException implements Exception { + String cause; + + InvalidKeyException(this.cause); +} + +class IllegalArgumentException implements Exception { + String cause; + + IllegalArgumentException(this.cause); +} + +class DerivationException implements Exception { + String cause; + + DerivationException(this.cause); +} + +class InvalidPathException implements Exception { + String cause; + + InvalidPathException(this.cause); +} + +class UTXOException implements Exception { + String cause; + + UTXOException(this.cause); +} + +class TransactionAmountException implements Exception { + String cause; + + TransactionAmountException(this.cause); +} + +class ScriptException implements Exception { + String cause; + + ScriptException(this.cause); +} + +class SignatureException implements Exception { + String cause; + + SignatureException(this.cause); +} + +class TransactionFeeException implements Exception { + String cause; + + TransactionFeeException(this.cause); +} + +class InputScriptException implements Exception { + String cause; + + InputScriptException(this.cause); +} + +class TransactionException implements Exception { + String cause; + + TransactionException(this.cause); +} + +class LockTimeException implements Exception { + String cause; + + LockTimeException(this.cause); +} + +class InterpreterException implements Exception { + String cause; + + InterpreterException(this.cause); +} + +class BlockException implements Exception { + String cause; + + BlockException(this.cause); +} + +class MerkleTreeException implements Exception { + String cause; + + MerkleTreeException(this.cause); +} diff --git a/lib/src/hdnode.dart b/lib/src/hdnode.dart index 6f8f945..44edbb3 100644 --- a/lib/src/hdnode.dart +++ b/lib/src/hdnode.dart @@ -35,13 +35,13 @@ class HDNode { int index = 0; int parentFingerprint = 0x00000000; - Uint8List get identifier => hash160(publicKeyList); + Uint8List get identifier => Crypto.hash160(publicKeyList!); Uint8List get fingerprint => identifier.sublist(0, 4); - Uint8List get publicKeyList => _keyPair.publicKey; + Uint8List? get publicKeyList => _keyPair.publicKey; - String get privateKey => HEX.encode(_keyPair.privateKey); - String get publicKey => HEX.encode(publicKeyList); + String get privateKey => HEX.encode(_keyPair.privateKey!); + String get publicKey => HEX.encode(publicKeyList!); get rawPrivateKey => _keyPair.privateKey; @@ -57,7 +57,7 @@ class HDNode { final key = utf8.encode('Bitcoin seed'); - final I = hmacSHA512(key, seed); + final I = Crypto.hmacSHA512(key as Uint8List, seed); final keyPair = ECPair(I.sublist(0, 32), null, network: network); @@ -68,7 +68,8 @@ class HDNode { /// Creates [HDNode] from extended public key factory HDNode.fromXPub(String xPub) { - final network = xPub[0] == "x" ? Network.bitcoinCash() : Network.bitcoinCashTest(); + final network = + xPub[0] == "x" ? Network.bitcoinCash() : Network.bitcoinCashTest(); HDNode hdNode = HDNode._fromBase58(xPub, network); return hdNode; @@ -76,13 +77,13 @@ class HDNode { /// Creates [HDNode] from extended private key factory HDNode.fromXPriv(String xPriv) { - final network = xPriv[0] == "x" ? Network.bitcoinCash() : Network.bitcoinCashTest(); + final network = + xPriv[0] == "x" ? Network.bitcoinCash() : Network.bitcoinCashTest(); HDNode hdNode = HDNode._fromBase58(xPriv, network); return hdNode; } - factory HDNode._fromBase58(String string, Network network) { Uint8List buffer = bs58check.decode(string); if (buffer.length != 78) throw new ArgumentError("Invalid buffer length"); @@ -99,7 +100,8 @@ class HDNode { // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) var parentFingerprint = bytes.getUint32(5); if (depth == 0) { - if (parentFingerprint != 0x00000000) throw new ArgumentError("Invalid parent fingerprint"); + if (parentFingerprint != 0x00000000) + throw new ArgumentError("Invalid parent fingerprint"); } // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. @@ -113,7 +115,8 @@ class HDNode { ECPair keyPair; // 33 bytes: private key data (0x00 + k) if (version == network.bip32Private) { - if (bytes.getUint8(45) != 0x00) throw new ArgumentError("Invalid private key"); + if (bytes.getUint8(45) != 0x00) + throw new ArgumentError("Invalid private key"); Uint8List d = buffer.sublist(46, 78); keyPair = ECPair(d, null, network: network); // hdNode = HDNode.fromPrivateKey(d, chainCode, network); @@ -164,14 +167,14 @@ class HDNode { throw ArgumentError("Missing private key for hardened child key"); } data[0] = 0x00; - data.setRange(1, 33, _keyPair.privateKey); + data.setRange(1, 33, _keyPair.privateKey!); data.buffer.asByteData().setUint32(33, index); } else { - data.setRange(0, 33, publicKeyList); + data.setRange(0, 33, publicKeyList!); data.buffer.asByteData().setUint32(33, index); } - final I = hmacSHA512(_chainCode, data); + final I = Crypto.hmacSHA512(_chainCode, data); final IL = I.sublist(0, 32); final IR = I.sublist(32); // if (!ecc.isPrivate(IL)) { @@ -179,14 +182,14 @@ class HDNode { // } ECPair derivedKeyPair; if (!_isNeutered()) { - final ki = ECurve.privateAdd(_keyPair.privateKey, IL); + final ki = ECurve.privateAdd(_keyPair.privateKey!, IL); if (ki == null) return derive(index + 1); derivedKeyPair = ECPair(ki, null, network: this._keyPair.network); } else { - final ki = ECurve.pointAddScalar(publicKeyList, IL, true); + final ki = ECurve.pointAddScalar(publicKeyList!, IL, true); if (ki == null) return derive(index + 1); - + derivedKeyPair = ECPair(null, ki, network: this._keyPair.network); } final hd = HDNode(derivedKeyPair, IR); @@ -207,14 +210,21 @@ class HDNode { String toLegacyAddress() => _keyPair.address; /// Returns HDNode's address in cashAddr format - String toCashAddress() => Address.toCashAddress(toLegacyAddress()); + String? toCashAddress() => Address.toCashAddress(toLegacyAddress()); + + /// Returns HDNode's address in slpAddr format + String? toSLPAddress() => Address.toSLPAddress(toLegacyAddress()); + + String? toTokenAddress() => Address.toTokenAddress(toLegacyAddress()); HDNode _deriveHardened(int index) { return derive(index + HIGHEST_BIT); } String _toBase58() { - final version = (!_isNeutered()) ? this._keyPair.network.bip32Private : this._keyPair.network.bip32Public; + final version = (!_isNeutered()) + ? this._keyPair.network.bip32Private + : this._keyPair.network.bip32Public; Uint8List buffer = new Uint8List(78); ByteData bytes = buffer.buffer.asByteData(); bytes.setUint32(0, version); @@ -224,15 +234,17 @@ class HDNode { buffer.setRange(13, 45, _chainCode); if (!_isNeutered()) { bytes.setUint8(45, 0); - buffer.setRange(46, 78, _keyPair.privateKey); + buffer.setRange(46, 78, _keyPair.privateKey!); } else { - buffer.setRange(45, 78, publicKeyList); + buffer.setRange(45, 78, publicKeyList!); } return bs58check.encode(buffer); } HDNode _neutered() { - final neutered = HDNode(ECPair(null, this.publicKeyList, network: this._keyPair.network), _chainCode); + final neutered = HDNode( + ECPair(null, this.publicKeyList, network: this._keyPair.network), + _chainCode); neutered.depth = this.depth; neutered.index = this.index; neutered.parentFingerprint = this.parentFingerprint; @@ -242,4 +254,4 @@ class HDNode { bool _isNeutered() { return this._keyPair.privateKey == null; } -} \ No newline at end of file +} diff --git a/lib/src/networks.dart b/lib/src/networks.dart new file mode 100644 index 0000000..4f5512d --- /dev/null +++ b/lib/src/networks.dart @@ -0,0 +1,113 @@ +import 'exceptions.dart'; +import 'dart:typed_data'; +import 'package:hex/hex.dart'; + +enum NetworkType { MAIN, TEST, REGTEST, SCALINGTEST } + +enum AddressType { PUBKEY_HASH, SCRIPT_HASH } + +enum KeyType { PUBLIC, PRIVATE } + +enum NetworkAddressType { MAIN_PKH, MAIN_P2SH, TEST_PKH, TEST_P2SH } + +/// Utility class used to inject strong typing into the [Address] class' representation of network and address types. +/// +/// NOTE: The network type and address type fields are overloaded in the way that wallet's interpret them. +/// This is not consensus-level stuff. Used only in Address representations by wallets. +/// +/// MAINNET +/// ------- +/// Address Header = 0; +/// P2SH Header = 5; +/// +/// TESTNET / REG_TESTNET / SCALING_TESTNET +/// ---------------------------------------- +/// Address Header = 111; +/// P2SH Header = 196; +class Networks { + /// Retrieve the list of network types corresponding to the version byte + /// + /// [version] - The version byte from the head of a serialized [Address] + /// + /// Returns a list of possible network types for the corresponding version byte + static List getNetworkTypes(int version) { + switch (version) { + case 0: + return [NetworkType.MAIN]; + case 111: + return [NetworkType.TEST, NetworkType.REGTEST, NetworkType.SCALINGTEST]; + case 5: + return [NetworkType.MAIN]; + case 196: + return [NetworkType.TEST, NetworkType.REGTEST, NetworkType.SCALINGTEST]; + + default: + throw new AddressFormatException( + '[$version] is not a valid network type.'); + break; + } + } + + /// Retrieve the address type corresponding to a specific version. + /// + /// [version] - The version byte from the head of a serialized [Address] + /// + /// Returns the address type corresponding to a specific [Address] version byte. + static AddressType getAddressType(int version) { + switch (version) { + case 0: + return AddressType.PUBKEY_HASH; + case 111: + return AddressType.PUBKEY_HASH; + case 5: + return AddressType.SCRIPT_HASH; + case 196: + return AddressType.SCRIPT_HASH; + + default: + throw new AddressFormatException( + '[$version] is not a valid address type.'); + break; + } + } + + /// This method retrieves the version byte corresponding to the NetworkAddressType + /// + /// [type] - The network address type + /// + /// Returns the version byte to prepend to a serialized [Address] + static int getNetworkVersion(NetworkAddressType type) { + switch (type) { + case NetworkAddressType.MAIN_P2SH: + return 5; + case NetworkAddressType.MAIN_PKH: + return 0; + case NetworkAddressType.TEST_P2SH: + return 196; + case NetworkAddressType.TEST_PKH: + return 111; + default: + return 0; + } + } + + /// Given an address' version byte this method retrieves the corresponding network type. + /// + /// [versionByte] - The version byte at the head of a wallet address + /// + /// Returns the network address type + static NetworkAddressType getNetworkAddressType(int versionByte) { + switch (versionByte) { + case 5: + return NetworkAddressType.MAIN_P2SH; + case 0: + return NetworkAddressType.MAIN_PKH; + case 196: + return NetworkAddressType.TEST_P2SH; + case 111: + return NetworkAddressType.TEST_PKH; + default: + return NetworkAddressType.MAIN_PKH; + } + } +} diff --git a/lib/src/privatekey.dart b/lib/src/privatekey.dart new file mode 100644 index 0000000..6c5230b --- /dev/null +++ b/lib/src/privatekey.dart @@ -0,0 +1,124 @@ +import 'package:bitbox/src/publickey.dart'; +import 'package:pointycastle/pointycastle.dart'; +import 'package:pointycastle/random/fortuna_random.dart'; +import 'dart:typed_data'; +import 'dart:math'; +import 'package:pointycastle/key_generators/ec_key_generator.dart'; +import 'package:pointycastle/ecc/curves/secp256k1.dart'; + +/// Manages an ECDSA private key. +/// +/// Bitcoin uses ECDSA for it's public/private key cryptography. +/// Specifically it uses the `secp256k1` elliptic curve. +/// +/// This class wraps cryptographic operations related to ECDSA from the +/// [PointyCastle](https://pub.dev/packages/pointycastle) library/package. +/// +/// You can read a good primer on Elliptic Curve Cryptography at [This Cloudflare blog post](https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/) +/// +/// +class BCHPrivateKey { + final _domainParams = ECDomainParameters('secp256k1'); + final _secureRandom = FortunaRandom(); + + var _hasCompressedPubKey = false; + var _networkType = 0; //Mainnet by default + + var random = Random.secure(); + + BigInt? _d; + late ECPrivateKey _ecPrivateKey; + BCHPublicKey? _bchPublicKey; + + /// Constructs a random private key. + /// + /// [networkType] - Optional network type. Defaults to mainnet. The network type is only + /// used when serialising the Private Key in *WIF* format. See [toWIF()]. + /// + BCHPrivateKey({networkType = 0}) { + var keyParams = ECKeyGeneratorParameters(ECCurve_secp256k1()); + _secureRandom.seed(KeyParameter(_seed())); + + var generator = ECKeyGenerator(); + generator.init(ParametersWithRandom(keyParams, _secureRandom)); + + var retry = + 100; //100 retries to get correct bitLength. Problem in PointyCastle lib ? + late AsymmetricKeyPair keypair; + while (retry > 0) { + keypair = generator.generateKeyPair(); + ECPrivateKey key = keypair.privateKey as ECPrivateKey; + if (key.d!.bitLength == 256) { + break; + } else { + retry--; + } + } + + _hasCompressedPubKey = true; + _networkType = networkType; + _ecPrivateKey = keypair.privateKey as ECPrivateKey; + _d = _ecPrivateKey.d; + + if (_d!.bitLength != 256) { + throw Exception( + "Failed to generate a valid private key after 100 tries. Try again. "); + } + + _bchPublicKey = BCHPublicKey.fromPrivateKey(this); + } + + /// Construct a Private Key from the hexadecimal value representing the + /// BigInt value of (d) in ` Q = d * G ` + /// + /// [privhex] - The BigInt representation of the private key as a hexadecimal string + /// + /// [networkType] - The network type we intend to use to corresponding WIF representation on. + BCHPrivateKey.fromHex(String privhex) { + var d = BigInt.parse(privhex, radix: 16); + + _hasCompressedPubKey = true; + _networkType = 0; + _ecPrivateKey = _privateKeyFromBigInt(d); + _d = d; + _bchPublicKey = BCHPublicKey.fromPrivateKey(this); + } + + /// Returns the *naked* private key Big Integer value as a hexadecimal string + String toHex() { + return _d!.toRadixString(16); + } + + Uint8List _seed() { + var random = Random.secure(); + var seed = List.generate(32, (_) => random.nextInt(256)); + return Uint8List.fromList(seed); + } + + ECPrivateKey _privateKeyFromBigInt(BigInt d) { + if (d == BigInt.zero) { + throw Exception( + 'Zero is a bad value for a private key. Pick something else.'); + } + + return ECPrivateKey(d, _domainParams); + } + + /// Returns the *naked* private key Big Integer value as a Big Integer + BigInt? get privateKey { + return _d; + } + + /// Returns the [BCHPublicKey] corresponding to this ECDSA private key. + /// + /// NOTE: `Q = d * G` where *Q* is the public key, *d* is the private key and `G` is the curve's Generator. + BCHPublicKey? get publicKey { + return _bchPublicKey; + } + + /// Returns true if the corresponding public key for this private key + /// is in *compressed* format. To read more about compressed public keys see [BCHPublicKey().getEncoded()] + bool get isCompressed { + return _hasCompressedPubKey; + } +} diff --git a/lib/src/publickey.dart b/lib/src/publickey.dart new file mode 100644 index 0000000..8892a9c --- /dev/null +++ b/lib/src/publickey.dart @@ -0,0 +1,311 @@ +import 'dart:typed_data'; +import 'package:bitbox/src/privatekey.dart'; +import 'package:hex/hex.dart'; +import 'package:pointycastle/pointycastle.dart'; + +/// Manages an ECDSA public key. +/// +/// Bitcoin uses ECDSA for it's public/private key cryptography. +/// Specifically it uses the `secp256k1` elliptic curve. +/// +/// This class wraps cryptographic operations related to ECDSA from the +/// [PointyCastle](https://pub.dev/packages/pointycastle) library/package. +/// +/// You can read a good primer on Elliptic Curve Cryptography at [This Cloudflare blog post](https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/) +/// +/// +class BCHPublicKey { + //We only deal with secp256k1 + final _domainParams = ECDomainParameters('secp256k1'); + + ECPoint? _point; + + /// Creates a public key from it's corresponding ECDSA private key. + /// + /// NOTE: public key *Q* is computed as `Q = d * G` where *d* is the private key + /// and *G* is the elliptic curve Generator. + /// + /// [privkey] - The private key who's *d*-value we will use. + BCHPublicKey.fromPrivateKey(BCHPrivateKey privkey) { + var decodedPrivKey = encodeBigInt(privkey.privateKey!); + var hexPrivKey = HEX.encode(decodedPrivKey); + + var actualKey = hexPrivKey; + var point = (_domainParams.G * BigInt.parse(actualKey, radix: 16))!; + if (point.x == null && point.y == null) { + throw Exception( + 'Cannot generate point from private key. Private key greater than N ?'); + } + + //create a point taking into account compression request/indicator of parent private key + var finalPoint = _domainParams.curve.createPoint( + point.x!.toBigInteger()!, point.y!.toBigInteger()!, privkey.isCompressed); + + _checkIfOnCurve(finalPoint); // a bit paranoid + + _point = finalPoint; +// _publicKey = ECPublicKey((_point), _domainParams); + } + + /// Creates a public key instance from the ECDSA public key's `x-coordinate` + /// + /// ECDSA has some cool properties. Because we are dealing with an elliptic curve in a plane, + /// the public key *Q* has (x,y) cartesian coordinates. + /// It is possible to reconstruct the full public key from only it's `x-coordinate` + /// *IFF* one knows whether the Y-Value is *odd* or *even*. + /// + /// [xValue] - The Big Integer value of the `x-coordinate` in hexadecimal format + /// + /// [oddYValue] - *true* if the corresponding `y-coordinate` is even, *false* otherwise + BCHPublicKey.fromX(String xValue, bool oddYValue) { + _point = _getPointFromX(xValue, oddYValue); +// _publicKey = ECPublicKey((_point), _domainParams); + } + + /// Creates a public key from it's known *(x,y)* coordinates. + /// + /// [x] - X coordinate of the public key + /// + /// [y] - Y coordinate of the public key + /// + /// [compressed] = Specifies whether we will render this point in it's + /// compressed form by default with [toString()]. See [getEncoded()] to + /// learn more about compressed public keys. + BCHPublicKey.fromXY(BigInt x, BigInt y, {bool compressed = true}) { + //create a compressed point by default + var point = _domainParams.curve.createPoint(x, y, compressed); + + _checkIfOnCurve(point); + + _point = point; + +// _publicKey = ECPublicKey(_point, _domainParams); + } + + /// Reconstructs a public key from a DER-encoding. + /// + /// [buffer] - Byte array containing public key in DER format. + /// + /// [strict] - If *true* then we enforce strict DER encoding rules. Defaults to *true*. + BCHPublicKey.fromDER(List buffer, {bool strict = true}) { + if (buffer.isEmpty) { + throw Exception('Empty compressed DER buffer'); + } + + _point = _transformDER(buffer, strict); + + if (_point!.isInfinity) { + throw Exception('That public key generates point at infinity'); + } + + if (_point!.y!.toBigInteger() == BigInt.zero) { + throw Exception('Invalid Y value for this public key'); + } + + _checkIfOnCurve(_point!); + +// _publicKey = ECPublicKey(_point, _domainParams); + } + + /// Reconstruct a public key from the hexadecimal format of it's DER-encoding. + /// + /// [pubkey] - The DER-encoded public key as a hexadecimal string + /// + /// [strict] - If *true* then we enforce strict DER encoding rules. Defaults to *true*. + BCHPublicKey.fromHex(String pubkey, {bool strict = true}) { + if (pubkey.trim() == '') { + throw Exception('Empty compressed public key string'); + } + +// _parseHexString(pubkey); + _point = _transformDER(HEX.decode(pubkey), strict); + + if (_point!.isInfinity) { + throw Exception('That public key generates point at infinity'); + } + + if (_point!.y!.toBigInteger() == BigInt.zero) { + throw Exception('Invalid Y value for this public key'); + } + + _checkIfOnCurve(_point!); + +// _publicKey = ECPublicKey(_point, _domainParams); + } + + /// Validates that the DER-encoded hexadecimal string contains a valid + /// public key. + /// + /// [pubkey] - The DER-encoded public key as a hexadecimal string + /// + /// Returns *true* if the public key is valid, *false* otherwise. + static bool isValid(String pubkey) { + try { + BCHPublicKey.fromHex(pubkey); + } catch (err) { + return false; + } + + return true; + } + + /// Returns the *naked* public key value as either an (x,y) coordinate + /// or in a compact format using elliptic-curve point-compression. + /// + /// With EC point compression it is possible to reduce by half the + /// space occupied by a point, by taking advantage of a EC-curve property. + /// Specifically it is possible to recover the `y-coordinate` *IFF* the + /// `x-coordinate` is known *AND* we know whether the `y-coordinate` is + /// *odd* or *even*. + /// + /// [compressed] - If *true* the 'naked' public key value is returned in + /// compact format where the first byte is either 'odd' or 'even' followed + /// by the `x-coordinate`. If *false*, the full *(x,y)* coordinate pair will + /// be returned. + /// + /// NOTE: The first byte will contain either an odd number or an even number, + /// but this number is *NOT* a boolean flag. + String getEncoded(bool compressed) { + return HEX.encode(_point!.getEncoded(compressed)); + } + + /// Returns the 'naked' public key value. Point compression is determined by + /// the default parameter in the constructor. If you want to enforce a specific preference + /// for the encoding, you can use the [getEncoded()] function instead. + @override + String toString() { + if (_point == null) { + return ''; + } + + return HEX.encode(_point!.getEncoded(_point!.isCompressed)); + } + + /// Alias for the [toString()] method. + String toHex() => toString(); + + ECPoint _transformDER(List buf, bool strict) { + BigInt x; + BigInt y; + List xbuf; + List ybuf; + ECPoint point; + + if (buf[0] == 0x04 || (!strict && (buf[0] == 0x06 || buf[0] == 0x07))) { + xbuf = buf.sublist(1, 33); + ybuf = buf.sublist(33, 65); + if (xbuf.length != 32 || ybuf.length != 32 || buf.length != 65) { + throw Exception('Length of x and y must be 32 bytes'); + } + x = BigInt.parse(HEX.encode(xbuf), radix: 16); + y = BigInt.parse(HEX.encode(ybuf), radix: 16); + + point = _domainParams.curve.createPoint(x, y); + } else if (buf[0] == 0x03 || buf[0] == 0x02) { + xbuf = buf.sublist(1); + x = BigInt.parse(HEX.encode(xbuf), radix: 16); + + var yTilde = buf[0] & 1; + point = _domainParams.curve.decompressPoint(yTilde, x); + } else { + throw Exception('Invalid DER format public key'); + } + return point; + } + + ECPoint _getPointFromX(String xValue, bool oddYValue) { + var prefixByte; + if (oddYValue) { + prefixByte = 0x03; + } else { + prefixByte = 0x02; + } + + var encoded = HEX.decode(xValue); + + var addressBytes = List.filled(1 + encoded.length, 0); + addressBytes[0] = prefixByte; + addressBytes.setRange(1, addressBytes.length, encoded); + + return _decodePoint(HEX.encode(addressBytes)); + } + + ECPoint _decodePoint(String pkHex) { + if (pkHex.trim() == '') { + throw Exception('Empty compressed public key string'); + } + + var encoded = HEX.decode(pkHex); + try { + var point = _domainParams.curve.decodePoint(encoded)!; + + if (point.isCompressed && encoded.length != 33) { + throw Exception( + "Compressed public keys must be 33 bytes long. Yours is [${encoded.length}]"); + } else if (!point.isCompressed && encoded.length != 65) { + throw Exception( + "Uncompressed public keys must be 65 bytes long. Yours is [${encoded.length}]"); + } + + _checkIfOnCurve(point); + + return point; + } on ArgumentError catch (err) { + throw Exception(err.message); + } + } + + /// Encode a BigInt into bytes using big-endian encoding. + Uint8List encodeBigInt(BigInt number) { + var _byteMask = BigInt.from(0xff); + int size = (number.bitLength + 7) >> 3; + + var result = Uint8List(size); + for (int i = 0; i < size; i++) { + result[size - i - 1] = (number & _byteMask).toInt(); + number = number >> 8; + } + + return result; + } + + String _compressPoint(ECPoint point) { + return HEX.encode(point.getEncoded(true)); + } + + bool _checkIfOnCurve(ECPoint point) { + //a bit of math copied from PointyCastle. ecc/ecc_fp.dart -> decompressPoint() + var x = _domainParams.curve.fromBigInteger(point.x!.toBigInteger()!); + var alpha = (x * ((x * x) + _domainParams.curve.a!)) + _domainParams.curve.b!; + ECFieldElement? beta = alpha.sqrt(); + + if (beta == null) { + throw Exception('This point is not on the curve'); + } + + //slight-of-hand. Create compressed point, reconstruct and check Y value. + var compressedPoint = _compressPoint(point); + var checkPoint = + _domainParams.curve.decodePoint(HEX.decode(compressedPoint))!; + if (checkPoint.y!.toBigInteger() != point.y!.toBigInteger()) { + throw Exception('This point is not on the curve'); + } + + return (point.x!.toBigInteger() == BigInt.zero) && + (point.y!.toBigInteger() == BigInt.zero); + } + + /// Returns the (x,y) coordinates of this public key as an [ECPoint]. + /// The author dislikes leaking the wrapped PointyCastle implementation, but is too + /// lazy to write his own Point implementation. + ECPoint? get point { + return _point; + } + + /// Returns *true* if this public key will render using EC point compression by + /// default when one calls the [toString()] or [toHex()] methods. + /// Returns *false* otherwise. + bool get isCompressed { + return _point!.isCompressed; + } +} diff --git a/lib/src/rawtransactions.dart b/lib/src/rawtransactions.dart index a45a617..0c5826a 100644 --- a/lib/src/rawtransactions.dart +++ b/lib/src/rawtransactions.dart @@ -1,9 +1,54 @@ +import 'dart:async'; +import 'dart:convert'; import 'utils/rest_api.dart'; +import 'package:http/http.dart' as http; /// Utilities for working raw transactions class RawTransactions { /// Send raw transaction to the network /// Returns the resulting txid - static Future sendRawTransaction(String rawTx) async => - await RestApi.sendGetRequest("rawtransactions/sendRawTransaction", rawTx); -} \ No newline at end of file + static Future sendRawTransaction(String? rawTx) async => + await (RestApi.sendGetRequest("rawtransactions/sendRawTransaction", rawTx) as FutureOr); + + /// Send multiple raw transactions to the network + /// Returns the resulting array of txids + static Future sendRawTransactions(List rawTxs) async => + await (RestApi.sendPostRequest( + "rawtransactions/sendRawTransaction", "hexes", rawTxs) as FutureOr?>); + + /// Returns a JSON object representing the serialized, hex-encoded transaction + static Future decodeRawTransaction(String hex) async => + await (RestApi.sendGetRequest("rawtransactions/decodeRawTransaction", hex) as FutureOr?>); + + /// Returns bulk hex encoded transaction + static Future decodeRawTransactions(List hexes) async => + await (RestApi.sendPostRequest( + "rawtransactions/decodeRawTransaction", "hexes", hexes) as FutureOr?>); + + /// Decodes a hex-encoded script + static Future decodeScript(String script) async => + await (RestApi.sendGetRequest("rawtransactions/decodeScript", script) as FutureOr?>); + + /// Decodes multiple hex-encoded scripts + static Future decodeScripts(List scripts) async => + await (RestApi.sendPostRequest( + "rawtransactions/decodeScript", "hexes", scripts) as FutureOr?>); + + /// Returns the raw transaction data + static Future getRawtransaction(String txid, {bool verbose = true}) async { + final response = await http.get(Uri.parse( + "https://rest1.biggestfan.net/v2/rawtransactions/getRawTransaction/$txid?verbose=$verbose")); + return jsonDecode(response.body); + } + + /// Returns raw transaction data for multiple transactions + static Future getRawtransactions(List txids, + {bool verbose = true}) async { + final response = await http.post( + Uri.parse( + "https://rest1.biggestfan.net/v2/rawtransactions/getRawTransaction"), + headers: {"content-type": "application/json"}, + body: jsonEncode({'txids': txids, "verbose": verbose})); + return jsonDecode(response.body); + } +} diff --git a/lib/src/slp.dart b/lib/src/slp.dart new file mode 100644 index 0000000..5a1a913 --- /dev/null +++ b/lib/src/slp.dart @@ -0,0 +1,511 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:bitbox/bitbox.dart'; +import 'package:hex/hex.dart'; +import 'package:slp_mdm/slp_mdm.dart'; +import 'package:slp_parser/slp_parser.dart'; +import 'dart:math' as math; + +class SLP { + static getTokenInformation(String tokenID, + [bool decimalConversion = false]) async { + var res; + try { + res = await RawTransactions.getRawtransaction(tokenID, verbose: true); + if (res.containsKey('error')) { + throw Exception( + "BITBOX response error for 'RawTransactions.getRawTransaction'"); + } + } catch (e) { + throw Exception(e); + } + var slpMsg = + parseSLP(HEX.decode(res['vout'][0]['scriptPubKey']['hex'])).toMap(); + if (decimalConversion) { + Map? slpMsgData = slpMsg['data'] as Map?; + + if (slpMsg['transactionType'] == "GENESIS" || + slpMsg['transactionType'] == "MINT") { + slpMsgData!['qty'] = + slpMsgData['qty'] / math.pow(10, slpMsgData['decimals']); + } else { + slpMsgData!['amounts'] + .map((o) => o / math.pow(10, slpMsgData['decimals'])); + } + } + + if (slpMsg['transactionType'] == "GENESIS") { + slpMsg['tokenIdHex'] = tokenID; + } + return slpMsg; + } + + static getAllSlpBalancesAndUtxos(String address) async { + List utxos = await (mapToSlpAddressUtxoResultArray(address) + as FutureOr>); + var txIds = []; + utxos.forEach((i) { + txIds.add(i['txid']); + }); + if (txIds.length == 0) { + return []; + } + } + + static Future mapToSlpAddressUtxoResultArray(String address) async { + var result; + try { + result = await Address.utxo([address]); + } catch (e) { + return []; + } + List utxo = []; + return result['utxos'].forEach((txo) => utxo.add({ + 'satoshis': txo.satoshis, + 'txid': txo.txid, + 'amount': txo.amount, + 'confirmations': txo.confirmations, + 'height': txo.height, + 'vout': txo.vout, + 'cashAddress': result.cashAddress, + 'legacyAddress': result.legacyAddress, + 'slpAddress': Address.toSLPAddress(result.legacyAddress), + 'scriptPubKey': result.scriptPubKey, + })); + } + + static mapToSLPUtxoArray({required List utxos, String? xpriv, String? wif}) { + List utxo = []; + utxos.forEach((txo) => utxo.add({ + 'satoshis': new BigInt.from(txo['satoshis']), + 'xpriv': xpriv, + 'wif': wif, + 'txid': txo['txid'], + 'vout': txo['vout'], + 'utxoType': txo['utxoType'], + 'transactionType': txo['transactionType'], + 'tokenId': txo['tokenId'], + 'tokenTicker': txo['tokenTicker'], + 'tokenName': txo['tokenName'], + 'decimals': txo['decimals'], + 'tokenType': txo['tokenType'], + 'tokenQty': txo['tokenQty'], + 'isValid': txo['isValid'], + })); + return utxo; + } + + static parseSlp(String scriptPubKey) { + var slpMsg = parseSLP(HEX.decode(scriptPubKey)); + return slpMsg.toMap(raw: true); + } + + static simpleTokenSend({ + String? tokenId, + List? sendAmounts, + List? inputUtxos, + List? bchInputUtxos, + List? tokenReceiverAddresses, + String? slpChangeReceiverAddress, + String? bchChangeReceiverAddress, + List? requiredNonTokenOutputs, + int? extraFee, + int extraBCH = 0, + int type = 0x01, + bool buildIncomplete = false, + int hashType = Transaction.SIGHASH_ALL, + }) async { + List amounts = []; + BigInt totalAmount = BigInt.from(0); + if (tokenId is! String) { + return Exception("Token id should be a String"); + } + tokenReceiverAddresses!.forEach((tokenReceiverAddress) { + if (tokenReceiverAddress is! String) { + throw new Exception("Token receiving address should be a String"); + } + }); + if (slpChangeReceiverAddress is! String) { + throw new Exception("Slp change receiving address should be a String"); + } + if (bchChangeReceiverAddress is! String) { + throw new Exception("Bch change receiving address should be a String"); + } + + var tokenInfo = await getTokenInformation(tokenId); + int? decimals = tokenInfo['data']['decimals']; + + sendAmounts!.forEach((sendAmount) async { + if (sendAmount > 0) { + totalAmount += BigInt.from(sendAmount * math.pow(10, decimals!)); + amounts.add(BigInt.from(sendAmount * math.pow(10, decimals))); + } + }); + + // 1 Set the token send amounts, send tokens to a + // new receiver and send token change back to the sender + BigInt totalTokenInputAmount = BigInt.from(0); + inputUtxos!.forEach((txo) => + totalTokenInputAmount += _preSendSlpJudgementCheck(txo, tokenId)); + + // 2 Compute the token Change amount. + BigInt tokenChangeAmount = totalTokenInputAmount - totalAmount; + bool sendChange = tokenChangeAmount > new BigInt.from(0); + + if (tokenChangeAmount < new BigInt.from(0)) { + return throw Exception('Token inputs less than the token outputs'); + } + if (tokenChangeAmount > BigInt.from(0)) { + amounts.add(tokenChangeAmount); + } + + if (sendChange) { + tokenReceiverAddresses.add(slpChangeReceiverAddress); + } + + int? tokenType = tokenInfo['tokenType']; + + // 3 Create the Send OP_RETURN message + var sendOpReturn; + if (tokenType == 1) { + sendOpReturn = Send(HEX.decode(tokenId), amounts); + } else if (tokenType == 129) { + sendOpReturn = Nft1GroupSend(HEX.decode(tokenId), amounts); + } else if (tokenType == 65) { + sendOpReturn = Nft1ChildSend(HEX.decode(tokenId), amounts[0]); + } else { + return throw Exception('Invalid token type'); + } + // 4 Create the raw Send transaction hex + Map result = await _buildRawSendTx( + slpSendOpReturn: sendOpReturn, + inputTokenUtxos: inputUtxos, + bchInputUtxos: bchInputUtxos!, + tokenReceiverAddresses: tokenReceiverAddresses, + bchChangeReceiverAddress: bchChangeReceiverAddress, + requiredNonTokenOutputs: requiredNonTokenOutputs, + extraFee: extraFee, + extraBCH: extraBCH, + buildIncomplete: buildIncomplete, + hashType: hashType); + return result; + } + + static BigInt _preSendSlpJudgementCheck(Map txo, tokenID) { + // if (txo['slpUtxoJudgement'] == "undefined" || + // txo['slpUtxoJudgement'] == null || + // txo['slpUtxoJudgement'] == "UNKNOWN") { + // throw Exception( + // "There is at least one input UTXO that does not have a proper SLP judgement"); + // } + // if (txo['slpUtxoJudgement'] == "UNSUPPORTED_TYPE") { + // throw Exception( + // "There is at least one input UTXO that is an Unsupported SLP type."); + // } + // if (txo['slpUtxoJudgement'] == "SLP_BATON") { + // throw Exception( + // "There is at least one input UTXO that is a baton. You can only spend batons in a MINT transaction."); + // } + //if (txo.containsKey('slpTransactionDetails')) { + //if (txo['slpUtxoJudgement'] == "SLP_TOKEN") { + if (txo['utxoType'] == "token") { + //if (txo['transactionType'] != 'send') { + // throw Exception( + // "There is at least one input UTXO that does not have a proper SLP judgement"); + // } + //if (!txo.containsKey('slpUtxoJudgementAmount')) { + if (!txo.containsKey('tokenQty')) { + throw Exception( + "There is at least one input token that does not have the 'slpUtxoJudgementAmount' property set."); + } + //if (txo['slpTransactionDetails']['tokenIdHex'] != tokenID) { + if (txo['tokenId'] != tokenID) { + throw Exception( + "There is at least one input UTXO that is a different SLP token than the one specified."); + } + // if (txo['slpTransactionDetails']['tokenIdHex'] == tokenID) { + if (txo['tokenId'] == tokenID) { + //return BigInt.from(num.parse(txo['slpUtxoJudgementAmount'])); + return BigInt.from(txo['tokenQty'] * math.pow(10, txo['decimals'])); + } + } + // } + return BigInt.from(0); + } + + static _buildRawSendTx( + {required List slpSendOpReturn, + required List inputTokenUtxos, + required List bchInputUtxos, + required List tokenReceiverAddresses, + String? bchChangeReceiverAddress, + List? requiredNonTokenOutputs, + int? extraBCH, + int? extraFee, + bool? buildIncomplete, + int? hashType}) async { + // Check proper address formats are given + tokenReceiverAddresses.forEach((addr) { + if (!addr.startsWith('simpleledger:')) { + throw new Exception("Token receiver address not in SlpAddr format."); + } + }); + + if (bchChangeReceiverAddress != null) { + if (!bchChangeReceiverAddress.startsWith('bitcoincash:')) { + throw new Exception( + "BCH change receiver address is not in CashAddr format."); + } + } + + // Parse the SLP SEND OP_RETURN message + var sendMsg = parseSLP(slpSendOpReturn).toMap(); + Map sendMsgData = sendMsg['data'] as Map; + + // Make sure we're not spending inputs from any other token or baton + var tokenInputQty = new BigInt.from(0); + inputTokenUtxos.forEach((txo) { + //if (txo['slpUtxoJudgement'] == "NOT_SLP") { + if (txo['isValid'] == null) { + return; + } + if (!txo['isValid']) { + return; + } + //if (txo['slpUtxoJudgement'] == "SLP_TOKEN") { + if (txo['utxoType'] == "token") { + // if (txo['slpTransactionDetails']['tokenIdHex'] != + // sendMsgData['tokenId']) { + if (txo['tokenId'] != sendMsgData['tokenId']) { + throw Exception("Input UTXOs included a token for another tokenId."); + } + // tokenInputQty += + // BigInt.from(double.parse(txo['slpUtxoJudgementAmount'])); + tokenInputQty += + BigInt.from(txo['tokenQty'] * math.pow(10, txo['decimals'])); + return; + } + // if (txo['slpUtxoJudgement'] == "SLP_BATON") { + // throw Exception("Cannot spend a minting baton."); + // } + // if (txo['slpUtxoJudgement'] == ['INVALID_TOKEN_DAG'] || + // txo['slpUtxoJudgement'] == "INVALID_BATON_DAG") { + // throw Exception("Cannot currently spend UTXOs with invalid DAGs."); + // } + throw Exception("Cannot spend utxo with no SLP judgement."); + }); + + // Make sure the number of output receivers + // matches the outputs in the OP_RETURN message. + if (tokenReceiverAddresses.length != sendMsgData['amounts'].length) { + throw Exception( + "Number of token receivers in config does not match the OP_RETURN outputs"); + } + + // Make sure token inputs == token outputs + var outputTokenQty = BigInt.from(0); + sendMsgData['amounts'].forEach((a) => outputTokenQty += a); + if (tokenInputQty != outputTokenQty) { + throw Exception("Token input quantity does not match token outputs."); + } + + // Create a transaction builder + var transactionBuilder = Bitbox.transactionBuilder(); + // let sequence = 0xffffffff - 1; + + // Calculate the total input amount & add all inputs to the transaction + var inputSatoshis = BigInt.from(0); + inputTokenUtxos.forEach((i) { + inputSatoshis += i['satoshis']; + transactionBuilder.addInput(i['txid'], i['vout']); + }); + + // Calculate the total BCH input amount & add all inputs to the transaction + bchInputUtxos.forEach((i) { + inputSatoshis += BigInt.from(i['satoshis']); + transactionBuilder.addInput(i['txid'], i['vout']); + }); + + // Start adding outputs to transaction + // Add SLP SEND OP_RETURN message + transactionBuilder.addOutput(compile(slpSendOpReturn), 0); + + // Add dust outputs associated with tokens + tokenReceiverAddresses.forEach((outputAddress) { + outputAddress = Address.toLegacyAddress(outputAddress); + outputAddress = Address.toCashAddress(outputAddress); + transactionBuilder.addOutput(outputAddress, 546); + }); + + // Calculate the amount of outputs set aside for special BCH-only outputs for fee calculation + var bchOnlyCount = + requiredNonTokenOutputs != null ? requiredNonTokenOutputs.length : 0; + BigInt bchOnlyOutputSatoshis = BigInt.from(0); + requiredNonTokenOutputs != null + ? requiredNonTokenOutputs + .forEach((o) => bchOnlyOutputSatoshis += BigInt.from(o['satoshis'])) + : bchOnlyOutputSatoshis = bchOnlyOutputSatoshis; + + // Add BCH-only outputs + if (requiredNonTokenOutputs != null) { + if (requiredNonTokenOutputs.length > 0) { + requiredNonTokenOutputs.forEach((output) { + transactionBuilder.addOutput( + output['bchaddress'], output['satoshis']); + }); + } + } + + // Calculate mining fee cost + int sendCost = _calculateSendCost( + slpSendOpReturn.length, + inputTokenUtxos.length + bchInputUtxos.length, + tokenReceiverAddresses.length + bchOnlyCount, + bchChangeAddress: bchChangeReceiverAddress, + feeRate: extraFee ?? 1); + + // Compute BCH change amount + BigInt bchChangeAfterFeeSatoshis = + inputSatoshis - BigInt.from(sendCost) - bchOnlyOutputSatoshis; + if (bchChangeAfterFeeSatoshis < BigInt.from(0)) { + return {'hex': null, 'fee': "Insufficient fees"}; + } + + // Add change, if any + if (bchChangeAfterFeeSatoshis + new BigInt.from(extraBCH!) > + new BigInt.from(546)) { + transactionBuilder.addOutput(bchChangeReceiverAddress, + bchChangeAfterFeeSatoshis.toInt() + extraBCH); + } + + if (hashType == null) hashType = Transaction.SIGHASH_ALL; + // Sign txn and add sig to p2pkh input with xpriv, + int slpIndex = 0; + inputTokenUtxos.forEach((i) { + ECPair paymentKeyPair; + String? xpriv = i['xpriv']; + String? wif = i['wif']; + if (xpriv != null) { + paymentKeyPair = HDNode.fromXPriv(xpriv).keyPair; + } else { + paymentKeyPair = ECPair.fromWIF(wif!); + } + + transactionBuilder.sign( + slpIndex, paymentKeyPair, i['satoshis'].toInt(), hashType!); + slpIndex++; + }); + + int bchIndex = inputTokenUtxos.length; + bchInputUtxos.forEach((i) { + ECPair paymentKeyPair; + String? xpriv = i['xpriv']; + String? wif = i['wif']; + if (xpriv != null) { + paymentKeyPair = HDNode.fromXPriv(xpriv).keyPair; + } else { + paymentKeyPair = ECPair.fromWIF(wif!); + } + transactionBuilder.sign( + bchIndex, paymentKeyPair, i['satoshis'].toInt(), hashType!); + bchIndex++; + }); + + int _extraFee = (tokenReceiverAddresses.length + bchOnlyCount) * 546; + + // Build the transaction to hex and return + // warn user if the transaction was not fully signed + String hex; + if (buildIncomplete!) { + hex = transactionBuilder.buildIncomplete().toHex(); + return {'hex': hex, 'fee': sendCost - _extraFee}; + } else { + hex = transactionBuilder.build().toHex(); + } + + // Check For Low Fee + int outValue = 0; + transactionBuilder.tx.outputs.forEach((o) => outValue += o.value ?? 0); + int inValue = 0; + inputTokenUtxos + .forEach((i) => inValue += int.parse(i['satoshis'].toString())); + bchInputUtxos + .forEach((i) => inValue += int.parse(i['satoshis'].toString())); + if (inValue - outValue < hex.length / 2) { + return {'hex': null, 'fee': "Insufficient fee"}; + } + + return {'hex': hex, 'fee': sendCost - _extraFee}; + } + + static int _calculateSendCost( + int sendOpReturnLength, int inputUtxoSize, int outputs, + {String? bchChangeAddress, int feeRate = 1, bool forTokens = true}) { + int nonfeeoutputs = 0; + if (forTokens) { + nonfeeoutputs = outputs * 546; + } + if (bchChangeAddress != null && bchChangeAddress != 'undefined') { + outputs += 1; + } + + int fee = BitcoinCash.getByteCount(inputUtxoSize, outputs); + fee += sendOpReturnLength; + fee += 10; // added to account for OP_RETURN ammount of 0000000000000000 + fee *= feeRate; + //print("SEND cost before outputs: " + fee.toString()); + fee += nonfeeoutputs; + //print("SEND cost after outputs are added: " + fee.toString()); + return fee; + } + + /* + Todo + */ + + _createSimpleToken( + {required String tokenName, + required String tokenTicker, + int? tokenAmount, + required String documentUri, + required Uint8List documentHash, + int? decimals, + String? tokenReceiverAddress, + required String batonReceiverAddress, + String? bchChangeReceiverAddress, + List? inputUtxos, + int type = 0x01}) async { + int batonVout = batonReceiverAddress.isNotEmpty ? 2 : 0; + if (decimals == null) { + throw Exception("Decimals property must be in range 0 to 9"); + } + if (tokenTicker != null && tokenTicker is! String) { + throw Exception("ticker must be a string"); + } + if (tokenName != null && tokenName is! String) { + throw Exception("name must be a string"); + } + + var genesisOpReturn = Genesis(tokenTicker, tokenName, documentUri, + documentHash, decimals, BigInt.from(batonVout), tokenAmount); + if (genesisOpReturn.length > 223) { + throw Exception( + "Script too long, must be less than or equal to 223 bytes."); + } + return genesisOpReturn; + + // var genesisTxHex = buildRawGenesisTx({ + // slpGenesisOpReturn: genesisOpReturn, + // mintReceiverAddress: tokenReceiverAddress, + // batonReceiverAddress: batonReceiverAddress, + // bchChangeReceiverAddress: bchChangeReceiverAddress, + // input_utxos: Utils.mapToUtxoArray(inputUtxos) + // }); + + // return await RawTransactions.sendRawTransaction(genesisTxHex); + } + + //simpleTokenMint() {} + + //simpleTokenBurn() {} +} diff --git a/lib/src/transaction.dart b/lib/src/transaction.dart index 23df692..d9a0b18 100644 --- a/lib/src/transaction.dart +++ b/lib/src/transaction.dart @@ -4,7 +4,6 @@ import 'utils/p2pkh.dart' show P2PKH, P2PKHData; import 'crypto/crypto.dart' as bcrypto; import 'utils/script.dart' as bscript; import 'utils/opcodes.dart'; -import 'hdnode.dart'; import 'utils/check_types.dart'; import 'varuint.dart' as varuint; @@ -21,11 +20,14 @@ class Transaction { static const SIGHASH_BITCOINCASHBIP143 = 0x40; static const ADVANCED_TRANSACTION_MARKER = 0x00; static const ADVANCED_TRANSACTION_FLAG = 0x01; - static final EMPTY_SCRIPT = Uint8List.fromList([]); - static final ZERO = HEX.decode('0000000000000000000000000000000000000000000000000000000000000000'); - static final ONE = HEX.decode('0000000000000000000000000000000000000000000000000000000000000001'); - static final VALUE_UINT64_MAX = HEX.decode('ffffffffffffffff'); - static final BLANK_OUTPUT = Output(script: EMPTY_SCRIPT, valueBuffer: VALUE_UINT64_MAX); + static final emptyScript = Uint8List.fromList([]); + static final zero = HEX.decode( + '0000000000000000000000000000000000000000000000000000000000000000'); + static final one = HEX.decode( + '0000000000000000000000000000000000000000000000000000000000000001'); + static final valueUint64Max = HEX.decode('ffffffffffffffff'); + static final blankOutput = + Output(script: emptyScript, valueBuffer: valueUint64Max as Uint8List?); static const SATOSHI_MAX = 21 * 1e14; int version; @@ -34,15 +36,15 @@ class Transaction { List outputs; /// If [inputs] or [outputs] are not defined, empty lists are created for each - Transaction([version = 2, locktime = 0, ins, outs]) : - version = version, - locktime = locktime, - inputs = ins ?? [], - outputs = outs ?? []; + Transaction([version = 2, locktime = 0, ins, outs]) + : version = version, + locktime = locktime, + inputs = ins ?? [], + outputs = outs ?? []; /// Creates transaction from its hex representation factory Transaction.fromHex(String hex) { - return Transaction.fromBuffer(HEX.decode(hex)); + return Transaction.fromBuffer(HEX.decode(hex) as Uint8List); } /// Creates transaction from its hex representation stored in list of integers @@ -50,7 +52,7 @@ class Transaction { var offset = 0; ByteData bytes = buffer.buffer.asByteData(); Uint8List readSlice(n) { - offset += n; + offset += n as int; return buffer.sublist(offset - n, offset); } @@ -87,10 +89,10 @@ class Transaction { final vinLen = readVarInt(); for (var i = 0; i < vinLen; ++i) { tx.inputs.add(new Input( - hash: readSlice(32), - index: readUInt32(), - script: readVarSlice(), - sequence: readUInt32())); + hash: readSlice(32), + index: readUInt32(), + script: readVarSlice(), + sequence: readUInt32())); } final voutLen = readVarInt(); for (var i = 0; i < voutLen; ++i) { @@ -103,7 +105,7 @@ class Transaction { } bool isCoinbaseHash(buffer) { - assert (buffer.length == 32); + assert(buffer.length == 32); for (var i = 0; i < 32; ++i) { if (buffer[i] != 0) return false; @@ -116,30 +118,31 @@ class Transaction { } /// Add input to the transaction. If [sequence] is not provided, defaults to [DEFAULT_SEQUENCE] - int addInput(Uint8List hash, int index, [int sequence, Uint8List scriptSig]) { + int addInput(Uint8List hash, int? index, [int? sequence, Uint8List? scriptSig]) { inputs.add(new Input( - hash: hash, - index: index, - sequence: sequence ?? DEFAULT_SEQUENCE, - script: scriptSig ?? EMPTY_SCRIPT)); + hash: hash, + index: index, + sequence: sequence ?? DEFAULT_SEQUENCE, + script: scriptSig ?? emptyScript)); return inputs.length - 1; } /// Add input to the transaction - int addOutput(Uint8List scriptPubKey, int value) { + int addOutput(Uint8List? scriptPubKey, int? value) { outputs.add(new Output(script: scriptPubKey, value: value)); return outputs.length - 1; } - setInputScript(int index, Uint8List scriptSig) { + setInputScript(int index, Uint8List? scriptSig) { inputs[index].script = scriptSig; } /// Create hash for legacy signature - hashForSignature(int inIndex, Uint8List prevOutScript, int hashType) { - if (inIndex >= inputs.length) return ONE; + hashForSignature(int inIndex, Uint8List? prevOutScript, int hashType) { + if (inIndex >= inputs.length) return one; // ignore OP_CODESEPARATOR - final ourScript = bscript.compile(bscript.decompile(prevOutScript).where((x) { + final ourScript = + bscript.compile(bscript.decompile(prevOutScript!)!.where((x) { return x != Opcodes.OP_CODESEPARATOR; }).toList()); final txTmp = Transaction.clone(this); @@ -156,14 +159,14 @@ class Transaction { // SIGHASH_SINGLE: ignore all outputs, except at the same index? } else if ((hashType & 0x1f) == SIGHASH_SINGLE) { // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60 - if (inIndex >= outputs.length) return ONE; + if (inIndex >= outputs.length) return one; // truncate outputs after txTmp.outputs.length = inIndex + 1; // "blank" outputs before for (var i = 0; i < inIndex; i++) { - txTmp.outputs[i] = BLANK_OUTPUT; + txTmp.outputs[i] = blankOutput; } // ignore sequence numbers (except at inIndex) for (var i = 0; i < txTmp.inputs.length; i++) { @@ -178,13 +181,15 @@ class Transaction { /// legacy signature /// /// [amount] must not be null for BCH signatures - hashForCashSignature(int inIndex, Uint8List prevOutScript, int amount, int hashType) { + hashForCashSignature( + int inIndex, Uint8List? prevOutScript, int? amount, int hashType) { if ((hashType & SIGHASH_BITCOINCASHBIP143) > 0) { if (amount == null) { - throw ArgumentError('Bitcoin Cash sighash requires value of input to be signed.'); + throw ArgumentError( + 'Bitcoin Cash sighash requires value of input to be signed.'); } - return _hashForWitnessV0(inIndex, prevOutScript, amount, hashType); + return _hashForWitnessV0(inIndex, prevOutScript!, amount, hashType); } else { return hashForSignature(inIndex, prevOutScript, hashType); } @@ -192,13 +197,14 @@ class Transaction { int virtualSize() { return 8 + - varuint.encodingLength(inputs.length) + - varuint.encodingLength(outputs.length) + - inputs.fold(0, (sum, input) => sum + 40 + _varSliceSize(input.script)) + - outputs.fold(0, (sum, output) => sum + 8 + _varSliceSize(output.script)); + varuint.encodingLength(inputs.length) + + varuint.encodingLength(outputs.length) + + inputs.fold(0, (sum, input) => sum + 40 + _varSliceSize(input.script!)) + + outputs.fold( + 0, (sum, output) => sum + 8 + _varSliceSize(output.script!)) as int; } - Uint8List toBuffer([Uint8List buffer, int initialOffset]) { + Uint8List toBuffer([Uint8List? buffer, int? initialOffset]) { return this._toBuffer(buffer, initialOffset); } @@ -207,35 +213,36 @@ class Transaction { } Uint8List getHash() { - return bcrypto.hash256(_toBuffer()); + return bcrypto.Crypto.hash256(_toBuffer()); } String getId() { return HEX.encode(getHash().reversed.toList()); } - _hashForWitnessV0(int inIndex, Uint8List prevOutScript, int amount, int hashType) { - Uint8List tBuffer; - int tOffset; + _hashForWitnessV0( + int inIndex, Uint8List prevOutScript, int amount, int hashType) { + Uint8List? tBuffer; + int? tOffset; void writeSlice(Uint8List slice) { - tBuffer.setRange(tOffset, slice.length + tOffset, slice); - tOffset += slice.length; - }; + tBuffer!.setRange(tOffset!, slice.length + tOffset!, slice); + tOffset = tOffset! + slice.length; + } void writeUint32(int i) { - tBuffer.buffer.asByteData().setUint32(tOffset, i, Endian.little); - tOffset += 4; + tBuffer!.buffer.asByteData().setUint32(tOffset!, i, Endian.little); + tOffset = tOffset! + 4; } void writeUint64(int i) { - tBuffer.buffer.asByteData().setUint64(tOffset, i, Endian.little); - tOffset += 8; + tBuffer!.buffer.asByteData().setUint64(tOffset!, i, Endian.little); + tOffset = tOffset! + 8; } void writeVarInt(int i) { varuint.encode(i, tBuffer, tOffset); - tOffset += varuint.encodingLength(i); + tOffset = tOffset! + varuint.encodingLength(i); } writeVarSlice(slice) { @@ -243,58 +250,60 @@ class Transaction { writeSlice(slice); } - Uint8List hashPrevoutputs; - Uint8List hashSequence; - Uint8List hashOutputs; + Uint8List hashPrevoutputs = zero as Uint8List; + Uint8List hashSequence = zero as Uint8List; + Uint8List hashOutputs = zero as Uint8List; if ((hashType & SIGHASH_ANYONECANPAY == 0)) { tBuffer = Uint8List(36 * this.inputs.length); tOffset = 0; this.inputs.forEach((txInput) { - writeSlice(txInput.hash); - writeUint32(txInput.index); + writeSlice(txInput.hash!); + writeUint32(txInput.index!); }); - hashPrevoutputs = bcrypto.hash256(tBuffer); + hashPrevoutputs = bcrypto.Crypto.hash256(tBuffer); } if ((hashType & SIGHASH_ANYONECANPAY) == 0 && - (hashType & 0x1f) != SIGHASH_SINGLE && - (hashType & 0x1f) != SIGHASH_NONE) { + (hashType & 0x1f) != SIGHASH_SINGLE && + (hashType & 0x1f) != SIGHASH_NONE) { tBuffer = Uint8List(4 * this.inputs.length); tOffset = 0; this.inputs.forEach((txInput) { - writeUint32(txInput.sequence); + writeUint32(txInput.sequence!); }); - hashSequence = bcrypto.hash256(tBuffer); + hashSequence = bcrypto.Crypto.hash256(tBuffer); } - if ((hashType & 0x1f) != SIGHASH_SINGLE && (hashType & 0x1f) != SIGHASH_NONE) { + if ((hashType & 0x1f) != SIGHASH_SINGLE && + (hashType & 0x1f) != SIGHASH_NONE) { final txOutputsSize = this.outputs.fold(0, (int sum, Output output) { - return sum + 8 + _varSliceSize(output.script); + return sum + 8 + _varSliceSize(output.script!); }); tBuffer = Uint8List(txOutputsSize); tOffset = 0; this.outputs.forEach((Output output) { - writeUint64(output.value); + writeUint64(output.value!); writeVarSlice(output.script); }); - hashOutputs = bcrypto.hash256(tBuffer); - } else if ((hashType & 0x1f) == SIGHASH_SINGLE && (inIndex < this.outputs.length)) { + hashOutputs = bcrypto.Crypto.hash256(tBuffer); + } else if ((hashType & 0x1f) == SIGHASH_SINGLE && + (inIndex < this.outputs.length)) { final output = this.outputs[inIndex]; - tBuffer = Uint8List(8 + _varSliceSize(output.script)); + tBuffer = Uint8List(8 + _varSliceSize(output.script!)); tOffset = 0; - writeUint64(output.value); + writeUint64(output.value!); writeVarSlice(output.script); - hashOutputs = bcrypto.hash256(tBuffer); + hashOutputs = bcrypto.Crypto.hash256(tBuffer); } tBuffer = Uint8List(156 + _varSliceSize(prevOutScript)); @@ -304,23 +313,23 @@ class Transaction { writeUint32(this.version); writeSlice(hashPrevoutputs); writeSlice(hashSequence); - writeSlice(input.hash); - writeUint32(input.index); + writeSlice(input.hash!); + writeUint32(input.index!); writeVarSlice(prevOutScript); writeUint64(amount); - writeUint32(input.sequence); + writeUint32(input.sequence!); writeSlice(hashOutputs); writeUint32(this.locktime); writeUint32(hashType); - return bcrypto.hash256(tBuffer); + return bcrypto.Crypto.hash256(tBuffer); } - _toBuffer([Uint8List buffer, initialOffset]) { + _toBuffer([Uint8List? buffer, initialOffset]) { if (buffer == null) buffer = new Uint8List(virtualSize()); var bytes = buffer.buffer.asByteData(); var offset = initialOffset ?? 0; writeSlice(slice) { - buffer.setRange(offset, offset + slice.length, slice); + buffer!.setRange(offset, offset + slice.length, slice); offset += slice.length; } @@ -381,11 +390,7 @@ class Transaction { return Output.clone(output); }).toList(); Transaction clonedTx = new Transaction( - originalTx.version, - originalTx.locktime, - inputs, - outputs - ); + originalTx.version, originalTx.locktime, inputs, outputs); return clonedTx; } @@ -398,17 +403,17 @@ class Transaction { /// Container for input data and factories to create them class Input { - Uint8List hash; - int index; - int sequence; - int value; - Uint8List script; - Uint8List signScript; - Uint8List prevOutScript; - List pubkeys; - List signatures; + Uint8List? hash; + int? index; + int? sequence; + int? value; + Uint8List? script; + Uint8List? signScript; + Uint8List? prevOutScript; + List? pubkeys; + List? signatures; Input( - {this.hash, + {this.hash, this.index, this.script, this.sequence, @@ -416,13 +421,13 @@ class Input { this.prevOutScript, this.pubkeys, this.signatures}) { - if (this.hash != null && this.hash.length != 32) + if (this.hash != null && this.hash!.length != 32) throw new ArgumentError("Invalid input hash"); - if (this.index != null && !isUint(this.index, 32)) + if (this.index != null && !isUint(this.index!, 32)) throw new ArgumentError("Invalid input index"); - if (this.sequence != null && !isUint(this.sequence, 32)) + if (this.sequence != null && !isUint(this.sequence!, 32)) throw new ArgumentError("Invalid input sequence"); - if (this.value != null && !isSatoshi(this.value)) + if (this.value != null && !isSatoshi(this.value!)) throw ArgumentError("Invalid ouput value"); } @@ -432,57 +437,62 @@ class Input { } P2PKH p2pkh = new P2PKH(data: new P2PKHData(input: scriptSig)); return new Input( - prevOutScript: p2pkh.data.output, - pubkeys: [p2pkh.data.pubkey], - signatures: [p2pkh.data.signature]); + prevOutScript: p2pkh.data.output, + pubkeys: [p2pkh.data.pubkey], + signatures: [p2pkh.data.signature]); } factory Input.clone(Input input) { return new Input( - hash: input.hash != null ? Uint8List.fromList(input.hash) : null, + hash: input.hash != null ? Uint8List.fromList(input.hash!) : null, index: input.index, - script: input.script != null ? Uint8List.fromList(input.script) : null, + script: input.script != null ? Uint8List.fromList(input.script!) : null, sequence: input.sequence, value: input.value, prevOutScript: input.prevOutScript != null - ? Uint8List.fromList(input.prevOutScript) - : null, + ? Uint8List.fromList(input.prevOutScript!) + : null, pubkeys: input.pubkeys != null - ? input.pubkeys.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) - : null, + ? input.pubkeys!.map( + (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) as List? + : null, signatures: input.signatures != null - ? input.signatures.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) - : null, + ? input.signatures!.map((signature) => + signature != null ? Uint8List.fromList(signature) : null) as List? + : null, ); } @override String toString() { - return 'Input{hash: $hash, index: $index, sequence: $sequence, value: $value, script: $script, ' - + 'signScript: $signScript, prevOutScript: $prevOutScript, pubkeys: $pubkeys, signatures: $signatures}'; + return 'Input{hash: $hash, index: $index, sequence: $sequence, value: $value, script: $script, ' + + 'signScript: $signScript, prevOutScript: $prevOutScript, pubkeys: $pubkeys, signatures: $signatures}'; } static bool _isP2PKHInput(script) { final chunks = bscript.decompile(script); return chunks != null && - chunks.length == 2 && - bscript.isCanonicalScriptSignature(chunks[0]) && - bscript.isCanonicalPubKey(chunks[1]); + chunks.length == 2 && + bscript.isCanonicalScriptSignature(chunks[0]) && + bscript.isCanonicalPubKey(chunks[1]); } } /// Container for storing outputs and factories for working with them class Output { - Uint8List script; - int value; - Uint8List valueBuffer; - List pubkeys; - List signatures; - - Output({this.script, this.value, this.pubkeys, this.signatures, this.valueBuffer}) { - if (value != null && !isSatoshi(value)) + Uint8List? script; + int? value; + Uint8List? valueBuffer; + List? pubkeys; + List? signatures; + + Output( + {this.script, + this.value, + this.pubkeys, + this.signatures, + this.valueBuffer}) { + if (value != null && !isSatoshi(value!)) throw ArgumentError("Invalid ouput value"); } /* @@ -498,19 +508,19 @@ class Output { }*/ factory Output.clone(Output output) { return new Output( - script: output.script != null ? Uint8List.fromList(output.script) : null, + script: output.script != null ? Uint8List.fromList(output.script!) : null, value: output.value, valueBuffer: output.valueBuffer != null - ? Uint8List.fromList(output.valueBuffer) - : null, + ? Uint8List.fromList(output.valueBuffer!) + : null, pubkeys: output.pubkeys != null - ? output.pubkeys.map( - (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) - : null, + ? output.pubkeys!.map( + (pubkey) => pubkey != null ? Uint8List.fromList(pubkey) : null) as List? + : null, signatures: output.signatures != null - ? output.signatures.map((signature) => - signature != null ? Uint8List.fromList(signature) : null) - : null, + ? output.signatures!.map((signature) => + signature != null ? Uint8List.fromList(signature) : null) as List? + : null, ); } @@ -518,4 +528,4 @@ class Output { String toString() { return 'Output{script: $script, value: $value, valueBuffer: $valueBuffer, pubkeys: $pubkeys, signatures: $signatures}'; } -} \ No newline at end of file +} diff --git a/lib/src/transactionbuilder.dart b/lib/src/transactionbuilder.dart index ae62634..6fc8289 100644 --- a/lib/src/transactionbuilder.dart +++ b/lib/src/transactionbuilder.dart @@ -1,12 +1,12 @@ import 'dart:typed_data'; import 'package:hex/hex.dart'; import 'package:bs58check/bs58check.dart' as bs58check; -import 'hdnode.dart'; import 'address.dart'; import 'crypto/crypto.dart'; import 'utils/network.dart'; import 'utils/opcodes.dart'; import 'utils/p2pkh.dart'; +import 'utils/p2sh.dart'; import 'utils/script.dart' as bscript; import 'ecpair.dart'; import 'transaction.dart'; @@ -15,12 +15,12 @@ import 'bitcoincash.dart'; /// Toolbox for creating a transaction, that can be broadcasted to BCH network. Works only as an instance created /// through one of the factories or a constructor class TransactionBuilder { - final DEFAULT_SEQUENCE = 0xffffffff; - final SIGHASH_ALL = 0x01; - final SIGHASH_NONE = 0x02; - final SIGHASH_SINGLE = 0x03; - final SIGHASH_ANYONECANPAY = 0x80; - final SIGHASH_BITCOINCASHBIP143 = 0x40; + static const DEFAULT_SEQUENCE = 0xffffffff; + static const SIGHASH_ALL = 0x01; + static const SIGHASH_NONE = 0x02; + static const SIGHASH_SINGLE = 0x03; + static const SIGHASH_ANYONECANPAY = 0x80; + static const SIGHASH_BITCOINCASHBIP143 = 0x40; final Network _network; final int _maximumFeeRate; @@ -29,14 +29,15 @@ class TransactionBuilder { final Map _prevTxSet = {}; /// Creates an empty transaction builder - TransactionBuilder({Network network, int maximumFeeRate}) : - this._network = network ?? Network.bitcoinCash(), - this._maximumFeeRate = maximumFeeRate ?? 2500, - this._inputs = [], - this._tx = new Transaction(); + TransactionBuilder({Network? network, int? maximumFeeRate}) + : this._network = network ?? Network.bitcoinCash(), + this._maximumFeeRate = maximumFeeRate ?? 2500, + this._inputs = [], + this._tx = new Transaction(); /// Creates a builder from pre-built transaction - factory TransactionBuilder.fromTransaction(Transaction transaction, [Network network]) { + factory TransactionBuilder.fromTransaction(Transaction transaction, + [Network? network]) { final txb = new TransactionBuilder(network: network); // Copy transaction fields txb.setVersion(transaction.version); @@ -49,8 +50,8 @@ class TransactionBuilder { // Copy inputs transaction.inputs.forEach((txIn) { - txb._addInputUnsafe(txIn.hash, txIn.index, - new Input(sequence: txIn.sequence, script: txIn.script)); + txb._addInputUnsafe(txIn.hash!, txIn.index, + new Input(sequence: txIn.sequence, script: txIn.script)); }); return txb; @@ -72,7 +73,7 @@ class TransactionBuilder { // if any signatures exist, throw if (this._inputs.map((input) { if (input.signatures == null) return false; - return input.signatures.map((s) { + return input.signatures!.map((s) { return s != null; }).contains(true); }).contains(true)) { @@ -90,8 +91,11 @@ class TransactionBuilder { /// Returns vin of the input /// /// Throws [ArgumentError] if the inputs of this transaction can't be modified or if [txHashOrInstance] is invalid - int addInput(dynamic txHashOrInstance, int vout, [int sequence, Uint8List prevOutScript]) { - assert(txHashOrInstance is String || txHashOrInstance is Uint8List || txHashOrInstance is Transaction); + int addInput(dynamic txHashOrInstance, int? vout, + [int? sequence, Uint8List? prevOutScript]) { + assert(txHashOrInstance is String || + txHashOrInstance is Uint8List || + txHashOrInstance is Transaction); if (!_canModifyInputs()) { throw new ArgumentError('No, this would invalidate signatures'); @@ -103,7 +107,7 @@ class TransactionBuilder { } else if (txHashOrInstance is Uint8List) { hash = txHashOrInstance; } else if (txHashOrInstance is Transaction) { - final txOut = txHashOrInstance.outputs[vout]; + final txOut = txHashOrInstance.outputs[vout!]; prevOutScript = txOut.script; value = txOut.value; hash = txHashOrInstance.getHash(); @@ -111,22 +115,27 @@ class TransactionBuilder { throw ArgumentError('txHash invalid'); } - return _addInputUnsafe(hash, vout, new Input(sequence: sequence, prevOutScript: prevOutScript, value: value)); + return _addInputUnsafe( + hash, + vout, + new Input( + sequence: sequence, prevOutScript: prevOutScript, value: value)); } /// Adds transaction output, which can be provided as: /// * Address as [String] in either _legacy_ or _cashAddr_ format /// * scriptPubKey + /// * scriptHash /// /// Returns output id /// /// Throws [ArgumentError] if outputs can't be modified or the output format is invalid - int addOutput(dynamic data, int value) { - assert (data is String || data is Uint8List); + int addOutput(dynamic data, int? value) { + assert(data is String || data is Uint8List); - Uint8List scriptPubKey; + Uint8List? scriptPubKey; if (data is String) { - if (Address.detectFormat(data) == Address.formatCashAddr) { + if (Address.detectAddressFormat(data) == Address.formatCashAddr) { data = Address.toLegacyAddress(data); } scriptPubKey = _addressToOutputScript(data, _network); @@ -147,16 +156,18 @@ class TransactionBuilder { /// indicate, that the developer plans to add change address later based on a result of this calculation /// /// Throws [ArgumentError] if something goes wrong - int getByteCount([bool addChangeOutput = true]) => - BitcoinCash.getByteCount(this._inputs.length, this._tx.outputs.length + (addChangeOutput ? 1 : 0)); + int getByteCount([bool addChangeOutput = true]) => BitcoinCash.getByteCount( + this._inputs.length, this._tx.outputs.length + (addChangeOutput ? 1 : 0)); /// Add signature for the input [vin] using [keyPair] and with a specified [value] /// /// Throws [ArgumentError] if something goes wrong - sign(int vin, ECPair keyPair, int value, [int hashType = Transaction.SIGHASH_ALL]) { + sign(int vin, ECPair keyPair, int? value, + [int hashType = Transaction.SIGHASH_ALL]) { hashType = hashType | Transaction.SIGHASH_BITCOINCASHBIP143; - if (keyPair.network != null && keyPair.network.toString().compareTo(_network.toString()) != 0) { + if (keyPair.network != null && + keyPair.network.toString().compareTo(_network.toString()) != 0) { throw ArgumentError('Inconsistent network'); } @@ -172,26 +183,27 @@ class TransactionBuilder { final ourPubKey = keyPair.publicKey; if (!_canSign(input)) { - // Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); - _prepareInput(input, ourPubKey, value); + // Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey); + _prepareInput(input, ourPubKey!, value); } - var signatureHash = this._tx.hashForCashSignature(vin, input.signScript, value, hashType); + var signatureHash = + this._tx.hashForCashSignature(vin, input.signScript, value, hashType); // enforce in order signing of public keys var signed = false; - for (var i = 0; i < input.pubkeys.length; i++) { - if (HEX.encode(ourPubKey).compareTo(HEX.encode(input.pubkeys[i])) != 0) { + for (var i = 0; i < input.pubkeys!.length; i++) { + if (HEX.encode(ourPubKey!).compareTo(HEX.encode(input.pubkeys![i]!)) != 0) { continue; } - if (input.signatures[i] != null) { + if (input.signatures![i] != null) { throw ArgumentError('Signature already exists'); } final signature = keyPair.sign(signatureHash); - input.signatures[i] = bscript.encodeSignature(signature, hashType); + input.signatures![i] = bscript.encodeSignature(signature, hashType); signed = true; } @@ -212,8 +224,10 @@ class TransactionBuilder { _build(bool allowIncomplete) { if (!allowIncomplete) { - if (_tx.inputs.length == 0) throw ArgumentError('Transaction has no inputs'); - if (_tx.outputs.length == 0) throw ArgumentError('Transaction has no outputs'); + if (_tx.inputs.length == 0) + throw ArgumentError('Transaction has no inputs'); + if (_tx.outputs.length == 0) + throw ArgumentError('Transaction has no outputs'); } final tx = Transaction.clone(_tx); @@ -227,12 +241,11 @@ class TransactionBuilder { for (var i = 0; i < _inputs.length; i++) { if (_inputs[i].pubkeys != null && - _inputs[i].signatures != null && - _inputs[i].pubkeys.length != 0 && - _inputs[i].signatures.length != 0) { + _inputs[i].signatures != null && + _inputs[i].pubkeys!.length != 0 && + _inputs[i].signatures!.length != 0) { final result = _buildInput(_inputs[i]); tx.setInputScript(i, result); - } else if (!allowIncomplete) { throw new ArgumentError('Transaction is not complete'); } @@ -251,7 +264,7 @@ class TransactionBuilder { bool _canModifyInputs() { return _inputs.every((input) { if (input.signatures == null) return true; - return input.signatures.every((signature) { + return input.signatures!.every((signature) { if (signature == null) return true; return _signatureHashType(signature) & SIGHASH_ANYONECANPAY != 0; }); @@ -263,7 +276,7 @@ class TransactionBuilder { final nOutputs = _tx.outputs.length; return _inputs.every((input) { if (input.signatures == null) return true; - return input.signatures.every((signature) { + return input.signatures!.every((signature) { if (signature == null) return true; final hashType = _signatureHashType(signature); final hashTypeMod = hashType & 0x1f; @@ -286,28 +299,28 @@ class TransactionBuilder { // if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs // .build() will fail, but .buildIncomplete() is OK return (this._tx.outputs.length == 0) && - _inputs.map((input) { - if (input.signatures == null || input.signatures.length == 0) - return false; - return input.signatures.map((signature) { - if (signature == null) return false; // no signature, no issue - final hashType = _signatureHashType(signature); - if (hashType & SIGHASH_NONE != 0) - return false; // SIGHASH_NONE doesn't care about outputs - return true; // SIGHASH_* does care + _inputs.map((input) { + if (input.signatures == null || input.signatures!.length == 0) + return false; + return input.signatures!.map((signature) { + if (signature == null) return false; // no signature, no issue + final hashType = _signatureHashType(signature); + if (hashType & SIGHASH_NONE != 0) + return false; // SIGHASH_NONE doesn't care about outputs + return true; // SIGHASH_* does care + }).contains(true); }).contains(true); - }).contains(true); } bool _canSign(Input input) { return input.pubkeys != null && - input.signScript != null && - input.signatures != null && - input.signatures.length == input.pubkeys.length && - input.pubkeys.length > 0; + input.signScript != null && + input.signatures != null && + input.signatures!.length == input.pubkeys!.length && + input.pubkeys!.length > 0; } - _addInputUnsafe(Uint8List hash, int vout, Input options) { + _addInputUnsafe(Uint8List hash, int? vout, Input options) { String txHash = HEX.encode(hash); Input input; if (_isCoinbaseHash(hash)) { @@ -318,7 +331,7 @@ class TransactionBuilder { if (_prevTxSet[prevTxOut] != null) throw new ArgumentError('Duplicate TxOut: ' + prevTxOut); if (options.script != null) { - input = Input.expandInput(options.script); + input = Input.expandInput(options.script!); } else { input = new Input(); } @@ -343,15 +356,14 @@ class TransactionBuilder { Map get prevTxSet => _prevTxSet; - Input _prepareInput(Input input, Uint8List kpPubKey, int value) { + Input _prepareInput(Input input, Uint8List kpPubKey, int? value) { final prevOutScript = bscript.compile([ Opcodes.OP_DUP, Opcodes.OP_HASH160, - hash160(kpPubKey), + Crypto.hash160(kpPubKey), Opcodes.OP_EQUALVERIFY, Opcodes.OP_CHECKSIG - ]); - + ])!; final expanded = _expandOutput(prevOutScript, kpPubKey); @@ -363,12 +375,9 @@ class TransactionBuilder { } // returns input script - Uint8List _buildInput(Input input) { + Uint8List? _buildInput(Input input) { // this is quite rudimentary for P2PKH purposes - return bscript.compile([ - input.signatures.first, - input.pubkeys.first - ]); + return bscript.compile([input.signatures!.first, input.pubkeys!.first]); } Output _expandOutput(Uint8List script, Uint8List ourPubKey) { @@ -376,12 +385,13 @@ class TransactionBuilder { //TODO: implement other script types too throw ArgumentError("Unsupport script!"); } - - final scriptChunks = bscript.decompile(script); + + final scriptChunks = bscript.decompile(script)!; // does our hash160(pubKey) match the output scripts? - Uint8List pkh1 = scriptChunks[2];//new P2PKH(data: new P2PKHData(output: script)).data.hash; - Uint8List pkh2 = hash160(ourPubKey); + Uint8List pkh1 = scriptChunks[ + 2]; //new P2PKH(data: new P2PKHData(output: script)).data.hash; + Uint8List pkh2 = Crypto.hash160(ourPubKey); // this check should work, but for some reason doesn't - it returns false even if both lists are the same // TODO: debug and re-enable this validation @@ -390,37 +400,67 @@ class TransactionBuilder { return new Output(pubkeys: [ourPubKey], signatures: [null]); } - Uint8List _addressToOutputScript(String address, [Network nw]) { + Uint8List? _addressToOutputScript(String address, [Network? nw]) { final network = nw ?? Network.bitcoinCash(); final payload = bs58check.decode(address); if (payload.length < 21) throw ArgumentError(address + ' is too short'); if (payload.length > 21) throw ArgumentError(address + ' is too long'); - final p2pkh = P2PKH(data: P2PKHData(address: address), network: network); - return p2pkh.data.output; + final version = payload.buffer.asByteData().getUint8(0); + var hash = payload.sublist(1); + Uint8List? output; + + if (hash.length != 20) throw new ArgumentError('Invalid address'); + + if (version == network.pubKeyHash) { + final p2pkh = P2PKH(data: P2PKHData(address: address), network: network); + output = p2pkh.data.output; + } else if (version == network.scriptHash) { + final p2sh = P2SH(data: P2SHData(address: address), network: network); + output = p2sh.data.output; + } else { + return throw ArgumentError(address + 'does not match a valid script'); + } + return output; } - Uint8List _pubkeyToOutputScript(Uint8List pubkey, [Network nw]) { + Uint8List? _pubkeyToOutputScript(Uint8List pubkey, [Network? nw]) { final network = nw ?? Network.bitcoinCash(); final p2pkh = P2PKH(data: P2PKHData(pubkey: pubkey), network: network); return p2pkh.data.output; } - Uint8List _toInputScript(Uint8List pubkey, Uint8List signature, [Network nw]) { + Uint8List? _scriptHashToOutputScript(Uint8List scriptHash, [Network? nw]) { + final network = nw ?? Network.bitcoinCash(); + final p2shh = + P2SH(data: P2SHData(scriptHash: scriptHash), network: network); + return p2shh.data.output; + } + + Uint8List? _toInputScript(Uint8List pubkey, Uint8List signature, + [Network? nw]) { final network = nw ?? Network.bitcoinCash(); final p2pkh = P2PKH( - data: P2PKHData(pubkey: pubkey, signature: signature), - network: network); + data: P2PKHData(pubkey: pubkey, signature: signature), + network: network); return p2pkh.data.input; } bool _isP2PKHOutput(script) { - final buffer = bscript.compile(script); + final buffer = bscript.compile(script)!; return buffer.length == 25 && - buffer[0] == Opcodes.OP_DUP && - buffer[1] == Opcodes.OP_HASH160 && - buffer[2] == 0x14 && - buffer[23] == Opcodes.OP_EQUALVERIFY && - buffer[24] == Opcodes.OP_CHECKSIG; + buffer[0] == Opcodes.OP_DUP && + buffer[1] == Opcodes.OP_HASH160 && + buffer[2] == 0x14 && + buffer[23] == Opcodes.OP_EQUALVERIFY && + buffer[24] == Opcodes.OP_CHECKSIG; + } + + bool _isP2SHOutput(script) { + final buffer = bscript.compile(script)!; + return buffer.length == 23 && + buffer[0] == Opcodes.OP_HASH160 && + buffer[1] == 0x14 && + buffer[2] == Opcodes.OP_EQUAL; } bool _isCoinbaseHash(Uint8List buffer) { @@ -429,4 +469,5 @@ class TransactionBuilder { if (buffer[i] != 0) return false; } return true; - }} \ No newline at end of file + } +} diff --git a/lib/src/utils/bip21.dart b/lib/src/utils/bip21.dart new file mode 100644 index 0000000..8230528 --- /dev/null +++ b/lib/src/utils/bip21.dart @@ -0,0 +1,65 @@ +class Bip21 { + static Map decode(String uri) { + // if (uri.indexOf('bitcoincash') != 0 || uri['bitcoincash'.length] != ":") { + // if (uri.indexOf('bchtest') != 0) throw ("Invalid BIP21 URI"); + // } + + int split = uri.indexOf("?"); + Map uriOptions = Uri.parse(uri).queryParameters; + + Map options = Map.from({ + "message": uriOptions["message"], + "label": uriOptions["label"], + }); + + String address = uri.substring(0, split == -1 ? null : split); + + if (uriOptions["amount"] != null) { + if (uriOptions["amount"]!.indexOf(",") != -1) + throw ("Invalid amount: commas are invalid"); + + double? amount = double.tryParse(uriOptions["amount"]!); + if (amount == null || amount.isNaN) + throw ("Invalid amount: not a number"); + if (!amount.isFinite) throw ("Invalid amount: not finite"); + if (amount < 0) throw ("Invalid amount: not positive"); + options["amount"] = amount; + } + + return { + 'address': address, + 'options': options, + }; + } + + static String encode(String address, Map options) { + var isMainCashAddress = address.startsWith('bitcoincash:'); + var isTestCashAddress = address.startsWith('bchtest:'); + if (!isMainCashAddress) { + address = 'bitcoincash:$address'; + } else if (!isTestCashAddress) { + address = 'bchtest:$address'; + } + + String query = ""; + if (options != null && options.isNotEmpty) { + if (options['amount'] != null) { + if (!options['amount'].isFinite) throw ("Invalid amount: not finite"); + if (options['amount'] < 0) throw ("Invalid amount: not positive"); + } + + Map uriOptions = options; + uriOptions.removeWhere((key, value) => value == null); + uriOptions.forEach((key, value) { + uriOptions[key] = value.toString(); + }); + + if (uriOptions.isEmpty) uriOptions = {}; + query = Uri(queryParameters: uriOptions).toString(); + // Dart isn't following RFC-3986... + query = query.replaceAll(RegExp(r"\+"), "%20"); + } + + return "$address$query"; + } +} diff --git a/lib/src/utils/check_types.dart b/lib/src/utils/check_types.dart index 6dab37a..66fca14 100644 --- a/lib/src/utils/check_types.dart +++ b/lib/src/utils/check_types.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'dart:math'; + const SATOSHI_MAX = 21 * 1e14; bool isSatoshi(int value) { @@ -9,9 +10,11 @@ bool isSatoshi(int value) { bool isUint(int value, int bit) { return (value >= 0 && value <= pow(2, bit) - 1); } + bool isHash160bit(Uint8List value) { return value.length == 20; } + bool isHash256bit(Uint8List value) { return value.length == 32; -} \ No newline at end of file +} diff --git a/lib/src/utils/magic_hash.dart b/lib/src/utils/magic_hash.dart new file mode 100644 index 0000000..fefc947 --- /dev/null +++ b/lib/src/utils/magic_hash.dart @@ -0,0 +1,16 @@ +import 'dart:typed_data'; +import 'dart:convert'; +import '../../bitbox.dart'; + +Uint8List magicHash(String message) { + Uint8List messagePrefix = + Uint8List.fromList(utf8.encode('\x18Bitcoin Signed Message:\n')); + int messageVISize = encodingLength(message.length); + int length = messagePrefix.length + messageVISize + message.length; + Uint8List buffer = new Uint8List(length); + buffer.setRange(0, messagePrefix.length, messagePrefix); + encode(message.length, buffer, messagePrefix.length); + buffer.setRange( + messagePrefix.length + messageVISize, length, utf8.encode(message)); + return Crypto.hash256(buffer); +} diff --git a/lib/src/utils/network.dart b/lib/src/utils/network.dart index cf9956b..059dd91 100644 --- a/lib/src/utils/network.dart +++ b/lib/src/utils/network.dart @@ -9,17 +9,24 @@ class Network { static const bchPublic = 0x00; static const bchTestnetPublic = 0x6f; + static const bchPublicscriptHash = 0x05; + static const bchTestnetscriptHash = 0xc4; + final int bip32Private; final int bip32Public; final bool testnet; final int pubKeyHash; + final int scriptHash; final int private; final int public; - Network(this.bip32Private, this.bip32Public, this.testnet, this.pubKeyHash, this.private, this.public); + Network(this.bip32Private, this.bip32Public, this.testnet, this.pubKeyHash, + this.scriptHash, this.private, this.public); - factory Network.bitcoinCash() => Network(0x0488ade4, 0x0488b21e, false, 0x00, bchPrivate, bchPublic); - factory Network.bitcoinCashTest() => Network(0x04358394, 0x043587cf, true, 0x6f, bchTestnetPrivate, bchTestnetPublic); + factory Network.bitcoinCash() => + Network(0x0488ade4, 0x0488b21e, false, 0x00, 0x05, bchPrivate, bchPublic); + factory Network.bitcoinCashTest() => Network(0x04358394, 0x043587cf, true, + 0x6f, 0xc4, bchTestnetPrivate, bchTestnetPublic); String get prefix => this.testnet ? "bchtest" : "bitcoincash"; -} \ No newline at end of file +} diff --git a/lib/src/utils/opcodes.dart b/lib/src/utils/opcodes.dart index 189c2f7..25a700d 100644 --- a/lib/src/utils/opcodes.dart +++ b/lib/src/utils/opcodes.dart @@ -1,120 +1,120 @@ class Opcodes { - static const OP_FALSE = 0; - static const OP_0 = 0; - static const OP_PUSHDATA1 = 76; - static const OP_PUSHDATA2 = 77; - static const OP_PUSHDATA4 = 78; - static const OP_1NEGATE = 79; - static const OP_RESERVED = 80; - static const OP_TRUE = 81; - static const OP_1 = 81; - static const OP_2 = 82; - static const OP_3 = 83; - static const OP_4 = 84; - static const OP_5 = 85; - static const OP_6 = 86; - static const OP_7 = 87; - static const OP_8 = 88; - static const OP_9 = 89; - static const OP_10 = 90; - static const OP_11 = 91; - static const OP_12 = 92; - static const OP_13 = 93; - static const OP_14 = 94; - static const OP_15 = 95; - static const OP_16 = 96; - static const OP_NOP = 97; - static const OP_VER = 98; - static const OP_IF = 99; - static const OP_NOTIF = 100; - static const OP_VERIF = 101; - static const OP_VERNOTIF = 102; - static const OP_ELSE = 103; - static const OP_ENDIF = 104; - static const OP_VERIFY = 105; - static const OP_RETURN = 106; - static const OP_TOALTSTACK = 107; - static const OP_FROMALTSTACK = 108; - static const OP_2DROP = 109; - static const OP_2DUP = 110; - static const OP_3DUP = 111; - static const OP_2OVER = 112; - static const OP_2ROT = 113; - static const OP_2SWAP = 114; - static const OP_IFDUP = 115; - static const OP_DEPTH = 116; - static const OP_DROP = 117; - static const OP_DUP = 118; - static const OP_NIP = 119; - static const OP_OVER = 120; - static const OP_PICK = 121; - static const OP_ROLL = 122; - static const OP_ROT = 123; - static const OP_SWAP = 124; - static const OP_TUCK = 125; - static const OP_CAT = 126; - static const OP_SPLIT = 127; - static const OP_NUM2BIN = 128; - static const OP_BIN2NUM = 129; - static const OP_SIZE = 130; - static const OP_INVERT = 131; - static const OP_AND = 132; - static const OP_OR = 133; - static const OP_XOR = 134; - static const OP_EQUAL = 135; - static const OP_EQUALVERIFY = 136; - static const OP_RESERVED1 = 137; - static const OP_RESERVED2 = 138; - static const OP_1ADD = 139; - static const OP_1SUB = 140; - static const OP_2MUL = 141; - static const OP_2DIV = 142; - static const OP_NEGATE = 143; - static const OP_ABS = 144; - static const OP_NOT = 145; - static const OP_0NOTEQUAL = 146; - static const OP_ADD = 147; - static const OP_SUB = 148; - static const OP_MUL = 149; - static const OP_DIV = 150; - static const OP_MOD = 151; - static const OP_LSHIFT = 152; - static const OP_RSHIFT = 153; - static const OP_BOOLAND = 154; - static const OP_BOOLOR = 155; - static const OP_NUMEQUAL = 156; - static const OP_NUMEQUALVERIFY = 157; - static const OP_NUMNOTEQUAL = 158; - static const OP_LESSTHAN = 159; - static const OP_GREATERTHAN = 160; - static const OP_LESSTHANOREQUAL = 161; - static const OP_GREATERTHANOREQUAL = 162; - static const OP_MIN = 163; - static const OP_MAX = 164; - static const OP_WITHIN = 165; - static const OP_RIPEMD160 = 166; - static const OP_SHA1 = 167; - static const OP_SHA256 = 168; - static const OP_HASH160 = 169; - static const OP_HASH256 = 170; - static const OP_CODESEPARATOR = 171; - static const OP_CHECKSIG = 172; - static const OP_CHECKSIGVERIFY = 173; - static const OP_CHECKMULTISIG = 174; - static const OP_CHECKMULTISIGVERIFY = 175; - static const OP_NOP1 = 176; - static const OP_NOP2 = 177; - static const OP_CHECKLOCKTIMEVERIFY = 177; - static const OP_NOP3 = 178; - static const OP_CHECKSEQUENCEVERIFY = 178; - static const OP_NOP4 = 179; - static const OP_NOP5 = 180; - static const OP_NOP6 = 181; - static const OP_NOP7 = 182; - static const OP_NOP8 = 183; - static const OP_NOP9 = 184; - static const OP_NOP10 = 185; - static const OP_PUBKEYHASH = 253; - static const OP_PUBKEY = 254; - static const OP_INVALIDOPCODE = 255; -} \ No newline at end of file + static const OP_FALSE = 0; + static const OP_0 = 0; + static const OP_PUSHDATA1 = 76; + static const OP_PUSHDATA2 = 77; + static const OP_PUSHDATA4 = 78; + static const OP_1NEGATE = 79; + static const OP_RESERVED = 80; + static const OP_TRUE = 81; + static const OP_1 = 81; + static const OP_2 = 82; + static const OP_3 = 83; + static const OP_4 = 84; + static const OP_5 = 85; + static const OP_6 = 86; + static const OP_7 = 87; + static const OP_8 = 88; + static const OP_9 = 89; + static const OP_10 = 90; + static const OP_11 = 91; + static const OP_12 = 92; + static const OP_13 = 93; + static const OP_14 = 94; + static const OP_15 = 95; + static const OP_16 = 96; + static const OP_NOP = 97; + static const OP_VER = 98; + static const OP_IF = 99; + static const OP_NOTIF = 100; + static const OP_VERIF = 101; + static const OP_VERNOTIF = 102; + static const OP_ELSE = 103; + static const OP_ENDIF = 104; + static const OP_VERIFY = 105; + static const OP_RETURN = 106; + static const OP_TOALTSTACK = 107; + static const OP_FROMALTSTACK = 108; + static const OP_2DROP = 109; + static const OP_2DUP = 110; + static const OP_3DUP = 111; + static const OP_2OVER = 112; + static const OP_2ROT = 113; + static const OP_2SWAP = 114; + static const OP_IFDUP = 115; + static const OP_DEPTH = 116; + static const OP_DROP = 117; + static const OP_DUP = 118; + static const OP_NIP = 119; + static const OP_OVER = 120; + static const OP_PICK = 121; + static const OP_ROLL = 122; + static const OP_ROT = 123; + static const OP_SWAP = 124; + static const OP_TUCK = 125; + static const OP_CAT = 126; + static const OP_SPLIT = 127; + static const OP_NUM2BIN = 128; + static const OP_BIN2NUM = 129; + static const OP_SIZE = 130; + static const OP_INVERT = 131; + static const OP_AND = 132; + static const OP_OR = 133; + static const OP_XOR = 134; + static const OP_EQUAL = 135; + static const OP_EQUALVERIFY = 136; + static const OP_RESERVED1 = 137; + static const OP_RESERVED2 = 138; + static const OP_1ADD = 139; + static const OP_1SUB = 140; + static const OP_2MUL = 141; + static const OP_2DIV = 142; + static const OP_NEGATE = 143; + static const OP_ABS = 144; + static const OP_NOT = 145; + static const OP_0NOTEQUAL = 146; + static const OP_ADD = 147; + static const OP_SUB = 148; + static const OP_MUL = 149; + static const OP_DIV = 150; + static const OP_MOD = 151; + static const OP_LSHIFT = 152; + static const OP_RSHIFT = 153; + static const OP_BOOLAND = 154; + static const OP_BOOLOR = 155; + static const OP_NUMEQUAL = 156; + static const OP_NUMEQUALVERIFY = 157; + static const OP_NUMNOTEQUAL = 158; + static const OP_LESSTHAN = 159; + static const OP_GREATERTHAN = 160; + static const OP_LESSTHANOREQUAL = 161; + static const OP_GREATERTHANOREQUAL = 162; + static const OP_MIN = 163; + static const OP_MAX = 164; + static const OP_WITHIN = 165; + static const OP_RIPEMD160 = 166; + static const OP_SHA1 = 167; + static const OP_SHA256 = 168; + static const OP_HASH160 = 169; + static const OP_HASH256 = 170; + static const OP_CODESEPARATOR = 171; + static const OP_CHECKSIG = 172; + static const OP_CHECKSIGVERIFY = 173; + static const OP_CHECKMULTISIG = 174; + static const OP_CHECKMULTISIGVERIFY = 175; + static const OP_NOP1 = 176; + static const OP_NOP2 = 177; + static const OP_CHECKLOCKTIMEVERIFY = 177; + static const OP_NOP3 = 178; + static const OP_CHECKSEQUENCEVERIFY = 178; + static const OP_NOP4 = 179; + static const OP_NOP5 = 180; + static const OP_NOP6 = 181; + static const OP_NOP7 = 182; + static const OP_NOP8 = 183; + static const OP_NOP9 = 184; + static const OP_NOP10 = 185; + static const OP_PUBKEYHASH = 253; + static const OP_PUBKEY = 254; + static const OP_INVALIDOPCODE = 255; +} diff --git a/lib/src/utils/p2pkh.dart b/lib/src/utils/p2pkh.dart index 13692f3..a1f8a99 100644 --- a/lib/src/utils/p2pkh.dart +++ b/lib/src/utils/p2pkh.dart @@ -11,10 +11,10 @@ import 'network.dart'; /// This is almost exact copy of https://github.com/anicdh/bitcoin_flutter/blob/master/lib/src/payments/p2pkh.dart /// except using [Opcodes] static members instead of map class P2PKH { - P2PKHData data; - Network network; + late P2PKHData data; + late Network network; - P2PKH({@required data, network}) { + P2PKH({required data, network}) { this.network = network ?? Network.bitcoinCash(); this.data = data; _init(); @@ -22,21 +22,21 @@ class P2PKH { _init() { if (data.address != null) { - _getDataFromAddress(data.address); + _getDataFromAddress(data.address!); _getDataFromHash(); } else if (data.hash != null) { _getDataFromHash(); } else if (data.output != null) { - if (!isValidOutput(data.output)) + if (!isValidOutput(data.output!)) throw new ArgumentError('Output is invalid'); - data.hash = data.output.sublist(3, 23); + data.hash = data.output!.sublist(3, 23); _getDataFromHash(); } else if (data.pubkey != null) { - data.hash = hash160(data.pubkey); + data.hash = Crypto.hash160(data.pubkey!); _getDataFromHash(); _getDataFromChunk(); } else if (data.input != null) { - List _chunks = bscript.decompile(data.input); + List _chunks = bscript.decompile(data.input!)!; _getDataFromChunk(_chunks); if (_chunks.length != 2) throw new ArgumentError('Input is invalid'); if (!bscript.isCanonicalScriptSignature(_chunks[0])) @@ -48,14 +48,18 @@ class P2PKH { } } - void _getDataFromChunk([List _chunks]) { + void _getDataFromChunk([List? _chunks]) { if (data.pubkey == null && _chunks != null) { - data.pubkey = (_chunks[1] is int) ? new Uint8List.fromList([_chunks[1]]) : _chunks[1]; - data.hash = hash160(data.pubkey); + data.pubkey = (_chunks[1] is int) + ? new Uint8List.fromList([_chunks[1]]) + : _chunks[1]; + data.hash = Crypto.hash160(data.pubkey!); _getDataFromHash(); } if (data.signature == null && _chunks != null) - data.signature = (_chunks[0] is int) ? new Uint8List.fromList([_chunks[0]]) : _chunks[0]; + data.signature = (_chunks[0] is int) + ? new Uint8List.fromList([_chunks[0]]) + : _chunks[0]; if (data.input == null && data.pubkey != null && data.signature != null) { data.input = bscript.compile([data.signature, data.pubkey]); } @@ -65,7 +69,7 @@ class P2PKH { if (data.address == null) { final payload = new Uint8List(21); payload.buffer.asByteData().setUint8(0, network.pubKeyHash); - payload.setRange(1, payload.length, data.hash); + payload.setRange(1, payload.length, data.hash!); data.address = bs58check.encode(payload); } if (data.output == null) { @@ -85,19 +89,19 @@ class P2PKH { if (version != network.pubKeyHash) throw new ArgumentError('Invalid version or Network mismatch'); data.hash = payload.sublist(1); - if (data.hash.length != 20) throw new ArgumentError('Invalid address'); + if (data.hash!.length != 20) throw new ArgumentError('Invalid address'); } } class P2PKHData { - String address; - Uint8List hash; - Uint8List output; - Uint8List signature; - Uint8List pubkey; - Uint8List input; + String? address; + Uint8List? hash; + Uint8List? output; + Uint8List? signature; + Uint8List? pubkey; + Uint8List? input; P2PKHData( - {this.address, + {this.address, this.hash, this.output, this.pubkey, @@ -112,9 +116,9 @@ class P2PKHData { isValidOutput(Uint8List data) { return data.length == 25 && - data[0] == Opcodes.OP_DUP && - data[1] == Opcodes.OP_HASH160 && - data[2] == 0x14 && - data[23] == Opcodes.OP_EQUALVERIFY && - data[24] == Opcodes.OP_CHECKSIG; + data[0] == Opcodes.OP_DUP && + data[1] == Opcodes.OP_HASH160 && + data[2] == 0x14 && + data[23] == Opcodes.OP_EQUALVERIFY && + data[24] == Opcodes.OP_CHECKSIG; } diff --git a/lib/src/utils/p2sh.dart b/lib/src/utils/p2sh.dart new file mode 100644 index 0000000..f9ee4f8 --- /dev/null +++ b/lib/src/utils/p2sh.dart @@ -0,0 +1,119 @@ +import 'dart:typed_data'; +import '../crypto/crypto.dart'; +import '../utils/opcodes.dart'; +import 'package:meta/meta.dart'; +import 'package:bip32/src/utils/ecurve.dart' show isPoint; +import 'package:bs58check/bs58check.dart' as bs58check; +import 'script.dart' as bscript; + +import 'network.dart'; + +/// This is almost exact copy of https://github.com/anicdh/bitcoin_flutter/blob/master/lib/src/payments/p2pkh.dart +/// except using [Opcodes] static members instead of map +class P2SH { + late P2SHData data; + late Network network; + + P2SH({required data, network}) { + this.network = network ?? Network.bitcoinCash(); + this.data = data; + _init(); + } + + _init() { + if (data.address != null) { + _getDataFromAddress(data.address!); + _getDataFromHash(); + } else if (data.hash != null) { + _getDataFromHash(); + } else if (data.output != null) { + if (!isValidOutput(data.output!)) + throw new ArgumentError('Output is invalid'); + data.hash = data.output!.sublist(2, 22); + _getDataFromHash(); + } else if (data.scriptHash != null) { + data.hash = Crypto.hash160(data.scriptHash!); + _getDataFromHash(); + _getDataFromChunk(); + } else if (data.input != null) { + List _chunks = bscript.decompile(data.input!)!; + _getDataFromChunk(_chunks); + if (_chunks.length != 2) throw new ArgumentError('Input is invalid'); + if (!bscript.isCanonicalScriptSignature(_chunks[0])) + throw new ArgumentError('Input has invalid signature'); + if (!isPoint(_chunks[1])) + throw new ArgumentError('Input has invalid pubkey'); + } else { + throw new ArgumentError("Not enough data"); + } + } + + void _getDataFromChunk([List? _chunks]) { + if (data.scriptHash == null && _chunks != null) { + data.scriptHash = (_chunks[1] is int) + ? new Uint8List.fromList([_chunks[1]]) + : _chunks[1]; + data.hash = Crypto.hash160(data.scriptHash!); + _getDataFromHash(); + } + if (data.signature == null && _chunks != null) + data.signature = (_chunks[0] is int) + ? new Uint8List.fromList([_chunks[0]]) + : _chunks[0]; + if (data.input == null && + data.scriptHash != null && + data.signature != null) { + data.input = bscript.compile([data.signature, data.scriptHash]); + } + } + + void _getDataFromHash() { + if (data.address == null) { + final payload = new Uint8List(21); + payload.buffer.asByteData().setUint8(0, network.scriptHash); + payload.setRange(1, payload.length, data.hash!); + data.address = bs58check.encode(payload); + } + if (data.output == null) { + data.output = + bscript.compile([Opcodes.OP_HASH160, data.hash, Opcodes.OP_EQUAL]); + } + } + + void _getDataFromAddress(String address) { + Uint8List payload = bs58check.decode(address); + final version = payload.buffer.asByteData().getUint8(0); + if (version != network.scriptHash) + throw new ArgumentError('Invalid version or Network mismatch'); + data.hash = payload.sublist(1); + if (data.hash!.length != 20) throw new ArgumentError('Invalid address'); + } +} + +class P2SHData { + String? address; + Uint8List? hash; + Uint8List? output; + Uint8List? signature; + Uint8List? scriptHash; + Uint8List? input; + P2SHData( + {this.address, + this.hash, + this.output, + this.scriptHash, + this.input, + this.signature}); + + @override + String toString() { + return 'P2SHData{address: $address, hash: $hash, output: $output, signature: $signature, pubkey: $scriptHash, input: $input}'; + } +} + +isValidOutput(Uint8List data) { + return data.length == 23 && + data[0] == Opcodes.OP_HASH160 && + data[1] == 0x14 && + data[2] == Opcodes.OP_EQUAL; +} diff --git a/lib/src/utils/pushdata.dart b/lib/src/utils/pushdata.dart index 090a28c..43db703 100644 --- a/lib/src/utils/pushdata.dart +++ b/lib/src/utils/pushdata.dart @@ -2,48 +2,45 @@ import 'dart:typed_data'; import '../utils/opcodes.dart'; class DecodedPushData { - int opcode; - int number; - int size; + int? opcode; + int? number; + int? size; DecodedPushData({this.opcode, this.number, this.size}); } class EncodedPushData { - int size; - Uint8List buffer; + int? size; + Uint8List? buffer; EncodedPushData({this.size, this.buffer}); - } -EncodedPushData encode(Uint8List buffer, number, offset) { + +EncodedPushData encode(Uint8List? buffer, number, offset) { var size = encodingLength(number); // ~6 bit if (size == 1) { - buffer.buffer.asByteData().setUint8(offset, number); + buffer!.buffer.asByteData().setUint8(offset, number); // 8 bit } else if (size == 2) { - buffer.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA1); + buffer!.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA1); buffer.buffer.asByteData().setUint8(offset + 1, number); // 16 bit } else if (size == 3) { - buffer.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA2); + buffer!.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA2); buffer.buffer.asByteData().setUint16(offset + 1, number, Endian.little); // 32 bit } else { - buffer.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA4); + buffer!.buffer.asByteData().setUint8(offset, Opcodes.OP_PUSHDATA4); buffer.buffer.asByteData().setUint32(offset + 1, number, Endian.little); } - return new EncodedPushData( - size: size, - buffer: buffer - ); + return new EncodedPushData(size: size, buffer: buffer); } -DecodedPushData decode(Uint8List bf, int offset) { +DecodedPushData? decode(Uint8List bf, int offset) { ByteBuffer buffer = bf.buffer; int opcode = buffer.asByteData().getUint8(offset); int number, size; @@ -68,21 +65,16 @@ DecodedPushData decode(Uint8List bf, int offset) { // 32 bit } else { if (offset + 5 > buffer.lengthInBytes) return null; - if (opcode != Opcodes.OP_PUSHDATA4) throw new ArgumentError('Unexpected opcode'); + if (opcode != Opcodes.OP_PUSHDATA4) + throw new ArgumentError('Unexpected opcode'); number = buffer.asByteData().getUint32(offset + 1); size = 5; } - return DecodedPushData( - opcode: opcode, - number: number, - size: size - ); + return DecodedPushData(opcode: opcode, number: number, size: size); +} + +int encodingLength(i) { + return i < Opcodes.OP_PUSHDATA1 ? 1 : i <= 0xff ? 2 : i <= 0xffff ? 3 : 5; } -int encodingLength (i) { - return i < Opcodes.OP_PUSHDATA1 ? 1 - : i <= 0xff ? 2 - : i <= 0xffff ? 3 - : 5; -} \ No newline at end of file diff --git a/lib/src/utils/rest_api.dart b/lib/src/utils/rest_api.dart index 74ed000..ed314e5 100644 --- a/lib/src/utils/rest_api.dart +++ b/lib/src/utils/rest_api.dart @@ -2,14 +2,15 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class RestApi { - static String _restUrl = "https://rest.bitcoin.com/v2/"; + static String _restUrl = "https://rest1.biggestfan.net/v2"; static set restUrl(String restUrl) { _restUrl = restUrl; } - static Future sendGetRequest(String path, [String parameter = ""]) async { - final response = await http.get("$_restUrl$path/$parameter"); + static Future sendGetRequest(String path, + [String? parameter = ""]) async { + final response = await http.get(Uri.parse("$_restUrl$path/$parameter")); if (response.statusCode == 200) { return jsonDecode(response.body); @@ -18,11 +19,16 @@ class RestApi { } } - static Future sendPostRequest(String path, String postKey, List data, {String returnKey}) async { + static Future sendPostRequest( + String path, String postKey, List data, + {String? returnKey}) async { + if (postKey == null || postKey.isEmpty) { + postKey = "addresses"; + } final response = await http.post( - "${_restUrl}$path", + Uri.parse("$_restUrl$path"), headers: {"content-type": "application/json"}, - body: jsonEncode({"addresses" : data}), + body: jsonEncode({postKey: data}), ); if (response.statusCode != 200) { @@ -35,14 +41,16 @@ class RestApi { final responseData = jsonDecode(response.body); if (!responseData is List || !responseData.first is Map) { - throw FormatException("return data (below) is not List of Maps: \n${response.body}"); + throw FormatException( + "return data (below) is not List of Maps: \n${response.body}"); } - Map returnMap = {}; + Map returnMap = {}; responseData.forEach((Map item) { if (!item.containsKey(returnKey)) { - throw FormatException("return data (below) doesn't contain key $returnKey: $item"); + throw FormatException( + "return data (below) doesn't contain key $returnKey: $item"); } returnMap[item[returnKey]] = item; }); @@ -50,4 +58,4 @@ class RestApi { return returnMap; } } -} \ No newline at end of file +} diff --git a/lib/src/utils/script.dart b/lib/src/utils/script.dart index 51a09cf..5920713 100644 --- a/lib/src/utils/script.dart +++ b/lib/src/utils/script.dart @@ -3,20 +3,20 @@ import '../crypto/ecurve.dart'; import '../utils/opcodes.dart'; import 'pushdata.dart' as pushData; import 'check_types.dart'; -//import 'check_types.dart'; + //Map REVERSE_OPS = opcodes.map((String string, int number) => new MapEntry(number, string)); -final OP_INT_BASE = Opcodes.OP_RESERVED; -final ZERO = Uint8List.fromList([0]); +const OP_INT_BASE = Opcodes.OP_RESERVED; +final zero = Uint8List.fromList([0]); -Uint8List compile(List chunks) { - final bufferSize = chunks.fold(0, (acc, chunk) { +Uint8List? compile(List chunks) { + final bufferSize = chunks.fold(0, (dynamic acc, chunk) { if (chunk is int) return acc + 1; if (chunk.length == 1 && asMinimalOP(chunk) != null) { return acc + 1; } return acc + pushData.encodingLength(chunk.length) + chunk.length; }); - var buffer = new Uint8List(bufferSize); + Uint8List? buffer = new Uint8List(bufferSize); var offset = 0; chunks.forEach((chunk) { @@ -25,27 +25,29 @@ Uint8List compile(List chunks) { // adhere to BIP62.3, minimal push policy final opcode = asMinimalOP(chunk); if (opcode != null) { - buffer.buffer.asByteData().setUint8(offset, opcode); + buffer!.buffer.asByteData().setUint8(offset, opcode); offset += 1; return null; } - pushData.EncodedPushData epd = pushData.encode(buffer, chunk.length, offset); - offset += epd.size; + pushData.EncodedPushData epd = + pushData.encode(buffer, chunk.length, offset); + offset += epd.size!; buffer = epd.buffer; - buffer.setRange(offset, offset + chunk.length, chunk); + buffer!.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; // opcode } else { - buffer.buffer.asByteData().setUint8(offset, chunk); + buffer!.buffer.asByteData().setUint8(offset, chunk); offset += 1; } }); - if (offset != buffer.length) throw new ArgumentError("Could not decode chunks"); + if (offset != buffer!.length) + throw new ArgumentError("Could not decode chunks"); return buffer; } -List decompile(Uint8List buffer) { +List? decompile(Uint8List buffer) { List chunks = []; var i = 0; @@ -58,13 +60,13 @@ List decompile(Uint8List buffer) { // did reading a pushDataInt fail? if (d == null) return null; - i += d.size; + i += d.size!; // attempt to read too much data? - if (i + d.number > buffer.length) return null; + if (i + d.number! > buffer.length) return null; - final data = buffer.sublist(i, i + d.number); - i += d.number; + final data = buffer.sublist(i, i + d.number!); + i += d.number!; // decompile minimally final op = asMinimalOP(data); @@ -107,25 +109,28 @@ String toASM (List c) { }).join(' '); }*/ -int asMinimalOP (Uint8List buffer) { +int? asMinimalOP(Uint8List buffer) { if (buffer.length == 0) return Opcodes.OP_0; if (buffer.length != 1) return null; if (buffer[0] >= 1 && buffer[0] <= 16) return OP_INT_BASE + buffer[0]; if (buffer[0] == 0x81) return Opcodes.OP_1NEGATE; return null; } -bool isDefinedHashType (hashType) { + +bool isDefinedHashType(hashType) { final hashTypeMod = hashType & ~0x80; // return hashTypeMod > SIGHASH_ALL && hashTypeMod < SIGHASH_SINGLE return hashTypeMod > 0x00 && hashTypeMod < 0x04; } -bool isCanonicalPubKey (Uint8List buffer) => ECurve.isPoint(buffer); -bool isCanonicalScriptSignature (Uint8List buffer) { +bool isCanonicalPubKey(Uint8List buffer) => ECurve.isPoint(buffer); + +bool isCanonicalScriptSignature(Uint8List buffer) { if (!isDefinedHashType(buffer[buffer.length - 1])) return false; return bip66check(buffer.sublist(0, buffer.length - 1)); } -bool bip66check (buffer) { + +bool bip66check(buffer) { if (buffer.length < 8) return false; if (buffer.length > 72) return false; if (buffer[0] != 0x30) return false; @@ -145,7 +150,8 @@ bool bip66check (buffer) { if (lenR > 1 && (buffer[4] == 0x00) && buffer[5] & 0x80 == 0) return false; if (buffer[lenR + 6] & 0x80 != 0) return false; - if (lenS > 1 && (buffer[lenR + 6] == 0x00) && buffer[lenR + 7] & 0x80 == 0) return false; + if (lenS > 1 && (buffer[lenR + 6] == 0x00) && buffer[lenR + 7] & 0x80 == 0) + return false; return true; } @@ -158,29 +164,31 @@ Uint8List bip66encode(r, s) { if (lenS > 33) throw new ArgumentError('S length is too long'); if (r[0] & 0x80 != 0) throw new ArgumentError('R value is negative'); if (s[0] & 0x80 != 0) throw new ArgumentError('S value is negative'); - if (lenR > 1 && (r[0] == 0x00) && r[1] & 0x80 == 0) throw new ArgumentError('R value excessively padded'); - if (lenS > 1 && (s[0] == 0x00) && s[1] & 0x80 == 0) throw new ArgumentError('S value excessively padded'); + if (lenR > 1 && (r[0] == 0x00) && r[1] & 0x80 == 0) + throw new ArgumentError('R value excessively padded'); + if (lenS > 1 && (s[0] == 0x00) && s[1] & 0x80 == 0) + throw new ArgumentError('S value excessively padded'); - var signature = new Uint8List(6 + lenR + lenS); + var signature = new Uint8List(6 + lenR + lenS as int); // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] signature[0] = 0x30; signature[1] = signature.length - 2; signature[2] = 0x02; signature[3] = r.length; - signature.setRange(4, 4 + lenR, r); - signature[4 + lenR] = 0x02; - signature[5 + lenR] = s.length; - signature.setRange(6 + lenR, 6 + lenR + lenS, s); + signature.setRange(4, 4 + lenR as int, r); + signature[4 + lenR as int] = 0x02; + signature[5 + lenR as int] = s.length; + signature.setRange(6 + lenR as int, 6 + lenR + lenS as int, s); return signature; } - Uint8List encodeSignature(Uint8List signature, int hashType) { if (!isUint(hashType, 8)) throw ArgumentError("Invalid hasType $hashType"); if (signature.length != 64) throw ArgumentError("Invalid signature"); - final hashTypeMod = hashType & ~0xc0;//0x80; - if (hashTypeMod <= 0 || hashTypeMod >= 4) throw new ArgumentError('Invalid hashType $hashType'); + final hashTypeMod = hashType & ~0xc0; //0x80; + if (hashTypeMod <= 0 || hashTypeMod >= 4) + throw new ArgumentError('Invalid hashType $hashType'); final hashTypeBuffer = new Uint8List(1); hashTypeBuffer.buffer.asByteData().setUint8(0, hashType); @@ -190,13 +198,14 @@ Uint8List encodeSignature(Uint8List signature, int hashType) { combine.addAll(List.from(hashTypeBuffer)); return Uint8List.fromList(combine); } -Uint8List toDER (Uint8List x) { + +Uint8List toDER(Uint8List x) { var i = 0; while (x[i] == 0) ++i; - if (i == x.length) return ZERO; + if (i == x.length) return zero; x = x.sublist(i); - List combine = List.from(ZERO); + List combine = List.from(zero); combine.addAll(x); if (x[0] & 0x80 != 0) return Uint8List.fromList(combine); return x; -} \ No newline at end of file +} diff --git a/lib/src/varuint.dart b/lib/src/varuint.dart index 4ad58a0..f01bce1 100644 --- a/lib/src/varuint.dart +++ b/lib/src/varuint.dart @@ -1,9 +1,8 @@ import 'dart:typed_data'; -import 'transaction.dart'; import 'utils/check_types.dart'; -Uint8List encode(int number, [Uint8List buffer,int offset]) { - if (!isUint(number, 53)); +Uint8List encode(int number, [Uint8List? buffer,int? offset]) { +// if (!isUint(number, 53)); buffer = buffer ?? new Uint8List(encodingLength(number)); offset = offset ?? 0; @@ -31,7 +30,7 @@ Uint8List encode(int number, [Uint8List buffer,int offset]) { return buffer; } -int decode (Uint8List buffer, [int offset]) { +int decode (Uint8List buffer, [int? offset]) { offset = offset ?? 0; ByteData bytes = buffer.buffer.asByteData(); final first = bytes.getUint8(offset); diff --git a/pubspec.lock b/pubspec.lock index ec53c86..26a7f04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,76 +1,112 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.11.0" bip32: dependency: "direct main" description: - name: bip32 - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" + path: "." + ref: master + resolved-ref: "029814992478134be170a1497a637ec740015e36" + url: "https://github.com/zapit-io/bip32-dart.git" + source: git + version: "2.0.0" bip39: dependency: "direct main" description: - name: bip39 - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" + path: "." + ref: master + resolved-ref: "06a412e02ece46e648cd21e96b4f74fadeea5784" + url: "https://github.com/zapit-io/bip39.git" + source: git + version: "1.0.6" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.1.1" bs58check: dependency: "direct main" description: name: bs58check - url: "https://pub.dartlang.org" + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + buffer: + dependency: "direct main" + description: + name: buffer + sha256: "8962c12174f53e2e848a6acd7ac7fd63d8a1a6a316c20c458a832d87eba5422a" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.0.1" - charcode: + version: "1.3.0" + clock: dependency: transitive description: - name: charcode - url: "https://pub.dartlang.org" + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.14.11" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.1.1" crypto: - dependency: transitive + dependency: "direct overridden" description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "1.3.1" fixnum: dependency: "direct main" description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "0.10.9" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -85,125 +121,160 @@ packages: dependency: "direct main" description: name: hex - url: "https://pub.dartlang.org" + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.2.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" source: hosted - version: "0.12.0+2" + version: "0.13.6" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "0.6.7" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.5" - meta: + version: "0.12.15" + material_color_utilities: dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: "direct main" description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.9.1" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.6.2" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.0" + version: "1.8.3" pointycastle: dependency: "direct main" description: name: pointycastle - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.7.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + slp_mdm: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: a28df2937d97612cafb681352bcc1fa71be99e94 + url: "https://github.com/zapit-io/slp-metadatamaker.dart.git" + source: git + version: "1.0.0" + slp_parser: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: e0e53d225345e09cba3a1b056f655431cdcd36be + url: "https://github.com/zapit-io/slp-parser.dart.git" + source: git + version: "0.1.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.9.3" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.5.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.3.2" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.1.4" sdks: - dart: ">=2.2.0 <3.0.0" + dart: ">=3.0.0-0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f7f9d8b..2ef99f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,42 @@ name: bitbox -description: A new Flutter package project. +description: "BITBOX SDK lite for Flutter: build native cross-platform mobile apps with Bitcoin Cash" version: 0.0.1 -author: -homepage: +author: Tomas Forgac +homepage: https://github.com/tomasforgacbch/bitbox-flutter environment: - sdk: ">=2.1.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - http: ^0.12.0+1 - bip39: ^1.0.3 - pointycastle: ^1.0.1 - bip32: ^1.0.5 - hex: ^0.1.2 - bs58check: ^1.0.1 - fixnum: ^0.10.9 + http: ^0.13.5 + bip39: + git: + url: https://github.com/zapit-io/bip39.git + ref: master + pointycastle: ^3.6.2 + bip32: + git: + url: https://github.com/zapit-io/bip32-dart.git + ref: master + hex: ^0.2.0 + fixnum: ^1.0.1 + meta: ^1.8.0 + buffer: ^1.1.1 + bs58check: ^1.0.2 + slp_parser: + git: + url: https://github.com/zapit-io/slp-parser.dart.git + ref: master + slp_mdm: + git: + url: https://github.com/zapit-io/slp-metadatamaker.dart.git + ref: master + +dependency_overrides: + crypto: ^3.0.0 + hex: ^0.2.0 dev_dependencies: flutter_test: @@ -27,7 +47,6 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg @@ -38,7 +57,6 @@ flutter: # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. - # To add custom fonts to your package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a diff --git a/test/bitbox_test.dart b/test/bitbox_test.dart index 1c755f9..2b7d716 100644 --- a/test/bitbox_test.dart +++ b/test/bitbox_test.dart @@ -16,11 +16,11 @@ void main() { // If these are false, the transactions will be only built and compared to the output generated by bitbox js // You can turn these on separately - const BROADCAST_TESTNET_TRANSACTION = false; + const BROADCAST_TESTNET_TRANSACTION = true; const BROADCAST_MAINNET_TRANSACTION = true; // Data generated by the original bitbox library - Map testData; + Map? testData; // Placeholder for data about master, account and childnodes for both networks Map nodeData = {"mainnet" : {}, "testnet" : {}}; @@ -38,7 +38,7 @@ void main() { testData = jsonDecode(testDataJson); // create a seed from the mnemonic - final seed = Bitbox.Mnemonic.toSeed(testData["mnemonic"]); + final seed = Bitbox.Mnemonic.toSeed(testData!["mnemonic"]); // create master nodes for both networks and store their master keys for (int i = 0; i < networks.length; i++) { @@ -48,8 +48,8 @@ void main() { final masterXpub = nodeData[network]["master_node"].toXPub(); // compare the result with the js test data - expect(masterXPriv, testData[network]["master_xpriv"]); - expect(masterXpub, testData[network]["master_xpub"]); + expect(masterXPriv, testData![network]["master_xpriv"]); + expect(masterXpub, testData![network]["master_xpub"]); } }); @@ -58,13 +58,13 @@ void main() { // generate the nodes for both networks for (int i = 0; i < networks.length; i++) { final network = networks[i]; - nodeData[network]["account_node"] = nodeData[network]["master_node"].derivePath(testData["account_path"]); + nodeData[network]["account_node"] = nodeData[network]["master_node"].derivePath(testData!["account_path"]); final accountXPriv = nodeData[network]["account_node"].toXPriv(); final accountXPub = nodeData[network]["account_node"].toXPub(); // compare the master private and public key with the original testing data - expect(accountXPriv, testData[network]["account_xpriv"]); - expect(accountXPub, testData[network]["account_xpub"]); + expect(accountXPriv, testData![network]["account_xpriv"]); + expect(accountXPub, testData![network]["account_xpub"]); } }); @@ -80,7 +80,7 @@ void main() { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { // generate the child node and extract its private key final childNode = nodeData[network]["account_node"].derive(childTestData["index"]); final childPrivateKey = childNode.privateKey; @@ -94,7 +94,7 @@ void main() { test('Generating child nodes and legacy addresses', () { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { final childNode = nodeData[network]["account_node"].derive(childTestData["index"]); final childLegacy = childNode.toLegacyAddress(); @@ -106,7 +106,7 @@ void main() { test('Generating child nodes and cash addresses', () { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { final childNode = nodeData[network]["account_node"].derive(childTestData["index"]); final childCashAddr = childNode.toCashAddress(); @@ -119,7 +119,7 @@ void main() { test('Converting cashAddr to legacy', () { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { final cashAddr = childTestData["cashAddress"]; expect(Bitbox.Address.toLegacyAddress(cashAddr), childTestData["toLegacy"]); @@ -130,7 +130,7 @@ void main() { test('Converting legacy to cashAddr', () { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { final legacy = childTestData["legacy"]; expect(Bitbox.Address.toCashAddress(legacy), childTestData["toCashAddr"]); @@ -139,21 +139,21 @@ void main() { }); // Placeholder to store addresses with balance for which to fetch utxos later - final utxosToFetch = >{}; + final utxosToFetch = >{}; test('Fetching address details', () async { for (int i = 0; i < networks.length; i++) { final network = networks[i]; - utxosToFetch[network] = []; + utxosToFetch[network] = []; // set rest url based on which network is being tested Bitbox.Bitbox.setRestUrl(restUrl: network == "mainnet" ? Bitbox.Bitbox.restUrl : Bitbox.Bitbox.trestUrl); // Placeholder for test addresses to fetch the details off - List testAddresses = []; + List testAddresses = []; // Accumulate the list of all addresses from the test file - testData[network]["child_nodes"].forEach((childTestData) { + testData![network]["child_nodes"].forEach((childTestData) { testAddresses.add(childTestData["cashAddress"]); }); @@ -168,7 +168,7 @@ void main() { // store all addresses with non-zero confirmed balance detailsAll.forEach((addressDetails) { if (addressDetails["balance"] > 0) { - utxosToFetch[network].add(addressDetails["cashAddress"]); + utxosToFetch[network]!.add(addressDetails["cashAddress"]); } }); } @@ -185,7 +185,7 @@ void main() { utxos[network] = []; // If there were addresses with non-zero balance for this network, fetch their utxos - if (utxosToFetch[network].length > 0) { + if (utxosToFetch[network]!.length > 0) { // set the appropriate rest api url Bitbox.Bitbox.setRestUrl(restUrl: network == "mainnet" ? Bitbox.Bitbox.restUrl : Bitbox.Bitbox.trestUrl); @@ -193,9 +193,9 @@ void main() { utxos[network] = await Bitbox.Address.utxo(utxosToFetch[network]) as List; // go through the list of the returned utxos - utxos[network].forEach((addressUtxo) { + utxos[network]!.forEach((addressUtxo) { // go through each address in the testing data to test if the utxos match - testData[network]["child_nodes"].forEach((childNode) { + testData![network]["child_nodes"].forEach((childNode) { if (childNode["cashAddress"] == addressUtxo["cashAddress"]) { for (int i = 0; i < addressUtxo["utxos"].length; i++) { Bitbox.Utxo utxo = addressUtxo["utxos"][i]; @@ -228,8 +228,8 @@ void main() { final builder = Bitbox.Bitbox.transactionBuilder(testnet: network == "testnet"); // go through the list of utxos accumulated in the previous test - utxos[network].forEach((addressUtxos) { - testData[network]["child_nodes"].forEach((childNode) { + utxos[network]!.forEach((addressUtxos) { + testData![network]["child_nodes"].forEach((childNode) { if (childNode["cashAddress"] == addressUtxos["cashAddress"]) { addressUtxos["utxos"].forEach((Bitbox.Utxo utxo) { // add the utxo as an input for the transaction @@ -242,7 +242,7 @@ void main() { "original_amount": utxo.satoshis }); - totalBalance += utxo.satoshis; + totalBalance += utxo.satoshis!; }); } }); @@ -257,7 +257,7 @@ void main() { final sendAmount = totalBalance - fee; // add the ouput based on the address provided in the testing data - builder.addOutput(testData[network]["output_address"], sendAmount); + builder.addOutput(testData![network]["output_address"], sendAmount); // sign all inputs signatures.forEach((signature) { @@ -268,7 +268,7 @@ void main() { final tx = builder.build(); // compare the transaction raw hex with the output from the original bitbox - expect(tx.toHex(), testData[network]["testing_tx_hex"]); + expect(tx.toHex(), testData![network]["testing_tx_hex"]); // add the raw transaction to the list to be (optionally) broadcastd rawTx[network] = tx.toHex(); diff --git a/test/readmemdtest.dart b/test/readmemdtest.dart deleted file mode 100644 index 227e36d..0000000 --- a/test/readmemdtest.dart +++ /dev/null @@ -1,113 +0,0 @@ -// This is to make sure the code in readme.MD works - -import 'package:bitbox/bitbox.dart' as Bitbox; - -void main() async { -// set this to false to use mainnet -final testnet = false; - -// After running the code for the first time, depositing an amount to the address displayed in the console, -// and waiting for confirmation, paste the generated mnemonic here, -// so the code continues below with address withdrawal -String mnemonic = ""; - -if (mnemonic == "") { - // generate 12-word (128bit) mnemonic - mnemonic = Bitbox.Mnemonic.generate(); - - print(mnemonic); -} - -// generate a seed from mnemonic -final seed = Bitbox.Mnemonic.toSeed(mnemonic); - -// create an instance of Bitbox.HDNode for mainnet -final masterNode = Bitbox.HDNode.fromSeed(seed, testnet); - -// This format is compatible with Bitcoin.com wallet. -// Other wallets use Change to m/44'/145'/0'/0 -final accountDerivationPath = "m/44'/0'/0'/0"; - -// create an account node using the provided derivation path -final accountNode = masterNode.derivePath(accountDerivationPath); - -// get account's extended private key -final accountXPriv = accountNode.toXPriv(); - -// create a Bitbox.HDNode instance of the first child in this account -final childNode = accountNode.derive(0); - -// get an address of the child -final address = childNode.toCashAddress(); - -// if you are using testnet, set the appropriate rest api url before making -// any API calls (like getting address or transaction details or broadcasting a transaction -if (testnet) { - Bitbox.Bitbox.setRestUrl(restUrl: Bitbox.Bitbox.trestUrl); -} - -// get address details -final addressDetails = await Bitbox.Address.details(address); - -print(addressDetails); - -// If there is a confirmed balance, attempt to withdraw it to the address defined below -if (addressDetails["balance"] > 0) { - final builder = Bitbox.Bitbox.transactionBuilder(testnet: testnet); - - // retrieve address' utxos from the rest api - final utxos = await Bitbox.Address.utxo(address) as List; - - // placeholder for input signatures - final signatures = []; - - // placeholder for total input balance - int totalBalance = 0; - - // iterate through the list of address utxos and use them as inputs for the withdrawal transaction - utxos.forEach((Bitbox.Utxo utxo) { - // add the utxo as an input for the transaction - builder.addInput(utxo.txid, utxo.vout); - - // add a signature to the list to be used later - signatures.add({ - "vin": signatures.length, - "key_pair": childNode.keyPair, - "original_amount": utxo.satoshis - }); - - totalBalance += utxo.satoshis; - }); - - // set an address to send the remaining balance to - final outputAddress = ""; - - // if there is an unspent balance, create a spending transaction - if (totalBalance > 0 && outputAddress != "") { - // calculate the fee based on number of inputs and one expected output - final fee = Bitbox.BitcoinCash.getByteCount(signatures.length, 1); - - // calculate how much balance will be left over to spend after the fee - final sendAmount = totalBalance - fee; - - // add the output based on the address provided in the testing data - builder.addOutput(outputAddress, sendAmount); - - // sign all inputs - signatures.forEach((signature) { - builder.sign(signature["vin"], signature["key_pair"], signature["original_amount"]); - }); - - // build the transaction - final tx = builder.build(); - - // broadcast the transaction - final txid = await Bitbox.RawTransactions.sendRawTransaction(tx.toHex()); - - // Yatta! - print("Transaction broadcasted: $txid"); - } else if (totalBalance > 0) { - print("Enter an output address to test withdrawal transaction"); - } -} -}