Skip to content

Commit

Permalink
feat: add support for p2sh-p2wpkh, p2pkh and p2tr
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Nov 25, 2024
1 parent 960dc13 commit 8d6bf7c
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 95 deletions.
4 changes: 3 additions & 1 deletion e2e/helpers/common.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export function generateScanTweak(
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) => ({
Expand Down
145 changes: 128 additions & 17 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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');
Expand Down Expand Up @@ -65,7 +77,12 @@ export class WalletHelper {
return blockhash;
}

async addAmountToAddress(payment: Payment, amount): Promise<UTXO> {
async addAmountToAddress(
payment: Payment,
amount: number,
addressType: AddressType,
index: number,
): Promise<UTXO> {
this.ensureAmountAvailable(amount);

const txid = await this.bitcoinRPCUtil.sendToAddress(
Expand All @@ -87,33 +104,50 @@ export class WalletHelper {
vout: vout,
value: btcToSats(utxo.value),
rawTx: await this.bitcoinRPCUtil.getRawTransaction(txid),
addressType,
index,
};
}
}

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');
}
Expand All @@ -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(
Expand All @@ -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);
});

Expand All @@ -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;
}
87 changes: 52 additions & 35 deletions e2e/indexer.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
});
Loading

0 comments on commit 8d6bf7c

Please sign in to comment.