From ea33c654385722a9198dd11053e30dea080a82f6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jul 2023 12:10:28 +0200 Subject: [PATCH 1/4] Add `getAddressFromScriptPubKey` fn Add the new function that returns the Bitcoin address based on the script pub key placed on the output of a Bitcoin transaction. --- typescript/src/bitcoin.ts | 19 ++++- typescript/test/bitcoin.test.ts | 145 ++++++++++---------------------- typescript/test/data/bitcoin.ts | 65 ++++++++++++++ 3 files changed, 128 insertions(+), 101 deletions(-) create mode 100644 typescript/test/data/bitcoin.ts diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index bb330be3f..7941b802b 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -1,4 +1,4 @@ -import bcoin, { TX, Script } from "bcoin" +import bcoin, { TX, Script, Address } from "bcoin" import wif from "wif" import bufio from "bufio" import hash160 from "bcrypto/lib/hash160" @@ -612,3 +612,20 @@ export function locktimeToNumber(locktimeLE: Buffer | string): number { export function createOutputScriptFromAddress(address: string): Hex { return Hex.from(Script.fromAddress(address).toRaw().toString("hex")) } + +/** + * Returns the Bitcoin address based on the script pub key placed on the output + * of a Bitcoin transaction. + * @param scriptPubKey Scirpt pub key placed on the output of a Bitcoin + * transaction. + * @param network Bitcoin network. + * @returns The Bitcoin address. + */ +export function getAddressFromScriptPubKey( + scriptPubKey: string, + network: BitcoinNetwork = BitcoinNetwork.Mainnet +): string { + return Script.fromRaw(scriptPubKey.toString(), "hex") + .getAddress() + ?.toString(toBcoinNetwork(network)) +} diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index a1bb5da82..7c9795e40 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -12,11 +12,13 @@ import { bitsToTarget, targetToDifficulty, createOutputScriptFromAddress, + getAddressFromScriptPubKey, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" import { Hex } from "../src/hex" import { BigNumber } from "ethers" +import { btcAddresses } from "./data/bitcoin" describe("Bitcoin", () => { describe("compressPublicKey", () => { @@ -467,110 +469,53 @@ describe("Bitcoin", () => { }) describe("createOutputScriptFromAddress", () => { - context("with testnet addresses", () => { - const btcAddresses = { - P2PKH: { - address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", - redeemerOutputScript: - "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - outputScript: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - }, - P2WPKH: { - address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", - redeemerOutputScript: - "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - outputScript: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - }, - P2SH: { - address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", - redeemerOutputScript: - "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - outputScript: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - }, - P2WSH: { - address: - "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", - redeemerOutputScript: - "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - outputScript: - "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - }, - } - - Object.entries(btcAddresses).forEach( - ([ - addressType, - { - address, - redeemerOutputScript: expectedRedeemerOutputScript, - outputScript: expectedOutputScript, - }, - ]) => { - it(`should create correct output script for ${addressType} address type`, () => { - const result = createOutputScriptFromAddress(address) - - expect(result.toString()).to.eq(expectedOutputScript) - // Check if we can build the prefixed raw redeemer output script based - // on the result. - expect(buildRawPrefixedOutputScript(result.toString())).to.eq( - expectedRedeemerOutputScript - ) - }) - } - ) + Object.keys(btcAddresses).forEach((bitcoinNetwork) => { + context(`with ${bitcoinNetwork} addresses`, () => { + Object.entries( + btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] + ).forEach( + ([ + addressType, + { + address, + redeemerOutputScript: expectedRedeemerOutputScript, + scriptPubKey: expectedOutputScript, + }, + ]) => { + it(`should create correct output script for ${addressType} address type`, () => { + const result = createOutputScriptFromAddress(address) + + expect(result.toString()).to.eq(expectedOutputScript) + // Check if we can build the prefixed raw redeemer output script based + // on the result. + expect(buildRawPrefixedOutputScript(result.toString())).to.eq( + expectedRedeemerOutputScript + ) + }) + } + ) + }) }) + }) - context("with mainnet addresses", () => { - const btcAddresses = { - P2PKH: { - address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", - redeemerOutputScript: - "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - outputScript: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - }, - P2WPKH: { - address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", - redeemerOutputScript: - "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", - outputScript: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", - }, - P2SH: { - address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", - redeemerOutputScript: - "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - outputScript: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - }, - P2WSH: { - address: - "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", - redeemerOutputScript: - "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - outputScript: - "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - }, - } - - Object.entries(btcAddresses).forEach( - ([ - addressType, - { - address, - redeemerOutputScript: expectedRedeemerOutputScript, - outputScript: expectedOutputScript, - }, - ]) => { - it(`should create correct output script for ${addressType} address type`, () => { - const result = createOutputScriptFromAddress(address) - - expect(result.toString()).to.eq(expectedOutputScript) - // Check if we can build the prefixed raw redeemer output script based - // on the result. - expect(buildRawPrefixedOutputScript(result.toString())).to.eq( - expectedRedeemerOutputScript + describe("getAddressFromScriptPubKey", () => { + Object.keys(btcAddresses).forEach((bitcoinNetwork) => { + context(`with ${bitcoinNetwork} addresses`, () => { + Object.entries( + btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] + ).forEach(([addressType, { address, scriptPubKey }]) => { + it(`should return correct ${addressType} address`, () => { + const result = getAddressFromScriptPubKey( + scriptPubKey, + bitcoinNetwork === "mainnet" + ? BitcoinNetwork.Mainnet + : BitcoinNetwork.Testnet ) + + expect(result.toString()).to.eq(address) }) - } - ) + }) + }) }) }) }) diff --git a/typescript/test/data/bitcoin.ts b/typescript/test/data/bitcoin.ts new file mode 100644 index 000000000..bc266e1a0 --- /dev/null +++ b/typescript/test/data/bitcoin.ts @@ -0,0 +1,65 @@ +import { BitcoinNetwork } from "../../src/bitcoin-network" + +export const btcAddresses: Record< + Exclude, + { + [addressType: string]: { + address: string + redeemerOutputScript: string + scriptPubKey: string + } + } +> = { + testnet: { + P2PKH: { + address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", + redeemerOutputScript: + "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + scriptPubKey: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + }, + P2WPKH: { + address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + redeemerOutputScript: "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + scriptPubKey: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + }, + P2SH: { + address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", + redeemerOutputScript: + "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + scriptPubKey: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + }, + P2WSH: { + address: "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", + redeemerOutputScript: + "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + scriptPubKey: + "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + }, + }, + mainnet: { + P2PKH: { + address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", + redeemerOutputScript: + "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + scriptPubKey: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + }, + P2WPKH: { + address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", + redeemerOutputScript: "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", + scriptPubKey: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", + }, + P2SH: { + address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", + redeemerOutputScript: + "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + scriptPubKey: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + }, + P2WSH: { + address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + redeemerOutputScript: + "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + scriptPubKey: + "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + }, + }, +} From 5ceed1e5e93c02eca775d559ff94935d066a2a19 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 10:35:49 +0200 Subject: [PATCH 2/4] Remove unnecessary import --- typescript/src/bitcoin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 7941b802b..c3235cdb4 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -1,4 +1,4 @@ -import bcoin, { TX, Script, Address } from "bcoin" +import bcoin, { TX, Script } from "bcoin" import wif from "wif" import bufio from "bufio" import hash160 from "bcrypto/lib/hash160" From c6f94f641dda72a2a8c155d50de2a902daaa24c6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 12:27:26 +0200 Subject: [PATCH 3/4] Rename fn that converts script to BTC address The `scriptPubKey` is a field specific to transaction outputs but basically, this function converts any script to a Bitcoin address. Here we make this function more generic and rename to `createAddressFromOutputScript`. --- typescript/src/bitcoin.ts | 2 +- typescript/test/bitcoin.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index ceffb0fca..4abc66599 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -633,7 +633,7 @@ export function createOutputScriptFromAddress(address: string): Hex { * @param network Bitcoin network. * @returns The Bitcoin address. */ -export function getAddressFromScriptPubKey( +export function createAddressFromOutputScript( scriptPubKey: string, network: BitcoinNetwork = BitcoinNetwork.Mainnet ): string { diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index b581fb78e..ce525b06d 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -12,7 +12,7 @@ import { bitsToTarget, targetToDifficulty, createOutputScriptFromAddress, - getAddressFromScriptPubKey, + createAddressFromOutputScript, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" @@ -493,7 +493,7 @@ describe("Bitcoin", () => { btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] ).forEach(([addressType, { address, scriptPubKey }]) => { it(`should return correct ${addressType} address`, () => { - const result = getAddressFromScriptPubKey( + const result = createAddressFromOutputScript( scriptPubKey, bitcoinNetwork === "mainnet" ? BitcoinNetwork.Mainnet From 1c9b91b210eb22733a35e5549f4a735f8da2fcf3 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 12:36:12 +0200 Subject: [PATCH 4/4] Update `createAddressFromOutputScript` fn param Rename `scriptPubKey` to `script` and update docs. The `scriptPubKey` is a field specific to transaction outputs but basically, this function converts any script to a Bitcoin address. Here we also update type of this param to `Hex` instead of `string`. --- typescript/src/bitcoin.ts | 10 ++++------ typescript/test/bitcoin.test.ts | 2 +- typescript/test/data/bitcoin.ts | 29 ++++++++++++++++++----------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 4abc66599..3d7c32585 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -626,18 +626,16 @@ export function createOutputScriptFromAddress(address: string): Hex { } /** - * Returns the Bitcoin address based on the script pub key placed on the output - * of a Bitcoin transaction. - * @param scriptPubKey Scirpt pub key placed on the output of a Bitcoin - * transaction. + * Creates the Bitcoin address from the output script. + * @param script The unprefixed and not prepended with length output script. * @param network Bitcoin network. * @returns The Bitcoin address. */ export function createAddressFromOutputScript( - scriptPubKey: string, + script: Hex, network: BitcoinNetwork = BitcoinNetwork.Mainnet ): string { - return Script.fromRaw(scriptPubKey.toString(), "hex") + return Script.fromRaw(script.toString(), "hex") .getAddress() ?.toString(toBcoinNetwork(network)) } diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index ce525b06d..27ce2861f 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -478,7 +478,7 @@ describe("Bitcoin", () => { it(`should create correct output script for ${addressType} address type`, () => { const result = createOutputScriptFromAddress(address) - expect(result.toString()).to.eq(expectedOutputScript) + expect(result.toString()).to.eq(expectedOutputScript.toString()) }) } ) diff --git a/typescript/test/data/bitcoin.ts b/typescript/test/data/bitcoin.ts index bc266e1a0..d44b4b737 100644 --- a/typescript/test/data/bitcoin.ts +++ b/typescript/test/data/bitcoin.ts @@ -1,4 +1,5 @@ import { BitcoinNetwork } from "../../src/bitcoin-network" +import { Hex } from "../../src/hex" export const btcAddresses: Record< Exclude, @@ -6,7 +7,7 @@ export const btcAddresses: Record< [addressType: string]: { address: string redeemerOutputScript: string - scriptPubKey: string + scriptPubKey: Hex } } > = { @@ -15,25 +16,28 @@ export const btcAddresses: Record< address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", redeemerOutputScript: "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - scriptPubKey: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + scriptPubKey: Hex.from( + "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac" + ), }, P2WPKH: { address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", redeemerOutputScript: "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - scriptPubKey: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + scriptPubKey: Hex.from("0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), }, P2SH: { address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", redeemerOutputScript: "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - scriptPubKey: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + scriptPubKey: Hex.from("a914011beb6fb8499e075a57027fb0a58384f2d3f78487"), }, P2WSH: { address: "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", redeemerOutputScript: "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - scriptPubKey: - "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + scriptPubKey: Hex.from( + "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c" + ), }, }, mainnet: { @@ -41,25 +45,28 @@ export const btcAddresses: Record< address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", redeemerOutputScript: "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - scriptPubKey: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + scriptPubKey: Hex.from( + "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac" + ), }, P2WPKH: { address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", redeemerOutputScript: "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", - scriptPubKey: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", + scriptPubKey: Hex.from("00148d7a0a3461e3891723e5fdf8129caa0075060cff"), }, P2SH: { address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", redeemerOutputScript: "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - scriptPubKey: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + scriptPubKey: Hex.from("a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87"), }, P2WSH: { address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", redeemerOutputScript: "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - scriptPubKey: - "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + scriptPubKey: Hex.from( + "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70" + ), }, }, }