diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f92ccdb..0be0211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Use Node.js 20.12.0 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 5721218..d7f0571 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -9,6 +9,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: 'Dependency Review' uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 66db525..11be288 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -18,25 +18,25 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: results.sarif diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 7af02a9..3b2f00c 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -10,7 +10,7 @@ jobs: permissions: security-events: write steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Run Slither uses: crytic/slither-action@6ef3a33e56de4e8f59488cf60858b5c1bf4967c0 # v0.3.0 @@ -21,6 +21,6 @@ jobs: target: contracts/ - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@85b07cf1e13dd512be7c27c37a33c5864c252fcc # v2 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: ${{ steps.slither.outputs.sarif }} diff --git a/contracts/BtcUtils.sol b/contracts/BtcUtils.sol index 9e4e242..12f6711 100644 --- a/contracts/BtcUtils.sol +++ b/contracts/BtcUtils.sol @@ -13,19 +13,26 @@ library BtcUtils { uint8 private constant MAX_COMPACT_SIZE_LENGTH = 252; uint8 private constant MAX_BYTES_USED_FOR_COMPACT_SIZE = 8; + uint private constant HASH160_SIZE = 20; + uint private constant SHA256_SIZE = 32; + uint private constant TAPROOT_PUBKEY_SIZE = 32; + uint8 private constant OUTPOINT_SIZE = 36; uint8 private constant OUTPUT_VALUE_SIZE = 8; - uint8 private constant PUBKEY_HASH_SIZE = 20; uint8 private constant PUBKEY_HASH_START = 3; bytes1 private constant PUBKEY_HASH_MAINNET_BYTE = 0x00; bytes1 private constant PUBKEY_HASH_TESTNET_BYTE = 0x6f; - uint8 private constant SCRIPT_HASH_SIZE = 20; uint8 private constant SCRIPT_HASH_START = 2; bytes1 private constant SCRIPT_HASH_MAINNET_BYTE = 0x05; bytes1 private constant SCRIPT_HASH_TESTNET_BYTE = 0xc4; + uint private constant BECH32_WORD_SIZE = 5; + uint private constant BYTE_SIZE = 8; + + bytes1 private constant WITNESS_VERSION_0 = 0x00; + bytes1 private constant WITNESS_VERSION_1 = 0x01; /** @@ -39,6 +46,10 @@ library BtcUtils { uint256 totalSize; } + function version() external pure returns (string memory) { + return "0.2.1"; + } + /// @notice Parse a raw transaction to get an array of its outputs in a structured representation /// @param rawTx the raw transaction /// @return An array of `TxRawOutput` with the outputs of the transaction @@ -103,7 +114,15 @@ library BtcUtils { if (isP2SHOutput(outputScript)) { return parsePayToScriptHash(outputScript, mainnet); } - // TODO add here P2WPKH, P2WSH and P2TR + if (isP2WPKHOutput(outputScript)) { + return parsePayToWitnessPubKeyHash(outputScript); + } + if (isP2WSHOutput(outputScript)) { + return parsePayToWitnessScriptHash(outputScript); + } + if (isP2TROutput(outputScript)) { + return parsePayToTaproot(outputScript); + } revert("Unsupported script type"); } @@ -111,10 +130,10 @@ library BtcUtils { /// @param pkScript the fragment of the raw transaction containing the raw output script /// @return Whether the script has a pay-to-public-key-hash output structure or not function isP2PKHOutput(bytes memory pkScript) public pure returns (bool) { - return pkScript.length == 25 && + return pkScript.length == 5 + HASH160_SIZE && pkScript[0] == OpCodes.OP_DUP && pkScript[1] == OpCodes.OP_HASH160 && - uint8(pkScript[2]) == PUBKEY_HASH_SIZE && + uint8(pkScript[2]) == HASH160_SIZE && pkScript[23] == OpCodes.OP_EQUALVERIFY && pkScript[24] == OpCodes.OP_CHECKSIG; } @@ -123,12 +142,40 @@ library BtcUtils { /// @param pkScript the fragment of the raw transaction containing the raw output script /// @return Whether the script has a pay-to-script-hash output structure or not function isP2SHOutput(bytes memory pkScript) public pure returns (bool) { - return pkScript.length == 23 && + return pkScript.length == 3 + HASH160_SIZE && pkScript[0] == OpCodes.OP_HASH160 && - uint8(pkScript[1]) == SCRIPT_HASH_SIZE && + uint8(pkScript[1]) == HASH160_SIZE && pkScript[22] == OpCodes.OP_EQUAL; } + /// @notice Check if a raw output script is a pay-to-witness-pubkey-hash output + /// @param pkScript the fragment of the raw transaction containing the raw output script + /// @return Whether the script has a pay-to-witness-pubkey-hash output structure or not + function isP2WPKHOutput(bytes memory pkScript) public pure returns (bool) { + return pkScript.length == 2 + HASH160_SIZE && + pkScript[0] == OpCodes.OP_0 && + uint8(pkScript[1]) == HASH160_SIZE; + } + + /// @notice Check if a raw output script is a pay-to-witness-script-hash output + /// @param pkScript the fragment of the raw transaction containing the raw output script + /// @return Whether the script has a pay-to-witness-script-hash output structure or not + function isP2WSHOutput(bytes memory pkScript) public pure returns (bool) { + return pkScript.length == 2 + SHA256_SIZE && + pkScript[0] == OpCodes.OP_0 && + uint8(pkScript[1]) == SHA256_SIZE; + } + + /// @notice Check if a raw output script is a pay-to-taproot output + /// @notice Reference for implementation: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki + /// @param pkScript the fragment of the raw transaction containing the raw output script + /// @return Whether the script has a pay-to-taproot output structure or not + function isP2TROutput(bytes memory pkScript) public pure returns (bool) { + return pkScript.length == 2 + TAPROOT_PUBKEY_SIZE && + pkScript[0] == OpCodes.OP_1 && + uint8(pkScript[1]) == TAPROOT_PUBKEY_SIZE; + } + /// @notice Parse a raw pay-to-public-key-hash output script to get the corresponding address, /// the resulting byte array doesn't include the checksum bytes of the base58check encoding at /// the end @@ -138,8 +185,8 @@ library BtcUtils { function parsePayToPubKeyHash(bytes calldata outputScript, bool mainnet) public pure returns (bytes memory) { require(isP2PKHOutput(outputScript), "Script hasn't the required structure"); - bytes memory destinationAddress = new bytes(PUBKEY_HASH_SIZE + 1); - for(uint8 i = PUBKEY_HASH_START; i < PUBKEY_HASH_SIZE + PUBKEY_HASH_START; i++) { + bytes memory destinationAddress = new bytes(HASH160_SIZE + 1); + for(uint8 i = PUBKEY_HASH_START; i < HASH160_SIZE + PUBKEY_HASH_START; i++) { destinationAddress[i - PUBKEY_HASH_START + 1] = outputScript[i]; } @@ -156,8 +203,8 @@ library BtcUtils { function parsePayToScriptHash(bytes calldata outputScript, bool mainnet) public pure returns (bytes memory) { require(isP2SHOutput(outputScript), "Script hasn't the required structure"); - bytes memory destinationAddress = new bytes(SCRIPT_HASH_SIZE + 1); - for(uint8 i = SCRIPT_HASH_START; i < SCRIPT_HASH_SIZE + SCRIPT_HASH_START; i++) { + bytes memory destinationAddress = new bytes(HASH160_SIZE + 1); + for(uint8 i = SCRIPT_HASH_START; i < HASH160_SIZE + SCRIPT_HASH_START; i++) { destinationAddress[i - SCRIPT_HASH_START + 1] = outputScript[i]; } @@ -165,6 +212,54 @@ library BtcUtils { return destinationAddress; } + /// @notice Parse a raw pay-to-witness-pubkey-hash output script to get the corresponding address words, + /// the resulting words are only the data part of the bech32 encoding and doesn't include the HRP + /// @param outputScript the fragment of the raw transaction containing the raw output script + /// @return The address bech32 words generated using the pubkey hash + function parsePayToWitnessPubKeyHash(bytes calldata outputScript) public pure returns (bytes memory) { + require(isP2WPKHOutput(outputScript), "Script hasn't the required structure"); + uint length = 1 + total5BitWords(HASH160_SIZE); + bytes memory result = new bytes(length); + result[0] = WITNESS_VERSION_0; + bytes memory words = to5BitWords(outputScript[2:]); + for (uint i = 1; i < length; i++) { + result[i] = words[i - 1]; + } + return result; + } + + /// @notice Parse a raw pay-to-witness-script-hash output script to get the corresponding address words, + /// the resulting words are only the data part of the bech32 encoding and doesn't include the HRP + /// @param outputScript the fragment of the raw transaction containing the raw output script + /// @return The address bech32 words generated using the script hash + function parsePayToWitnessScriptHash(bytes calldata outputScript) public pure returns (bytes memory) { + require(isP2WSHOutput(outputScript), "Script hasn't the required structure"); + uint length = 1 + total5BitWords(SHA256_SIZE); + bytes memory result = new bytes(length); + result[0] = WITNESS_VERSION_0; + bytes memory words = to5BitWords(outputScript[2:]); + for (uint i = 1; i < length; i++) { + result[i] = words[i - 1]; + } + return result; + } + + /// @notice Parse a raw pay-to-taproot output script to get the corresponding address words, + /// the resulting words are only the data part of the bech32m encoding and doesn't include the HRP + /// @param outputScript the fragment of the raw transaction containing the raw output script + /// @return The address bech32m words generated using the taproot pubkey hash + function parsePayToTaproot(bytes calldata outputScript) public pure returns (bytes memory) { + require(isP2TROutput(outputScript), "Script hasn't the required structure"); + uint length = 1 + total5BitWords(TAPROOT_PUBKEY_SIZE); + bytes memory result = new bytes(length); + result[0] = WITNESS_VERSION_1; + bytes memory words = to5BitWords(outputScript[2:]); + for (uint i = 1; i < length; i++) { + result[i] = words[i - 1]; + } + return result; + } + /// @notice Parse a raw null-data output script to get its content /// @param outputScript the fragment of the raw transaction containing the raw output script /// @return The content embedded inside the script @@ -271,4 +366,36 @@ library BtcUtils { } return result; } + + /// @notice Referece for implementation: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + function to5BitWords(bytes memory byteArray) private pure returns(bytes memory) { + uint8 MAX_VALUE = 31; + + uint currentValue = 0; + uint bitCount = 0; + uint8 resultIndex = 0; + bytes memory result = new bytes(total5BitWords(byteArray.length)); + + for (uint i = 0; i < byteArray.length; ++i) { + currentValue = (currentValue << BYTE_SIZE) | uint8(byteArray[i]); + bitCount += BYTE_SIZE; + while (bitCount >= BECH32_WORD_SIZE) { + bitCount -= BECH32_WORD_SIZE; + // this mask ensures that the result will always have 5 bits + result[resultIndex] = bytes1(uint8((currentValue >> bitCount) & MAX_VALUE)); + resultIndex++; + } + } + + if (bitCount > 0) { + result[resultIndex] = bytes1(uint8((currentValue << (BECH32_WORD_SIZE - bitCount)) & MAX_VALUE)); + } + return result; + } + + function total5BitWords(uint numberOfBytes) private pure returns(uint) { + uint total = (numberOfBytes * BYTE_SIZE) / BECH32_WORD_SIZE; + bool extraWord = (numberOfBytes * BYTE_SIZE) % BECH32_WORD_SIZE == 0; + return total + (extraWord? 0 : 1); + } } \ No newline at end of file diff --git a/contracts/OpCodes.sol b/contracts/OpCodes.sol index bc574c2..f14b599 100644 --- a/contracts/OpCodes.sol +++ b/contracts/OpCodes.sol @@ -9,4 +9,7 @@ library OpCodes { bytes1 public constant OP_CHECKSIG = 0xac; bytes1 public constant OP_RETURN = 0x6a; bytes1 public constant OP_EQUAL = 0x87; + + bytes1 public constant OP_0 = 0x00; + bytes1 public constant OP_1 = 0x51; } diff --git a/package-lock.json b/package-lock.json index b8e6d6e..31cd386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@rsksmart/btc-transaction-solidity-helper", - "version": "0.1.1", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rsksmart/btc-transaction-solidity-helper", - "version": "0.1.1", + "version": "0.2.1", "license": "ISC", "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@nomiclabs/hardhat-solhint": "^3.0.1", + "bech32": "^2.0.0", "bs58check": "^3.0.1", "hardhat": "^2.17.0", "solidity-coverage": "^0.8.12" @@ -528,6 +529,13 @@ "ws": "7.4.6" } }, + "node_modules/@ethersproject/providers/node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "peer": true + }, "node_modules/@ethersproject/providers/node_modules/ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", @@ -970,81 +978,81 @@ } }, "node_modules/@nomicfoundation/edr": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.2.tgz", - "integrity": "sha512-yPUegN3sTWiAkRatCmGRkuvMgD9HSSpivl2ebAqq0aU2xgC7qmIO+YQPxQ3Z46MUoi7MrTf4e6GpbT4S/8x0ew==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.4.tgz", + "integrity": "sha512-YgrSuT3yo5ZQkbvBGqQ7hG+RDvz3YygSkddg4tb1Z0Y6pLXFzwrcEwWaJCFAVeeZxdxGfCgGMUYgRVneK+WXkw==", "dev": true, "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.6.2", - "@nomicfoundation/edr-darwin-x64": "0.6.2", - "@nomicfoundation/edr-linux-arm64-gnu": "0.6.2", - "@nomicfoundation/edr-linux-arm64-musl": "0.6.2", - "@nomicfoundation/edr-linux-x64-gnu": "0.6.2", - "@nomicfoundation/edr-linux-x64-musl": "0.6.2", - "@nomicfoundation/edr-win32-x64-msvc": "0.6.2" + "@nomicfoundation/edr-darwin-arm64": "0.6.4", + "@nomicfoundation/edr-darwin-x64": "0.6.4", + "@nomicfoundation/edr-linux-arm64-gnu": "0.6.4", + "@nomicfoundation/edr-linux-arm64-musl": "0.6.4", + "@nomicfoundation/edr-linux-x64-gnu": "0.6.4", + "@nomicfoundation/edr-linux-x64-musl": "0.6.4", + "@nomicfoundation/edr-win32-x64-msvc": "0.6.4" }, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.2.tgz", - "integrity": "sha512-o4A9SaPlxJ1MS6u8Ozqq7Y0ri2XO0jASw+qkytQyBYowNFNReoGqVSs7SCwenYCDiN+1il8+M0VAUq7wOovnCQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.4.tgz", + "integrity": "sha512-QNQErISLgssV9+qia8sIjRANqtbW8snSDvjspixT/kSQ5ZSGxxctTg7x72wPSrcu8+EBEveIe5uqENIp5GH8HQ==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.2.tgz", - "integrity": "sha512-WG8BeG2eR3rFC+2/9V1hoPGW7tmNRUcuztdHUijO1h2flRsf2YWv+kEHO+EEnhGkEbgBUiwOrwlwlSMxhe2cGA==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.4.tgz", + "integrity": "sha512-cjVmREiwByyc9+oGfvAh49IAw+oVJHF9WWYRD+Tm/ZlSpnEVWxrGNBak2bd/JSYjn+mZE7gmWS4SMRi4nKaLUg==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.2.tgz", - "integrity": "sha512-wvHaTmOwuPjRIOqBB+paI3RBdNlG8f3e1F2zWj75EdeWwefimPzzFUs05JxOYuPO0JhDQIn2tbYUgdZbBQ+mqg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.4.tgz", + "integrity": "sha512-96o9kRIVD6W5VkgKvUOGpWyUGInVQ5BRlME2Fa36YoNsRQMaKtmYJEU0ACosYES6ZTpYC8U5sjMulvPtVoEfOA==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.2.tgz", - "integrity": "sha512-UrOAxnsywUcEngQM2ZxIuucci0VX29hYxX7jcpwZU50HICCjxNsxnuXYPxv+IM+6gbhBY1FYvYJGW4PJcP1Nyw==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.4.tgz", + "integrity": "sha512-+JVEW9e5plHrUfQlSgkEj/UONrIU6rADTEk+Yp9pbe+mzNkJdfJYhs5JYiLQRP4OjxH4QOrXI97bKU6FcEbt5Q==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.2.tgz", - "integrity": "sha512-gYxlPLi7fkNcmDmCwZWQa5eOfNcTDundE+TWjpyafxLAjodQuKBD4I0p4XbnuocHjoBEeNzLWdE5RShbZEXEJA==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.4.tgz", + "integrity": "sha512-nzYWW+fO3EZItOeP4CrdMgDXfaGBIBkKg0Y/7ySpUxLqzut40O4Mb0/+quqLAFkacUSWMlFp8nsmypJfOH5zoA==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.2.tgz", - "integrity": "sha512-ev5hy9wmiHZi1GKQ1l6PJ2+UpsUh+DvK9AwiCZVEdaicuhmTfO6fdL4szgE4An8RU+Ou9DeiI1tZcq6iw++Wuw==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.4.tgz", + "integrity": "sha512-QFRoE9qSQ2boRrVeQ1HdzU+XN7NUgwZ1SIy5DQt4d7jCP+5qTNsq8LBNcqhRBOATgO63nsweNUhxX/Suj5r1Sw==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.2.tgz", - "integrity": "sha512-2ZXVVcmdmEeX0Hb3IAurHUjgU3H1GIk9h7Okosdjgl3tl+BaNHxi84Us+DblynO1LRj8nL/ATeVtSfBuW3Z1vw==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.4.tgz", + "integrity": "sha512-2yopjelNkkCvIjUgBGhrn153IBPLwnsDeNiq6oA0WkeM8tGmQi4td+PGi9jAriUDAkc59Yoi2q9hYA6efiY7Zw==", "dev": true, "engines": { "node": ">= 18" @@ -1116,9 +1124,9 @@ } }, "node_modules/@nomicfoundation/hardhat-chai-matchers": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.1.tgz", - "integrity": "sha512-qWKndseO8IPt8HiVamgEAutcBOYtX7/O6NPfe7uMNWxY2ywWaiWjDcRFuYYqxrZOMyQZl6ZuiHxbaRNctTUgLw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.8.tgz", + "integrity": "sha512-Z5PiCXH4xhNLASROlSUOADfhfpfhYO6D7Hn9xp8PddmHey0jq704cr6kfU8TRrQ4PUZbpfsZadPj+pCfZdjPIg==", "dev": true, "peer": true, "dependencies": { @@ -1135,9 +1143,9 @@ } }, "node_modules/@nomicfoundation/hardhat-ethers": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.4.tgz", - "integrity": "sha512-k9qbLoY7qn6C6Y1LI0gk2kyHXil2Tauj4kGzQ8pgxYXIGw8lWn8tuuL72E11CrlKaXRUvOgF0EXrv/msPI2SbA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.8.tgz", + "integrity": "sha512-zhOZ4hdRORls31DTOqg+GmEZM0ujly8GGIuRY7t7szEk2zW/arY1qDug/py8AEktT00v5K+b6RvbVog+va51IA==", "dev": true, "peer": true, "dependencies": { @@ -1150,9 +1158,9 @@ } }, "node_modules/@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz", - "integrity": "sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.12.tgz", + "integrity": "sha512-xTNQNI/9xkHvjmCJnJOTyqDSl8uq1rKb2WOVmixQxFtRd7Oa3ecO8zM0cyC2YmOK+jHB9WPZ+F/ijkHg1CoORA==", "dev": true, "peer": true, "dependencies": { @@ -1390,9 +1398,9 @@ } }, "node_modules/@nomiclabs/hardhat-solhint": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-solhint/-/hardhat-solhint-3.0.1.tgz", - "integrity": "sha512-GqDoStxL1aA9hZul9HRdk+3eEZ/XLBwvIlz3/EJUusUsoadNYEnio9aYOsBeNeZWo+B/Fd3EgMXgbf9wWQLyRA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-solhint/-/hardhat-solhint-3.1.0.tgz", + "integrity": "sha512-5jNiYwLuiHZ2B11Ds4U5jH+DR565PqpsdbXml6iYfqMguyJb+ulU2rt58+hprNhoFKZds8cOlf9FaoWvA9KqkA==", "dev": true, "dependencies": { "solhint": "^3.4.0" @@ -1647,9 +1655,9 @@ "peer": true }, "node_modules/@typechain/ethers-v6": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.4.2.tgz", - "integrity": "sha512-LPC4BBknGkWGR1TLM0d19zZ9/iXIyp2tf6+TDYMYCSbxoaP0F3jNvKVMboU1gDfr2MHaPB+fE/7ExLQ5t9RDwg==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.4.3.tgz", + "integrity": "sha512-TrxBsyb4ryhaY9keP6RzhFCviWYApcLCIRMPyWaKp2cZZrfaM3QBoxXTnw/eO4+DAY3l+8O0brNW0WgeQeOiDA==", "dev": true, "peer": true, "dependencies": { @@ -1658,24 +1666,24 @@ }, "peerDependencies": { "ethers": "6.x", - "typechain": "^8.3.0", + "typechain": "^8.3.1", "typescript": ">=4.7.0" } }, "node_modules/@typechain/hardhat": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-8.0.2.tgz", - "integrity": "sha512-wn8LPFU601jrIs9qjeuXbO2f2lcNBKW9Uuvj4MdTHpHM71Hv/6CuSHQxk941w1c5QYJxW0nJT15M9sVe33esqg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-8.0.3.tgz", + "integrity": "sha512-MytSmJJn+gs7Mqrpt/gWkTCOpOQ6ZDfRrRT2gtZL0rfGe4QrU4x9ZdW15fFbVM/XTa+5EsKiOMYXhRABibNeng==", "dev": true, "peer": true, "dependencies": { "fs-extra": "^9.1.0" }, "peerDependencies": { - "@typechain/ethers-v6": "^0.4.2", + "@typechain/ethers-v6": "^0.4.3", "ethers": "^6.1.0", "hardhat": "^2.9.9", - "typechain": "^8.3.0" + "typechain": "^8.3.1" } }, "node_modules/@types/bn.js": { @@ -2129,11 +2137,10 @@ "dev": true }, "node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", - "dev": true, - "peer": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -3724,14 +3731,14 @@ } }, "node_modules/hardhat": { - "version": "2.22.12", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.12.tgz", - "integrity": "sha512-yok65M+LsOeTBHQsjg//QreGCyrsaNmeLVzhTFqlOvZ4ZE5y69N0wRxH1b2BC9dGK8S8OPUJMNiL9X0RAvbm8w==", + "version": "2.22.15", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.15.tgz", + "integrity": "sha512-BpTGa9PE/sKAaHi4s/S1e9WGv63DR1m7Lzfd60C8gSEchDPfAJssVRSq0MZ2v2k76ig9m0kHAwVLf5teYwu/Mw==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/edr": "^0.6.1", + "@nomicfoundation/edr": "^0.6.4", "@nomicfoundation/ethereumjs-common": "4.0.4", "@nomicfoundation/ethereumjs-tx": "5.0.4", "@nomicfoundation/ethereumjs-util": "9.0.4", @@ -3791,9 +3798,9 @@ } }, "node_modules/hardhat-gas-reporter": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.9.tgz", - "integrity": "sha512-INN26G3EW43adGKBNzYWOlI3+rlLnasXTwW79YNnUhXPDa+yHESgt639dJEs37gCjhkbNKcRRJnomXEuMFBXJg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.10.tgz", + "integrity": "sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==", "dev": true, "peer": true, "dependencies": { @@ -5568,20 +5575,47 @@ "dev": true }, "node_modules/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", "dev": true, "hasInstallScript": true, "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, + "node_modules/secp256k1/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/secp256k1/node_modules/elliptic": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5914,9 +5948,9 @@ } }, "node_modules/solidity-coverage": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.12.tgz", - "integrity": "sha512-8cOB1PtjnjFRqOgwFiD8DaUsYJtVJ6+YdXQtSZDrLGf8cdhhh8xzTtGzVTGeBf15kTv0v7lYPJlV/az7zLEPJw==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.13.tgz", + "integrity": "sha512-RiBoI+kF94V3Rv0+iwOj3HQVSqNzA9qm/qDP1ZDXK5IX0Cvho1qiz8hAXTsAo6KOIUeP73jfscq0KlLqVxzGWA==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.0.9", @@ -6594,9 +6628,9 @@ } }, "node_modules/typechain": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.0.tgz", - "integrity": "sha512-AxtAYyOA7f2p28/JkcqrF+gnzam94VNTIbXcaUKodkrKzMX6P/XqBaP6d/OPuBZOi0WgOOmkg1zOSojX8uGkOg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", "dev": true, "peer": true, "dependencies": { diff --git a/package.json b/package.json index a549af1..c7e6a0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/btc-transaction-solidity-helper", - "version": "0.1.1", + "version": "0.2.1", "description": "Solidity library with functions to work with Bitcoin transactions inside smart contracts", "main": "contracts", "files": [ @@ -37,6 +37,7 @@ "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@nomiclabs/hardhat-solhint": "^3.0.1", + "bech32": "^2.0.0", "bs58check": "^3.0.1", "hardhat": "^2.17.0", "solidity-coverage": "^0.8.12" diff --git a/test/BtcUtils.ts b/test/BtcUtils.ts index 3edb22f..32e5f3d 100644 --- a/test/BtcUtils.ts +++ b/test/BtcUtils.ts @@ -1,10 +1,13 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BtcUtils as BtcUtilsLib } from "../typechain-types"; +import { bech32, bech32m } from "bech32"; import * as bs58check from "bs58check"; import * as p2kph from "./test-data/p2pkh-outputs"; import * as p2sh from "./test-data/p2sh-outputs"; -import * as b32 from "./test-data/bech32-outputs"; +import * as p2wpkh from "./test-data/p2wpkh-outputs"; +import * as p2wsh from "./test-data/p2wsh-outputs"; +import * as taproot from "./test-data/p2tr-outputs"; type RawTxOutput = { value:string @@ -445,13 +448,13 @@ describe("BtcUtils", function () { }, { type: 'P2WSH', - raw: '0x0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000', + raw: '0x0100000001ac7de87ae01110ed6803bb49279886a89cf473bc0bdd48cae960aed59b21ac77000000006b483045022100e12ddb2662bd3c44d482eef808a6fcc84805470c147a477d3b8c9b52b5608be3022060639ddb5690b340f51f935a4a8a1a4151116714c9f84e72791e482840525ca30121029d9286a9c0e8b9e8182d5cc18f3848834c906ed6c6c0b49c86b822f0ed67c9baffffffff016003b80700000000220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4c00000000', outputs: [ { - value: 129200000n, - pkScript: '0x76a914bbef244bcad13cffb68b5cef3017c7423675552288ac', - scriptSize: 25, - totalSize: 34n + value: 129500000n, + pkScript: '0x0020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4c', + scriptSize: 34, + totalSize: 43n } ] } @@ -477,10 +480,14 @@ describe("BtcUtils", function () { }); it('return false if the script is not a P2PKH script', async () => { - const testCases = b32.testnetOutputs - .concat(b32.mainnetOutputs) + const testCases = taproot.testnetOutputs + .concat(taproot.mainnetOutputs) .concat(p2sh.testnetOutputs) - .concat(p2sh.mainnetOutputs); + .concat(p2sh.mainnetOutputs) + .concat(p2wsh.testnetOutputs) + .concat(p2wsh.mainnetOutputs) + .concat(p2wpkh.testnetOutputs) + .concat(p2wpkh.mainnetOutputs); for (const output of testCases) { const result = await BtcUtils.isP2PKHOutput(output.script); expect(result).to.be.false; @@ -497,10 +504,14 @@ describe("BtcUtils", function () { }); it('return false if the script is not a P2SH script', async () => { - const testCases = b32.testnetOutputs - .concat(b32.mainnetOutputs) + const testCases = taproot.testnetOutputs + .concat(taproot.mainnetOutputs) .concat(p2kph.testnetOutputs) - .concat(p2kph.mainnetOutputs); + .concat(p2kph.mainnetOutputs) + .concat(p2wsh.testnetOutputs) + .concat(p2wsh.mainnetOutputs) + .concat(p2wpkh.testnetOutputs) + .concat(p2wpkh.mainnetOutputs); for (const output of testCases) { const result = await BtcUtils.isP2SHOutput(output.script); expect(result).to.be.false; @@ -508,6 +519,78 @@ describe("BtcUtils", function () { }); }); + describe('isP2WPKHOutput function should', () => { + it('return true if the script is a P2WPKH script', async () => { + for (const output of p2wpkh.testnetOutputs.concat(p2wpkh.mainnetOutputs)) { + const result = await BtcUtils.isP2WPKHOutput(output.script); + expect(result).to.be.true; + } + }); + + it('return false if the script is not a P2WPKH script', async () => { + const testCases = taproot.testnetOutputs + .concat(taproot.mainnetOutputs) + .concat(p2kph.testnetOutputs) + .concat(p2kph.mainnetOutputs) + .concat(p2wsh.testnetOutputs) + .concat(p2wsh.mainnetOutputs) + .concat(p2sh.testnetOutputs) + .concat(p2sh.mainnetOutputs); + for (const output of testCases) { + const result = await BtcUtils.isP2WPKHOutput(output.script); + expect(result).to.be.false; + } + }); + }); + + describe('isP2WSHOutput function should', () => { + it('return true if the script is a P2WSH script', async () => { + for (const output of p2wsh.testnetOutputs.concat(p2wsh.mainnetOutputs)) { + const result = await BtcUtils.isP2WSHOutput(output.script); + expect(result).to.be.true; + } + }); + + it('return false if the script is not a P2WSH script', async () => { + const testCases = taproot.testnetOutputs + .concat(taproot.mainnetOutputs) + .concat(p2kph.testnetOutputs) + .concat(p2kph.mainnetOutputs) + .concat(p2sh.testnetOutputs) + .concat(p2sh.mainnetOutputs) + .concat(p2wpkh.testnetOutputs) + .concat(p2wpkh.mainnetOutputs); + for (const output of testCases) { + const result = await BtcUtils.isP2WSHOutput(output.script); + expect(result).to.be.false; + } + }); + }); + + describe('isP2TROutput function should', () => { + it('return true if the script is a P2TR script', async () => { + for (const output of taproot.testnetOutputs.concat(taproot.mainnetOutputs)) { + const result = await BtcUtils.isP2TROutput(output.script); + expect(result).to.be.true; + } + }); + + it('return false if the script is not a P2TR script', async () => { + const testCases = p2wsh.testnetOutputs + .concat(p2wsh.mainnetOutputs) + .concat(p2kph.testnetOutputs) + .concat(p2kph.mainnetOutputs) + .concat(p2sh.testnetOutputs) + .concat(p2sh.mainnetOutputs) + .concat(p2wpkh.testnetOutputs) + .concat(p2wpkh.mainnetOutputs); + for (const output of testCases) { + const result = await BtcUtils.isP2TROutput(output.script); + expect(result).to.be.false; + } + }); + }); + describe('parsePayToScriptHash function should', () => { it('parse properly the P2SH scripts and return the corresponding address', async () => { for (const output of p2sh.testnetOutputs) { @@ -521,8 +604,8 @@ describe("BtcUtils", function () { }); it('fail if script doesn\'t have the required structure', async () => { - const testnetCases = b32.testnetOutputs.concat(p2kph.testnetOutputs); - const mainnetCases = b32.mainnetOutputs.concat(p2kph.mainnetOutputs); + const testnetCases = taproot.testnetOutputs.concat(p2kph.testnetOutputs).concat(p2wsh.testnetOutputs).concat(p2wpkh.testnetOutputs); + const mainnetCases = taproot.mainnetOutputs.concat(p2kph.mainnetOutputs).concat(p2wsh.mainnetOutputs).concat(p2wpkh.mainnetOutputs); for (const output of testnetCases) { await expect(BtcUtils.parsePayToScriptHash(output.script, false)).to.be.revertedWith("Script hasn't the required structure"); } @@ -545,8 +628,8 @@ describe("BtcUtils", function () { }); it('fail if script doesn\'t have the correct format', async () => { - const testnetCases = b32.testnetOutputs.concat(p2sh.testnetOutputs); - const mainnetCases = b32.mainnetOutputs.concat(p2sh.mainnetOutputs); + const testnetCases = taproot.testnetOutputs.concat(p2sh.testnetOutputs).concat(p2wsh.testnetOutputs).concat(p2wpkh.testnetOutputs); + const mainnetCases = taproot.mainnetOutputs.concat(p2sh.mainnetOutputs).concat(p2wsh.mainnetOutputs).concat(p2wpkh.mainnetOutputs); for (const output of testnetCases) { await expect(BtcUtils.parsePayToPubKeyHash(output.script, false)).to.be.revertedWith("Script hasn't the required structure"); } @@ -556,6 +639,78 @@ describe("BtcUtils", function () { }); }); + describe('parsePayToWitnessPubKeyHash function should', () => { + it('parse properly the P2WPKH scripts and return the corresponding address', async () => { + for (const output of p2wpkh.testnetOutputs) { + const address = await BtcUtils.parsePayToWitnessPubKeyHash(output.script); + expect(bech32.encode('tb', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of p2wpkh.mainnetOutputs) { + const address = await BtcUtils.parsePayToWitnessPubKeyHash(output.script); + expect(bech32.encode('bc', ethers.getBytes(address))).to.equal(output.address); + } + }); + + it('fail if script doesn\'t have the correct format', async () => { + const testnetCases = taproot.testnetOutputs.concat(p2sh.testnetOutputs).concat(p2wsh.testnetOutputs).concat(p2kph.testnetOutputs); + const mainnetCases = taproot.mainnetOutputs.concat(p2sh.mainnetOutputs).concat(p2wsh.mainnetOutputs).concat(p2kph.mainnetOutputs); + for (const output of testnetCases) { + await expect(BtcUtils.parsePayToWitnessPubKeyHash(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + for (const output of mainnetCases) { + await expect(BtcUtils.parsePayToWitnessPubKeyHash(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + }); + }); + + describe('parsePayToWitnessScriptHash function should', () => { + it('parse properly the P2WSH scripts and return the corresponding address', async () => { + for (const output of p2wsh.testnetOutputs) { + const address = await BtcUtils.parsePayToWitnessScriptHash(output.script); + expect(bech32.encode('tb', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of p2wsh.mainnetOutputs) { + const address = await BtcUtils.parsePayToWitnessScriptHash(output.script); + expect(bech32.encode('bc', ethers.getBytes(address))).to.equal(output.address); + } + }); + + it('fail if script doesn\'t have the correct format', async () => { + const testnetCases = taproot.testnetOutputs.concat(p2sh.testnetOutputs).concat(p2wpkh.testnetOutputs).concat(p2kph.testnetOutputs); + const mainnetCases = taproot.mainnetOutputs.concat(p2sh.mainnetOutputs).concat(p2wpkh.mainnetOutputs).concat(p2kph.mainnetOutputs); + for (const output of testnetCases) { + await expect(BtcUtils.parsePayToWitnessScriptHash(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + for (const output of mainnetCases) { + await expect(BtcUtils.parsePayToWitnessScriptHash(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + }); + }); + + describe('parsePayToTaproot function should', () => { + it('parse properly the P2TR scripts and return the corresponding address', async () => { + for (const output of taproot.testnetOutputs) { + const address = await BtcUtils.parsePayToTaproot(output.script); + expect(bech32m.encode('tb', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of taproot.mainnetOutputs) { + const address = await BtcUtils.parsePayToTaproot(output.script); + expect(bech32m.encode('bc', ethers.getBytes(address))).to.equal(output.address); + } + }); + + it('fail if script doesn\'t have the correct format', async () => { + const testnetCases = p2wsh.testnetOutputs.concat(p2sh.testnetOutputs).concat(p2wpkh.testnetOutputs).concat(p2kph.testnetOutputs); + const mainnetCases = p2wsh.mainnetOutputs.concat(p2sh.mainnetOutputs).concat(p2wpkh.mainnetOutputs).concat(p2kph.mainnetOutputs); + for (const output of testnetCases) { + await expect(BtcUtils.parsePayToTaproot(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + for (const output of mainnetCases) { + await expect(BtcUtils.parsePayToTaproot(output.script)).to.be.revertedWith("Script hasn't the required structure"); + } + }); + }); + describe('outputScriptToAddress function should', async () => { it('parse the script and return the address if its a supported type', async () => { for (const output of p2kph.testnetOutputs.concat(p2sh.testnetOutputs)) { @@ -566,15 +721,27 @@ describe("BtcUtils", function () { const address = await BtcUtils.outputScriptToAddress(output.script, true); expect(bs58check.encode(ethers.getBytes(address))).to.equal(output.address); } + for (const output of p2wpkh.mainnetOutputs.concat(p2wsh.mainnetOutputs)) { + const address = await BtcUtils.outputScriptToAddress(output.script, true); + expect(bech32.encode('bc', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of p2wpkh.testnetOutputs.concat(p2wsh.testnetOutputs)) { + const address = await BtcUtils.outputScriptToAddress(output.script, false); + expect(bech32.encode('tb', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of taproot.mainnetOutputs) { + const address = await BtcUtils.outputScriptToAddress(output.script, true); + expect(bech32m.encode('bc', ethers.getBytes(address))).to.equal(output.address); + } + for (const output of taproot.testnetOutputs) { + const address = await BtcUtils.outputScriptToAddress(output.script, false); + expect(bech32m.encode('tb', ethers.getBytes(address))).to.equal(output.address); + } }); it('fail if is an unsupported script type or script type cannot be converted to address', async() => { - for (const output of b32.mainnetOutputs) { - await expect(BtcUtils.outputScriptToAddress(output.script, true)).to.be.revertedWith("Unsupported script type"); - } - for (const output of b32.testnetOutputs) { - await expect(BtcUtils.outputScriptToAddress(output.script, false)).to.be.revertedWith("Unsupported script type"); - } + await expect(BtcUtils.outputScriptToAddress("0x0102030405", true)).to.be.revertedWith("Unsupported script type"); + await expect(BtcUtils.outputScriptToAddress("0x6a2448617468e76bc64be388085f432feb343fd758e1488af93af3df863092b28b3e40d60aec", false)).to.be.revertedWith("Unsupported script type"); }); }); }); diff --git a/test/test-data/p2tr-outputs.ts b/test/test-data/p2tr-outputs.ts new file mode 100644 index 0000000..f3a5b7e --- /dev/null +++ b/test/test-data/p2tr-outputs.ts @@ -0,0 +1,54 @@ +const testnetOutputs: TestingOutput[] = [ + { + script: '0x5120077b0a1b7ea3664a1e15a28b52e0bdb500da46174dbf3a95f2e56645753057db', + address: 'tb1pqaas5xm75dny58s452949c9ak5qd53shfkln490ju4ny2afs2ldsput844' + }, + { + script: '0x5120552ef34227abc1eee4d1b7cad85da7014a0174d6b9c740f9e2a547525b7350d7', + address: 'tb1p25h0xs3840q7aex3kl9dshd8q99qzaxkh8r5p70z54r4ykmn2rtsgcsj34' + }, + { + script: '0x5120f5d8e3ee60f8d61f253d3b85bb83cc6b7284a5b9e42b0fbc92d25d4a8a4dec74', + address: 'tb1p7hvw8mnqlrtp7ffa8wzmhq7vddegffdeus4sl0yj6fw54zjda36qhc5q8y' + }, + { + script: '0x5120981a3a6bfc46d63c197dc9ec5c273ebb32729f5c6bdbdb2ff285cb1724e0f72a', + address: 'tb1pnqdr56lugmtrcxtae8k9cfe7hve8986ud0daktljsh93wf8q7u4qhc2q3c' + }, + { + script: '0x5120ed2a8dca2d10ca8a07c036adc081053598b62b0dc2c9e3b904e93065831e55e8', + address: 'tb1pa54gmj3dzr9g5p7qx6kupqg9xkvtv2cdcty78wgyaycxtqc72h5qlqgz2c' + + }, + { + script: '0x51203fec64cc30a39ba1c07c8308b09c5ff8f6613e997ad7984277209df7db90c2bb', + address: 'tb1p8lkxfnps5wd6rsrusvytp8zllrmxz05e0ttessnhyzwl0kusc2as4s72wz' + } +] + +const mainnetOutputs: TestingOutput[] = [ + { + script: '0x5120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f9', + address: 'bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297' + }, + { + script: '0x512021d1565737ffe25283e1e988625911f0602bf5a5221ed0dccc95dbbb93c979d9', + address: 'bc1py8g4v4ehll399qlpaxyxykg37pszhad9yg0dphxvjhdmhy7f08vsn43s6p' + }, + { + script: '0x5120c3cb8ea59f47284b6cc42087547280718bd5085a48a3ffcb246b70517a7ea4ca', + address: 'bc1pc09cafvlgu5ykmxyyzr4gu5qwx9a2zz6fz3lljeyddc9z7n75n9qfz7ckr' + + }, + { + script: '0x5120f56d12f9fa4e75378194b45e2a982069d1849fa120a658507c9585597e6ae452', + address: 'bc1p74k39706fe6n0qv5k30z4xpqd8gcf8apyzn9s5rujkz4jln2u3fqwwta94' + + }, + { + script: '0x5120cad13b06ff3ab6d7d1a7aa1d9cbfbfb70410dd3221c98340b154d0c698e23f1f', + address: 'bc1petgnkphl82md05d84gwee0alkuzpphfjy8ycxs932ngvdx8z8u0s3dwj5t' + + } +] +export { testnetOutputs, mainnetOutputs } \ No newline at end of file diff --git a/test/test-data/bech32-outputs.ts b/test/test-data/p2wpkh-outputs.ts similarity index 52% rename from test/test-data/bech32-outputs.ts rename to test/test-data/p2wpkh-outputs.ts index 21ea5e2..3376d2d 100644 --- a/test/test-data/bech32-outputs.ts +++ b/test/test-data/p2wpkh-outputs.ts @@ -1,19 +1,8 @@ -// we don't differentiate between P2WPKH, P2WSH and P2TR yet since any of the bech32 addresses are -// supported right now, when we introduce its support to the library, this test data will be divided -// into three different files. const testnetOutputs: TestingOutput[] = [ - { - script: '0x5120077b0a1b7ea3664a1e15a28b52e0bdb500da46174dbf3a95f2e56645753057db', - address: 'tb1pqaas5xm75dny58s452949c9ak5qd53shfkln490ju4ny2afs2ldsput844' - }, { script: '0x001452b1b883a3f865144d34bbe360b6fd4adac494e0', address: 'tb1q22cm3qarlpj3gnf5h03kpdhaftdvf98q58dp75' }, - { - script: '0x5120552ef34227abc1eee4d1b7cad85da7014a0174d6b9c740f9e2a547525b7350d7', - address: 'tb1p25h0xs3840q7aex3kl9dshd8q99qzaxkh8r5p70z54r4ykmn2rtsgcsj34' - }, { script: '0x0014977d48ab28e429a95a53e68c8496772e805e07c8', address: 'tb1qja7532egus56jkjnu6xgf9nh96q9up7gq5473m' @@ -22,21 +11,21 @@ const testnetOutputs: TestingOutput[] = [ script: '0x0014e2236fe7d11674ec416dc55dc9da46e8413ee72b', address: 'tb1qug3kle73ze6wcstdc4wunkjxapqnaeetprqjql' }, + { + script: '0x0014d6b25f2201b2a31cafd655631a8b6a4e76c4d161', + address: 'tb1q66e97gspk233et7k24334zm2femvf5tpsq8ggm' + }, + { + script: '0x001492ff4f363ec63adde62fbb232b916b2a3050d5a0', + address: 'tb1qjtl57d37ccadme30hv3jhytt9gc9p4dq9zrz49' + } ] const mainnetOutputs: TestingOutput[] = [ - { - script: '0x5120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f9', - address: 'bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297' - }, { script: '0x0014173fd310e9db2c7e9550ce0f03f1e6c01d833aa9', address: 'bc1qzulaxy8fmvk8a92sec8s8u0xcqwcxw4fx037d8' }, - { - script: '0x512021d1565737ffe25283e1e988625911f0602bf5a5221ed0dccc95dbbb93c979d9', - address: 'bc1py8g4v4ehll399qlpaxyxykg37pszhad9yg0dphxvjhdmhy7f08vsn43s6p' - }, { script: '0x00143c0655b0b34548d3c3502e7334c3dbbaab266aac', address: 'bc1q8sr9tv9ng4yd8s6s9eenfs7mh24jv64vnwzl0p' @@ -45,5 +34,15 @@ const mainnetOutputs: TestingOutput[] = [ script: '0x0014a052249b6b346d3f48ee03b3a8c74bda1f3a7f61', address: 'bc1q5pfzfxmtx3kn7j8wqwe6336tmg0n5lmpqss9kx' }, + { + script: '0x00144028fd22bae29b3e77cfb45a40f343f1b91b2419', + address: 'bc1qgq506g46u2dnua70k3dypu6r7xu3kfqeee3c38' + + }, + { + script: '0x0014b287402cb54ff84cde571ea79b75fd404dc9a147', + address: 'bc1qk2r5qt94fluyehjhr6neka0agpxung28pndjly' + + } ] export { testnetOutputs, mainnetOutputs } \ No newline at end of file diff --git a/test/test-data/p2wsh-outputs.ts b/test/test-data/p2wsh-outputs.ts new file mode 100644 index 0000000..bf05fc4 --- /dev/null +++ b/test/test-data/p2wsh-outputs.ts @@ -0,0 +1,46 @@ +const testnetOutputs: TestingOutput[] = [ + { + script: '0x0020137b507ecd0c91b7196510d45086f71012ab71e50f9b28c404e20133bec8a6f8', + address: 'tb1qzda4qlkdpjgmwxt9zr29pphhzqf2ku09p7dj33qyugqn80kg5muq8x0wyv' + }, + { + script: '0x00204050b04b4713a0d178db600c6377b3f3709473da909f8488930206b63606f901', + address: 'tb1qgpgtqj68zwsdz7xmvqxxxaan7dcfgu76jz0cfzynqgrtvdsxlyqsf7dfz8' + }, + { + script: '0x0020b06bf361e5cc6b8c518e89e32aa868ee3830f9c30cdd2db3a26690a8e14468a2', + address: 'tb1qkp4lxc09e34cc5vw383j42rgacurp7wrpnwjmvazv6g23c2ydz3qx5tfhl' + }, + { + script: '0x002015f874c90ea77a0431e054b24ab678885181d71681e876eb3d497df9188ecf9e', + address: 'tb1qzhu8fjgw5aaqgv0q2jey4dnc3pgcr4cks858d6eaf97ljxywe70qwwsdku' + }, + { + script: '0x0020137b507ecd0c91b7196510d45086f71012ab71e50f9b28c404e20133bec8a6f8', + address: 'tb1qzda4qlkdpjgmwxt9zr29pphhzqf2ku09p7dj33qyugqn80kg5muq8x0wyv' + } +] + +const mainnetOutputs: TestingOutput[] = [ + { + script: '0x0020bcf9b62d11c14d2503d2dace69daa1a5b76a292903ba63e0699d6e058c9a0432', + address: 'bc1qhnumvtg3c9xj2q7jmt8xnk4p5kmk52ffqwax8crfn4hqtry6qseq8vahua' + }, + { + script: '0x0020657d39bcbedeafc903ad76ac85d2f0b16efd5bcb401f4cec3f8ce71dc2dd3e12', + address: 'bc1qv47nn097m6hujqadw6kgt5hsk9h06k7tgq05empl3nn3mska8cfqpkjl36' + }, + { + script: '0x002091c20bbacc7db5752f7172c1cf1633959896eb4d675f517da16e324ac3b6a5e2', + address: 'bc1qj8pqhwkv0k6h2tm3wtqu793njkvfd66dva04zldpdcey4sak5h3qx3n8nz' + }, + { + script: '0x0020ced4beeb9629c691fbc55d5c9582a092c7132e5d2b1252122342f02e871e3ca3', + address: 'bc1qem2ta6uk98rfr779t4wftq4qjtr3xtja9vf9yy3rgtczapc78j3sxa6570' + }, + { + script: '0x0020e8b67904706c8d37801ef106277b6a8d5e483edce13e87d19cfd574ffd5fbb78', + address: 'bc1qazm8jprsdjxn0qq77yrzw7m2340ys0kuuylg05vul4t5ll2lhduquuhngw' + } +] +export { testnetOutputs, mainnetOutputs } \ No newline at end of file