Skip to content

Commit

Permalink
test: e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Sep 28, 2024
1 parent cbdb41d commit b4c2a11
Show file tree
Hide file tree
Showing 6 changed files with 584 additions and 0 deletions.
127 changes: 127 additions & 0 deletions e2e/helpers/rpc.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import axios, { AxiosRequestConfig } from 'axios';

export class BitcoinRPCUtil {
private readonly url: string;
private readonly axiosConfig: AxiosRequestConfig;

constructor(configPath = './config/e2e.config.yaml') {
const config = yaml.load(readFileSync(configPath, 'utf8')) as Record<string, any>;
const user = config.bitcoinCore.rpcUser;
const password = config.bitcoinCore.rpcPass;
const host = config.bitcoinCore.rpcHost;
const port = config.bitcoinCore.rpcPort;
const protocol = config.bitcoinCore.protocol;
this.url = `${protocol}://${user}:${password}@${host}:${port}/`;
this.axiosConfig = {
method: 'POST',
headers: {
'content-type': 'application/json',
},
};
}

public async request(config: AxiosRequestConfig): Promise<any> {
try {
const response = await axios.request({
...this.axiosConfig,
...config,
url: this.url,
});
return response.data?.result;
} catch (error) {
throw new Error(`Request failed with status code ${error.response.status}`);
}
}

async getBlockHeight(): Promise<number> {
return await this.request({
data: {
method: 'getblockcount',
params: [],
},
});
}

async createWallet(walletName: string): Promise<any> {
return await this.request({
data: {
method: 'createwallet',
params: [walletName],
},
});
}

async loadWallet(walletName: string): Promise<any> {
return await this.request({
data: {
method: 'loadwallet',
params: [walletName],
},
});
}

async getNewAddress(): Promise<any> {
return await this.request({
data: {
method: 'getnewaddress',
params: [],
},
});
}

async mineToAddress(numBlocks: number, address: string): Promise<any> {
return await this.request({
data: {
method: 'generatetoaddress',
params: [numBlocks, address],
},
});
}

async sendToAddress(address: string, amount: number): Promise<any> {
return await this.request({
data: {
method: 'sendtoaddress',
params: [address, amount],
},
});
}

async sendRawTransaction(rawTx: string): Promise<any> {
return await this.request({
data: {
method: 'sendrawtransaction',
params: [rawTx],
},
});
}

async getTxOut(txid: string, vout: number): Promise<any> {
return await this.request({
data: {
method: 'gettxout',
params: [txid, vout],
},
});
}

async getRawTransaction(txid: string): Promise<any> {
return await this.request({
data: {
method: 'getrawtransaction',
params: [txid],
},
});
}

async listUnspent(addresses: string[]): Promise<any> {
return await this.request({
data: {
method: 'listunspent',
params: [addresses],
},
});
}
}
107 changes: 107 additions & 0 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { randomBytes } from 'crypto';
import { mnemonicToSeedSync, generateMnemonic } from 'bip39';
import { BIP32Factory } from 'bip32';
import * as ecc from 'tiny-secp256k1';
import { payments, Psbt } from 'bitcoinjs-lib';

const bip32 = BIP32Factory(ecc);

export class WalletHelper {
private mnemonic: string;
private seed: Buffer;
private root: any;

constructor(mnemonic: string = 'test test test test test test test test test test test test') {
this.mnemonic = mnemonic;
this.seed = mnemonicToSeedSync(this.mnemonic);
this.root = bip32.fromSeed(this.seed);
}

getMnemonic(): string {
return this.mnemonic;
}

generateAddresses(count: number, type: 'p2wpkh' | 'p2wsh' | 'p2tr'): string[] {
const addresses: string[] = [];
for (let i = 0; i < count; i++) {
const path = `m/84'/0'/0'/0/${i}`;
const child = this.root.derivePath(path);
let address: string;

switch (type) {
case 'p2wpkh':
address = payments.p2wpkh({ pubkey: child.publicKey }).address!;
break;
case 'p2wsh':
address = payments.p2wsh({
redeem: payments.p2ms({ m: 2, pubkeys: [child.publicKey, randomBytes(33)] }),
}).address!;
break;
case 'p2tr':
address = payments.p2tr({
internalPubkey: child.publicKey.slice(1, 33),
}).address!;
break;
default:
throw new Error('Unsupported address type');
}

addresses.push(address);
}
return addresses;
}

createWallet(): { mnemonic: string; addresses: string[] } {
const addresses = this.generateAddresses(10, 'p2wpkh');
return { mnemonic: this.mnemonic, addresses };
}

/**
* Craft and sign a transaction sending 6 BTC to the provided Taproot address.
*
* @param utxos - Array of UTXOs to spend from.
* @param taprootAddress - The Taproot address to send to.
* @param fee - The fee to apply in satoshis.
* @returns {string} The raw signed transaction hex.
*/
craftTransaction(
utxos: Array<{ txid: string; vout: number; value: number; rawTx: string }>,
taprootAddress: string
): string {
const psbt = new Psbt();

utxos.forEach((utxo, index) => {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
nonWitnessUtxo: Buffer.from(utxo.rawTx, 'hex'),
});
});

