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 c01074c..799abb3 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", "husky": "^8.0.3", @@ -548,6 +549,12 @@ "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 + }, "node_modules/@ethersproject/providers/node_modules/ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", @@ -2317,9 +2324,9 @@ ] }, "node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "dev": true }, "node_modules/bigint-crypto-utils": { diff --git a/package.json b/package.json index 1286f76..c1fffc9 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", "husky": "^8.0.3", 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