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 18, 2024
1 parent 2109f3b commit fc24d16
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 60 deletions.
4 changes: 3 additions & 1 deletion e2e/helpers/common.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
165 changes: 144 additions & 21 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,12 +65,18 @@ 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,
);

await this.mineBlock(1);
for (let vout = 0; vout < 2; vout++) {
const utxo = await this.bitcoinRPCUtil.getTxOut(txid, vout);
if (
Expand All @@ -71,6 +89,8 @@ export class WalletHelper {
vout: vout,
value: btcToSats(utxo.value),
rawTx: await this.bitcoinRPCUtil.getRawTransaction(txid),
addressType,
index,
};
}
}
Expand All @@ -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');
}
Expand All @@ -111,18 +146,18 @@ 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({
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(
Expand All @@ -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);
});

Expand All @@ -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;
}
85 changes: 48 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,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>(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);
},
);
});
Loading

0 comments on commit fc24d16

Please sign in to comment.