// Add the output to the Taproot address (6 BTC)
const totalInputValue = utxos.reduce((acc, utxo) => acc + utxo.value, 0);
const outputValue = 6 * 1e8;
const fee = 1 * 1e8;

if (totalInputValue < outputValue + fee) {
throw new Error('Insufficient funds');
}

psbt.addOutput({
address: taprootAddress,
value: BigInt(outputValue),
});

// Sign the inputs with the corresponding private keys
utxos.forEach((utxo, index) => {
const child = this.root.derivePath(`m/84'/0'/0'/0/${index}`);
const keyPair = child;
psbt.signInput(index, keyPair);
});

psbt.finalizeAllInputs();

const rawTx = psbt.extractTransaction().toHex();
return rawTx;
}
}
80 changes: 80 additions & 0 deletions e2e/indexer.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { WalletHelper } from '@e2e/helpers/wallet.helper';
import { BitcoinRPCUtil } from '@e2e/helpers/rpc.helper';
import { ApiHelper } from '@e2e/helpers/api.helper';
import { parseSilentBlock } from '@/common/common';

describe('WalletHelper Integration Tests', () => {
let walletHelper: WalletHelper;
let bitcoinRPCUtil: BitcoinRPCUtil;
let apiHelper: ApiHelper;
let initialAddress: string;
let newAddress: string;
let p2wkhAddresses: string[];
let taprootAddress: string;
let utxos: { txid: string, vout: number, value: number, rawTx: string }[];

beforeAll(async () => {
walletHelper = new WalletHelper();
bitcoinRPCUtil = new BitcoinRPCUtil();
apiHelper = new ApiHelper();

await bitcoinRPCUtil.createWallet('test_wallet');
initialAddress = await bitcoinRPCUtil.getNewAddress();

taprootAddress = walletHelper.generateAddresses(1, 'p2tr')[0];
p2wkhAddresses = walletHelper.generateAddresses(8, 'p2wpkh');

await bitcoinRPCUtil.mineToAddress(101, initialAddress);

for (const address of p2wkhAddresses) {
await bitcoinRPCUtil.sendToAddress(address, 1);
}

newAddress = await bitcoinRPCUtil.getNewAddress();
await bitcoinRPCUtil.mineToAddress(6, newAddress);

utxos = [];
for (let i = 0; i < 6; i++) {
const txid = await bitcoinRPCUtil.sendToAddress(p2wkhAddresses[i], 1);
for (let vout = 0; vout < 2; vout++) {
const utxo = await bitcoinRPCUtil.getTxOut(txid, vout);
if (utxo && Math.round(utxo.value * 1e8) === 1e8) {
utxos.push({
txid: txid,
vout: vout,
value: Math.round(utxo.value * 1e8),
rawTx: await bitcoinRPCUtil.getRawTransaction(txid),
});
break;
}
}
}
});

it('should craft and broadcast transactions, then verify them', async () => {
const rawTx = walletHelper.craftTransaction(
utxos.slice(0, 5),
taprootAddress, // Send 6 BTC to taproot address with 1 BTC fee
);

await bitcoinRPCUtil.sendRawTransaction(rawTx);
await bitcoinRPCUtil.mineToAddress(1, initialAddress);

await new Promise(resolve => setTimeout(resolve, 30000));

const response = await apiHelper.get(`/silent-block/height/108`);
const silentBlock = response.data;

const decodedBlock = parseSilentBlock(silentBlock);
const transactions = decodedBlock.transactions;

const foundTx = transactions.find((tx: any) => tx.txid === rawTx);
expect(foundTx).toBeDefined();

expect(foundTx.vout.length).toBeGreaterThan(0);

const output = foundTx.vout.find((vout: any) => vout.address === taprootAddress);
expect(output).toBeDefined();
expect(output.value).toEqual(5 * 1e8);
});
});
Loading

0 comments on commit b4c2a11

Please sign in to comment.