From 5583fc1bb7ddb398c3654e13cc751c7de17b3315 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Wed, 27 Nov 2024 16:32:02 +0530 Subject: [PATCH] add p2tr, p2pkh, p2sh-p2wpkh address support --- e2e/helpers/common.helper.ts | 4 +- e2e/helpers/wallet.helper.ts | 145 +++++++++++++++++++++++++++++++---- e2e/indexer.e2e-spec.ts | 87 ++++++++++++--------- package-lock.json | 15 ++++ package.json | 1 + 5 files changed, 199 insertions(+), 53 deletions(-) diff --git a/e2e/helpers/common.helper.ts b/e2e/helpers/common.helper.ts index 6b656ea..00048ee 100644 --- a/e2e/helpers/common.helper.ts +++ b/e2e/helpers/common.helper.ts @@ -22,7 +22,9 @@ export function generateScanTweakAndOutputEntity( witness: isWitness ? input.witness.map((v) => v.toString('hex')) : undefined, - prevOutScript: outputs[index].output.toString('hex'), + prevOutScript: outputs[index].redeem + ? outputs[index].redeem.output.toString('hex') + : outputs[index].output.toString('hex'), }; }); const txouts = transaction.outs.map((output) => ({ diff --git a/e2e/helpers/wallet.helper.ts b/e2e/helpers/wallet.helper.ts index d9597be..6d31644 100644 --- a/e2e/helpers/wallet.helper.ts +++ b/e2e/helpers/wallet.helper.ts @@ -7,20 +7,32 @@ import { networks, Payment, Transaction, + crypto, } from 'bitcoinjs-lib'; import { btcToSats } from '@e2e/helpers/common.helper'; import { randomBytes } from 'crypto'; import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371'; import { BitcoinRPCUtil } from '@e2e/helpers/rpc.helper'; import { assert } from 'console'; +import * as ecpair from 'ecpair'; initEccLib(ecc); +const ECPair = ecpair.ECPairFactory(ecc); + +export enum AddressType { + P2WPKH = 'p2wpkh', + P2TR = 'p2tr', + P2PKH = 'p2pkh', + P2SH_P2WPKH = 'p2sh-p2wpkh', +} export type UTXO = { txid: string; vout: number; value: number; rawTx: string; + addressType: AddressType; + index: number; }; export class WalletHelper { @@ -33,9 +45,9 @@ export class WalletHelper { constructor() { this.root = fromSeed(randomBytes(64), networks.regtest); this.bitcoinRPCUtil = new BitcoinRPCUtil(); + initEccLib(ecc); } - // generates 50 bitcoin async initializeSpendableAmount() { await this.bitcoinRPCUtil.createWallet('test_wallet'); await this.bitcoinRPCUtil.loadWallet('test_wallet'); @@ -65,7 +77,12 @@ export class WalletHelper { return blockhash; } - async addAmountToAddress(payment: Payment, amount): Promise { + async addAmountToAddress( + payment: Payment, + amount: number, + addressType: AddressType, + index: number, + ): Promise { this.ensureAmountAvailable(amount); const txid = await this.bitcoinRPCUtil.sendToAddress( @@ -87,6 +104,8 @@ export class WalletHelper { vout: vout, value: btcToSats(utxo.value), rawTx: await this.bitcoinRPCUtil.getRawTransaction(txid), + addressType, + index, }; } } @@ -94,26 +113,41 @@ export class WalletHelper { throw new Error('cant find transaction'); } - generateAddresses(count: number, type: 'p2wpkh' | 'p2tr'): Payment[] { + generateAddresses(count: number, type: AddressType): Payment[] { const outputs: Payment[] = []; for (let i = 0; i < count; i++) { - const path = `m/84'/0'/0'/0/${i}`; - const child = this.root.derivePath(path); + const child = this.root.derivePath(getDerivationPath(type, i)); let output: Payment; switch (type) { - case 'p2wpkh': + case AddressType.P2WPKH: output = payments.p2wpkh({ pubkey: child.publicKey, network: networks.regtest, }); break; - case 'p2tr': + case AddressType.P2TR: output = payments.p2tr({ internalPubkey: toXOnly(child.publicKey), network: networks.regtest, }); break; + case AddressType.P2PKH: + output = payments.p2pkh({ + pubkey: child.publicKey, + network: networks.regtest, + }); + break; + case AddressType.P2SH_P2WPKH: + const p2wpkh = payments.p2wpkh({ + pubkey: child.publicKey, + network: networks.regtest, + }); + output = payments.p2sh({ + redeem: p2wpkh, + network: networks.regtest, + }); + break; default: throw new Error('Unsupported address type'); } @@ -125,18 +159,59 @@ export class WalletHelper { async craftAndSpendTransaction( utxos: UTXO[], - taprootOutput: Payment, + output: Payment, outputValue: number, fee: number, ): Promise<[Transaction, string, string]> { const psbt = new Psbt({ network: networks.regtest }); - utxos.forEach((utxo) => { - psbt.addInput({ + const keyPair = this.root.derivePath( + getDerivationPath(utxo.addressType, utxo.index), + ); + const input: any = { hash: utxo.txid, index: utxo.vout, - nonWitnessUtxo: Buffer.from(utxo.rawTx, 'hex'), - }); + }; + switch (utxo.addressType) { + case AddressType.P2SH_P2WPKH: + const p2wpkh = payments.p2wpkh({ + pubkey: keyPair.publicKey, + network: networks.regtest, + }); + const p2sh = payments.p2sh({ + redeem: p2wpkh, + network: networks.regtest, + }); + input.witnessUtxo = { + script: p2sh.output, + value: utxo.value, + }; + input.redeemScript = p2sh.redeem.output; + break; + case AddressType.P2WPKH: + input.witnessUtxo = { + script: payments.p2wpkh({ + pubkey: keyPair.publicKey, + network: networks.regtest, + }).output, + value: utxo.value, + }; + break; + case AddressType.P2PKH: + input.nonWitnessUtxo = Buffer.from(utxo.rawTx, 'hex'); + break; + case AddressType.P2TR: + input.witnessUtxo = { + script: payments.p2tr({ + internalPubkey: toXOnly(keyPair.publicKey), + network: networks.regtest, + }).output, + value: utxo.value, + }; + input.tapInternalKey = toXOnly(keyPair.publicKey); + break; + } + psbt.addInput(input); }); const totalInputValue = utxos.reduce( @@ -149,14 +224,19 @@ export class WalletHelper { } psbt.addOutput({ - address: taprootOutput.address, - tapInternalKey: taprootOutput.internalPubkey, + address: output.address, + tapInternalKey: output.internalPubkey, value: btcToSats(outputValue), }); - // Sign the inputs with the corresponding private keys - utxos.forEach((_, index) => { - const keyPair = this.root.derivePath(`m/84'/0'/0'/0/${index}`); + utxos.forEach((utxo, index) => { + let keyPair: any = this.root.derivePath( + getDerivationPath(utxo.addressType, utxo.index), + ); + + if (utxo.addressType === AddressType.P2TR) { + keyPair = createTaprootKeyPair(keyPair); + } psbt.signInput(index, keyPair); }); @@ -172,3 +252,34 @@ export class WalletHelper { return [transaction, txid, blockHash]; } } + +function getDerivationPath(addressType: AddressType, index: number): string { + switch (addressType) { + case AddressType.P2PKH: + return `m/44'/0'/0'/0/${index}`; + case AddressType.P2SH_P2WPKH: + return `m/49'/0'/0'/0/${index}`; + case AddressType.P2WPKH: + return `m/84'/0'/0'/0/${index}`; + case AddressType.P2TR: + return `m/86'/0'/0'/0/${index}`; + default: + throw new Error('Unsupported address type'); + } +} + +function createTaprootKeyPair( + keyPair: BIP32Interface, + network = networks.regtest, +) { + const taprootKeyPair = ECPair.fromPrivateKey(keyPair.privateKey, { + compressed: true, + network: network, + }); + + const tweakedTaprootKey = taprootKeyPair.tweak( + crypto.taggedHash('TapTweak', toXOnly(keyPair.publicKey)), + ); + + return tweakedTaprootKey; +} diff --git a/e2e/indexer.e2e-spec.ts b/e2e/indexer.e2e-spec.ts index 632a31e..0d945f1 100644 --- a/e2e/indexer.e2e-spec.ts +++ b/e2e/indexer.e2e-spec.ts @@ -1,4 +1,4 @@ -import { UTXO, WalletHelper } from '@e2e/helpers/wallet.helper'; +import { UTXO, WalletHelper, AddressType } from '@e2e/helpers/wallet.helper'; import { transformTransaction } from '@e2e/helpers/common.helper'; import { IndexerService } from '@/indexer/indexer.service'; import { initialiseDep } from '@e2e/setup'; @@ -26,45 +26,62 @@ describe('Indexer', () => { await shutdownDep(); }); - it('p2wpkh - should ensure that the correct silent block is fetched', async () => { - const taprootOutput = walletHelper.generateAddresses(1, 'p2tr')[0]; - const p2wkhOutputs = walletHelper.generateAddresses(6, 'p2wpkh'); - const utxos: UTXO[] = []; + const addressTypes: AddressType[] = [ + AddressType.P2WPKH, + AddressType.P2SH_P2WPKH, + AddressType.P2PKH, + AddressType.P2TR, + ]; - for (const output of p2wkhOutputs) { - const utxo = await walletHelper.addAmountToAddress(output, 1); - utxos.push(utxo); - } + addressTypes.forEach((addressType) => { + it(`${addressType} - should ensure that the correct silent block is fetched`, async () => { + const taprootOutput = walletHelper.generateAddresses( + 1, + AddressType.P2TR, + )[0]; + const outputs = walletHelper.generateAddresses(6, addressType); + const utxos: UTXO[] = []; - const [transaction, txid, blockHash] = - await walletHelper.craftAndSpendTransaction( - utxos, - taprootOutput, - 5.999, - 0.001, - ); + for (const [index, output] of outputs.entries()) { + const utxo = await walletHelper.addAmountToAddress( + output, + 1, + addressType, + index, + ); + utxos.push(utxo); + } + + const [transaction, txid, blockHash] = + await walletHelper.craftAndSpendTransaction( + utxos, + taprootOutput, + 5.999, + 0.001, + ); - const transformedTransaction = transformTransaction( - transaction, - txid, - blockHash, - walletHelper.currentBlockCount, - p2wkhOutputs, - indexerService, - ); + const transformedTransaction = transformTransaction( + transaction, + txid, + blockHash, + walletHelper.currentBlockCount, + outputs, + indexerService, + ); - const silentBlock = silentBlockService.encodeSilentBlock([ - transformedTransaction, - ]); + const silentBlock = silentBlockService.encodeSilentBlock([ + transformedTransaction, + ]); - await new Promise((resolve) => setTimeout(resolve, 15000)); - const response = await apiHelper.get( - `/silent-block/hash/${blockHash}`, - { - responseType: 'arraybuffer', - }, - ); + await new Promise((resolve) => setTimeout(resolve, 15000)); + const response = await apiHelper.get( + `/silent-block/hash/${blockHash}`, + { + responseType: 'arraybuffer', + }, + ); - expect(response.data as Buffer).toEqual(silentBlock); + expect(response.data as Buffer).toEqual(silentBlock); + }); }); }); diff --git a/package-lock.json b/package-lock.json index e07a36e..2c009f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "bitcoinjs-lib": "^6.1.6-rc.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ecpair": "^2.1.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.26.0", @@ -5495,6 +5496,20 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecpair": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", + "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index 7945be6..30aa931 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "bitcoinjs-lib": "^6.1.6-rc.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ecpair": "^2.1.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.26.0",