diff --git a/packages/stacking/src/utils.ts b/packages/stacking/src/utils.ts index 1dbc9681..066b85ac 100644 --- a/packages/stacking/src/utils.ts +++ b/packages/stacking/src/utils.ts @@ -19,21 +19,49 @@ export class InvalidAddressError extends Error { } } +export const BitcoinNetworkVersion = { + mainnet: { + P2PKH: 0x00, // 0 + P2SH: 0x05, // 5 + }, + testnet: { + P2PKH: 0x6f, // 111 + P2SH: 0xc4, // 196 + }, +} as const; + export function btcAddressVersionToHashMode(btcAddressVersion: number): AddressHashMode { switch (btcAddressVersion) { - case 0: // btc mainnet P2PKH + case BitcoinNetworkVersion.mainnet.P2PKH: return AddressHashMode.SerializeP2PKH; - case 111: // btc mainnet P2PKH + case BitcoinNetworkVersion.testnet.P2PKH: return AddressHashMode.SerializeP2PKH; - case 5: // btc mainnet P2SH + case BitcoinNetworkVersion.mainnet.P2SH: return AddressHashMode.SerializeP2SH; - case 196: // btc testnet P2SH + case BitcoinNetworkVersion.testnet.P2SH: return AddressHashMode.SerializeP2SH; default: throw new Error('Invalid pox address version'); } } +export function hashModeToBtcAddressVersion( + hashMode: AddressHashMode, + network: 'mainnet' | 'testnet' +): number { + if (!['mainnet', 'testnet'].includes(network)) { + throw new Error(`Invalid network argument: ${network}`); + } + switch (hashMode) { + case AddressHashMode.SerializeP2PKH: + return BitcoinNetworkVersion[network].P2PKH; + case AddressHashMode.SerializeP2SH: + return BitcoinNetworkVersion[network].P2SH; + default: + throw new Error(`Invalid pox address hash mode: ${hashMode}`); + } +} + export function getAddressHashMode(btcAddress: string) { try { const { version } = address.fromBase58Check(btcAddress); @@ -57,6 +85,22 @@ export function decodeBtcAddress(btcAddress: string) { }; } +export function poxAddressToBtcAddress( + version: Buffer, + hashBytes: Buffer, + network: 'mainnet' | 'testnet' +) { + if (version.byteLength !== 1) { + throw new Error(`Invalid byte length for version buffer: ${version.toString('hex')}`); + } + if (hashBytes.byteLength !== 20) { + throw new Error(`Invalid byte length for hashBytes: ${hashBytes.toString('hex')}`); + } + const btcNetworkVersion = hashModeToBtcAddressVersion(version[0], network); + const btcAddress = address.toBase58Check(hashBytes, btcNetworkVersion); + return btcAddress; +} + export function getBTCAddress(version: Buffer, checksum: Buffer) { const btcAddress = address.toBase58Check(checksum, new BN(version).toNumber()); return btcAddress; diff --git a/packages/stacking/tests/stacking.test.ts b/packages/stacking/tests/stacking.test.ts index a14f2eaa..cc9a28e7 100644 --- a/packages/stacking/tests/stacking.test.ts +++ b/packages/stacking/tests/stacking.test.ts @@ -16,7 +16,7 @@ import { intCV, } from '@stacks/transactions'; import { address as btcAddress } from 'bitcoinjs-lib'; -import { decodeBtcAddress, getAddressHashMode, InvalidAddressError } from '../src/utils'; +import { decodeBtcAddress, getAddressHashMode, InvalidAddressError, poxAddressToBtcAddress } from '../src/utils'; beforeEach(() => { fetchMock.resetMocks(); @@ -893,3 +893,52 @@ test('pox address hash mode', async () => { expect(() => decodeBtcAddress(p2wsh)).toThrowError(InvalidAddressError); expect(() => decodeBtcAddress(p2wshTestnet)).toThrowError(InvalidAddressError); }) + + +test('pox address to btc address', () => { + const vectors: { + version: Buffer; + hashBytes: Buffer; + network: 'mainnet' | 'testnet'; + expectedBtcAddr: string; + }[] = [ + { + version: Buffer.from([0x01]), + hashBytes: Buffer.from('07366658d1e5f0f75c585a17b618b776f4f10a6b', 'hex'), + network: 'mainnet', + expectedBtcAddr: '32M9pegJxqXBoxXSKBN1s7HJUR2YMkMaFg' + }, + { + version: Buffer.from([0x01]), + hashBytes: Buffer.from('9b24b88b1334b0a17a99c09470c4df06ffd3ea22', 'hex'), + network: 'mainnet', + expectedBtcAddr: '3FqLegt1Lo1JuhiBAQQiM5WwDdmefTo5zd', + }, + { + version: Buffer.from([0x00]), + hashBytes: Buffer.from('fde9c82d7bc43f55e9054438470c3ca8d6e7237f', 'hex'), + network: 'mainnet', + expectedBtcAddr: '1Q9a1zGPfJ4oH5Xaz5wc7BdvWV21fSNkkr', + }, + { + version: Buffer.from([0x00]), + hashBytes: Buffer.from('5dc795522f81dcb7eb774a0b8e84b612e3edc141', 'hex'), + network: 'testnet', + expectedBtcAddr: 'mp4pEBdJiMh6aL5Uhs6nZX1XhyZ4V2xrzg', + }, + { + version: Buffer.from([0x01]), + hashBytes: Buffer.from('3149c3eba2d21cfdeea56894866b8f4cd11b72ad', 'hex'), + network: 'testnet', + expectedBtcAddr: '2MwjqTzEJodSaoehcxRSqfWrvJMGZHq4tdC', + } + ]; + + vectors.forEach(item => { + const btcAddress = poxAddressToBtcAddress(item.version, item.hashBytes, item.network); + expect(btcAddress).toBe(item.expectedBtcAddr); + const decodedAddress = decodeBtcAddress(btcAddress); + expect(decodedAddress.hashMode).toBe(item.version[0]); + expect(decodedAddress.data.toString('hex')).toBe(item.hashBytes.toString('hex')); + }); +});