From bf1fc446aa5ca7910f1a5b7bfa8a6b81fe485773 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Fri, 13 Dec 2024 18:42:26 +0530 Subject: [PATCH] feat: p2pkh, p2tr, p2sh-p2wpkh address support --- e2e/helpers/common.helper.ts | 4 +- e2e/helpers/wallet.helper.ts | 165 ++++++++++++++++++++++++++++++----- e2e/indexer.e2e-spec.ts | 85 ++++++++++-------- package-lock.json | 16 ++++ package.json | 3 +- 5 files changed, 213 insertions(+), 60 deletions(-) diff --git a/e2e/helpers/common.helper.ts b/e2e/helpers/common.helper.ts index 32f138c..0e2b16b 100644 --- a/e2e/helpers/common.helper.ts +++ b/e2e/helpers/common.helper.ts @@ -21,7 +21,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 93eed9b..1f64007 100644 --- a/e2e/helpers/wallet.helper.ts +++ b/e2e/helpers/wallet.helper.ts @@ -7,25 +7,37 @@ 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 { ECPairFactory } from 'ecpair'; initEccLib(ecc); +const 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 type SentTransactionDetails = { transaction: Transaction; txid: string; - blockhash: string; + blockHash: string; }; export class WalletHelper { @@ -53,12 +65,18 @@ export class WalletHelper { return this.bitcoinRPCUtil.mineToAddress(numOfBlocks, walletAddress); } - async addFundToUTXO(payment: Payment, amount): Promise { + async addFundToUTXO( + payment: Payment, + amount: number, + addressType: AddressType, + index: number, + ): Promise { const txid = await this.bitcoinRPCUtil.sendToAddress( payment.address, amount, ); + await this.mineBlock(1); for (let vout = 0; vout < 2; vout++) { const utxo = await this.bitcoinRPCUtil.getTxOut(txid, vout); if ( @@ -71,6 +89,8 @@ export class WalletHelper { vout: vout, value: btcToSats(utxo.value), rawTx: await this.bitcoinRPCUtil.getRawTransaction(txid), + addressType, + index, }; } } @@ -80,26 +100,41 @@ export class WalletHelper { ); } - 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'); } @@ -111,18 +146,18 @@ export class WalletHelper { async craftAndSendTransaction( utxos: UTXO[], - taprootOutput: Payment, + output: Payment, outputValue: number, fee: number, ): Promise { const psbt = new Psbt({ network: networks.regtest }); utxos.forEach((utxo) => { - psbt.addInput({ - hash: utxo.txid, - index: utxo.vout, - nonWitnessUtxo: Buffer.from(utxo.rawTx, 'hex'), - }); + const keyPair = this.root.derivePath( + getDerivationPath(utxo.addressType, utxo.index), + ); + const input = this.createInputFromUtxo(utxo, keyPair); + psbt.addInput(input); }); const totalInputValue = utxos.reduce( @@ -134,15 +169,26 @@ export class WalletHelper { throw new Error('Insufficient funds'); } - psbt.addOutput({ - address: taprootOutput.address, - tapInternalKey: taprootOutput.internalPubkey, + const outputData: any = { + address: output.address, value: btcToSats(outputValue), - }); + }; + + if (output.internalPubkey) { + outputData.tapInternalKey = output.internalPubkey; + } + + psbt.addOutput(outputData); // 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); }); @@ -153,8 +199,85 @@ export class WalletHelper { const txid = await this.bitcoinRPCUtil.sendRawTransaction( transaction.toHex(), ); - const blockhash = (await this.mineBlock(1))[0]; + const blockHash = (await this.mineBlock(1))[0]; - return { transaction, txid, blockhash }; + return { transaction, txid, blockHash }; } + + private createInputFromUtxo(utxo: UTXO, keyPair: BIP32Interface): any { + const input: any = { + hash: utxo.txid, + index: utxo.vout, + }; + + 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; + } + + return input; + } +} + +function getDerivationPath(addressType: AddressType, index: number): string { + switch (addressType) { + case AddressType.P2PKH: + return `m/44'/1'/0'/0/${index}`; + case AddressType.P2SH_P2WPKH: + return `m/49'/1'/0'/0/${index}`; + case AddressType.P2WPKH: + return `m/84'/1'/0'/0/${index}`; + case AddressType.P2TR: + return `m/86'/1'/0'/0/${index}`; + default: + throw new Error('Unsupported address type'); + } +} + +function createTaprootKeyPair(keyPair: BIP32Interface) { + const taprootKeyPair = ECPair.fromPrivateKey(keyPair.privateKey, { + compressed: true, + network: networks.regtest, + }); + + 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 a6bb564..7b773bd 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 { transactionToEntity } from '@e2e/helpers/common.helper'; import { initialiseDep } from '@e2e/setup'; import { ApiHelper } from '@e2e/helpers/api.helper'; @@ -21,46 +21,57 @@ 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[] = []; + it.each(Object.values(AddressType))( + '%s - should ensure that the correct silent block is fetched', + async (addressType) => { + const taprootOutput = walletHelper.generateAddresses( + 1, + AddressType.P2TR, + )[0]; + const outputs = walletHelper.generateAddresses(6, addressType); + const utxos: UTXO[] = []; - for (const output of p2wkhOutputs) { - const utxo = await walletHelper.addFundToUTXO(output, 1); - utxos.push(utxo); - } + for (const [index, output] of outputs.entries()) { + const utxo = await walletHelper.addFundToUTXO( + output, + 1, + addressType, + index, + ); + utxos.push(utxo); + } - const { transaction, txid, blockhash } = - await walletHelper.craftAndSendTransaction( - utxos, - taprootOutput, - 5.999, - 0.001, - ); + const { transaction, txid, blockHash } = + await walletHelper.craftAndSendTransaction( + utxos, + taprootOutput, + 5.999, + 0.001, + ); - const blockCount = await walletHelper.getBlockCount(); - const transformedTransaction = transactionToEntity( - transaction, - txid, - blockhash, - blockCount, - p2wkhOutputs, - ); + const blockCount = await walletHelper.getBlockCount(); + const transformedTransaction = transactionToEntity( + transaction, + txid, + blockHash, + blockCount, + outputs, + ); - const silentBlock = new SilentBlocksService( - {} as any, - {} as any, - ).encodeSilentBlock([transformedTransaction]); + const silentBlock = new SilentBlocksService( + {} as any, + {} as any, + ).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).toEqual(silentBlock); - }); + expect(response.data).toEqual(silentBlock); + }, + ); }); diff --git a/package-lock.json b/package-lock.json index 7d4799a..0eeadd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dockerode": "^4.0.2", + "ecpair": "^2.1.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.26.0", @@ -5584,6 +5585,21 @@ "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, + "license": "MIT", + "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 affae41..fd05f5a 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ "@nestjs/passport": "^10.0.0", "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-socket.io": "^10.3.7", + "@nestjs/platform-ws": "^10.4.4", "@nestjs/schedule": "^3.0.0", "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", - "@nestjs/platform-ws": "^10.4.4", "axios": "^1.7.2", "currency.js": "^2.0.4", "js-yaml": "^4.1.0", @@ -69,6 +69,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dockerode": "^4.0.2", + "ecpair": "^2.1.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.26.0",