Skip to content

Commit

Permalink
feat: p2pkh, p2tr, p2sh-p2wpkh address support
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Dec 14, 2024
1 parent 2109f3b commit c5e0f8b
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 52 deletions.
135 changes: 121 additions & 14 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,31 @@ 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 = {
Expand Down Expand Up @@ -53,7 +65,7 @@ export class WalletHelper {
return this.bitcoinRPCUtil.mineToAddress(numOfBlocks, walletAddress);
}

async addFundToUTXO(payment: Payment, amount): Promise<UTXO> {
async addFundToUTXO(payment: Payment, amount: number, addressType: AddressType, index: number): Promise<UTXO> {
const txid = await this.bitcoinRPCUtil.sendToAddress(
payment.address,
amount,
Expand All @@ -71,6 +83,8 @@ export class WalletHelper {
vout: vout,
value: btcToSats(utxo.value),
rawTx: await this.bitcoinRPCUtil.getRawTransaction(txid),
addressType,
index,
};
}
}
Expand All @@ -80,26 +94,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');
}
Expand All @@ -111,18 +140,58 @@ export class WalletHelper {

async craftAndSendTransaction(
utxos: UTXO[],
taprootOutput: Payment,
output: Payment,
outputValue: number,
fee: number,
): Promise<SentTransactionDetails> {
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 @@ -135,14 +204,20 @@ 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 @@ -158,3 +233,35 @@ 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;
}

88 changes: 51 additions & 37 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 { transactionToEntity } from '@e2e/helpers/common.helper';
import { initialiseDep } from '@e2e/setup';
import { ApiHelper } from '@e2e/helpers/api.helper';
Expand All @@ -21,46 +21,60 @@ 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.P2WPKH,
AddressType.P2TR,
AddressType.P2PKH,
AddressType.P2SH_P2WPKH,
];

for (const output of p2wkhOutputs) {
const utxo = await walletHelper.addFundToUTXO(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.craftAndSendTransaction(
utxos,
taprootOutput,
5.999,
0.001,
);
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 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);
});
});
});
});
Loading

0 comments on commit c5e0f8b

Please sign in to comment.