Skip to content

Commit

Permalink
test: e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Oct 7, 2024
1 parent cbdb41d commit 09fcedd
Show file tree
Hide file tree
Showing 12 changed files with 858 additions and 82 deletions.
4 changes: 2 additions & 2 deletions config/e2e.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ app:
providerType: BITCOIN_CORE_RPC
bitcoinCore:
protocol: http
rpcHost: localhost
rpcPass: polarpass
rpcHost: 127.0.0.1
rpcPass: password
rpcUser: polaruser
rpcPort: 18443
2 changes: 1 addition & 1 deletion e2e/helpers/api.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export class ApiHelper {
return this.makeRequest<TResponseData>({
method: 'get',
url: `${this.baseUrl}${path}`,
params,
validateStatus: () => true,
...params,
});
}

Expand Down
92 changes: 92 additions & 0 deletions e2e/helpers/common.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Payment, Transaction } from 'bitcoinjs-lib';
import { computeScantweak } from '@/indexer/indexer.service';

export function generateScantweak(
transaction: Transaction,
outputs: Payment[],
): string {
const txid = transaction.getId();

const txin = transaction.ins.map((input, index) => {
const isWitness = transaction.hasWitnesses();

return {
txid: Buffer.from(input.hash).reverse().toString('hex'),
vout: input.index,
scriptSig: isWitness
? ''
: Buffer.from(input.script).toString('hex'),
witness: isWitness
? input.witness.map((v) => Buffer.from(v).toString('hex'))
: undefined,
prevOutScript: Buffer.from(outputs[index].output).toString('hex'),
};
});

const txout = transaction.outs.map((output) => ({
scriptPubKey: Buffer.from(output.script).toString('hex'),
value: Number(output.value),
}));

const scantweak = computeScantweak(txid, txin, txout)[0];

return scantweak.toString('hex');
}

export const varIntSize = (value: number): number => {
if (value < 0xfd) return 1;
else if (value <= 0xffff) return 3;
else if (value <= 0xffffffff) return 5;
else return 9;
};

export const readVarInt = (data: Buffer, cursor: number): number => {
const firstByte = data.readUInt8(cursor);
if (firstByte < 0xfd) {
return firstByte;
} else if (firstByte === 0xfd) {
return data.readUInt16LE(cursor + 1);
} else if (firstByte === 0xfe) {
return data.readUInt32LE(cursor + 1);
} else {
return Number(data.readBigUInt64LE(cursor + 1));
}
};

export const parseSilentBlock = (data: Buffer) => {
const type = data.readUInt8(0);
const transactions = [];

let cursor = 1;
const count = readVarInt(data, cursor);
cursor += varIntSize(count);

for (let i = 0; i < count; i++) {
const txid = data.subarray(cursor, cursor + 32).toString('hex');
cursor += 32;

const outputs = [];
const outputCount = readVarInt(data, cursor);
cursor += varIntSize(outputCount);

for (let j = 0; j < outputCount; j++) {
const value = Number(data.readBigUInt64BE(cursor));
cursor += 8;

const pubkey = data.subarray(cursor, cursor + 32).toString('hex');
cursor += 32;

const vout = data.readUint32BE(cursor);
cursor += 4;

outputs.push({ value, pubkey, vout });
}

const scanTweak = data.subarray(cursor, cursor + 33).toString('hex');
cursor += 33;

transactions.push({ txid, outputs, scanTweak });
}

return { type, transactions };
};
3 changes: 2 additions & 1 deletion e2e/helpers/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ services:
rest=1
rpcbind=0.0.0.0:18443
rpcallowip=0.0.0.0/0
rpcauth=polaruser:29fc7c114646a46c59c029eb076a0967$a985383e5b88d84acf241765c558c408b3fed0ab887575568b4e7fb8e77af6e4
rpcuser=polaruser
rpcpassword=password
debug=1
logips=1
logtimemicros=1
Expand Down
139 changes: 139 additions & 0 deletions e2e/helpers/rpc.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import axios, { AxiosRequestConfig } from 'axios';

type PartialUtxo = {
value: number;
scriptPubKey: {
address: string;
};
};

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}://${host}:${port}/`;
this.axiosConfig = {
method: 'POST',
auth: {
username: user,
password: password,
},
};
}

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: {
jsonrpc: '1.0',
id: 'silent_payment_indexer',
method: 'getblockcount',
params: [],
},
});
}

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

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

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

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<string> {
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<PartialUtxo> {
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],
},
});
}
}
131 changes: 131 additions & 0 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { mnemonicToSeedSync } from 'bip39';
import { BIP32Factory } from 'bip32';
import * as ecc from 'tiny-secp256k1';
import {
initEccLib,
payments,
Psbt,
networks,
Payment,
Transaction,
} from 'bitcoinjs-lib';

initEccLib(ecc);
const bip32 = BIP32Factory(ecc);

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

constructor(
mnemonic = 'select approve zebra athlete happy whisper parrot will yellow fortune demand father',
) {
this.mnemonic = mnemonic;
this.seed = mnemonicToSeedSync(this.mnemonic);
this.root = bip32.fromSeed(this.seed, networks.regtest);
}

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

generateAddresses(
count: number,
type: 'p2wpkh' | 'p2wsh' | 'p2tr',
): 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);
let output;

switch (type) {
case 'p2wpkh':
output = payments.p2wpkh({
pubkey: child.publicKey,
network: networks.regtest,
});
break;
case 'p2tr':
output = payments.p2tr({
internalPubkey: toXOnly(child.publicKey),
network: networks.regtest,
});
break;
default:
throw new Error('Unsupported address type');
}

outputs.push(output);
}
return outputs;
}

createWallet(): { mnemonic: string; addresses: Payment[] } {
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;
}>,
taprootOutput: Payment,
): Transaction {
const psbt = new Psbt({ network: networks.regtest });

utxos.forEach((utxo) => {
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 BTC_TO_SATOSHI = 1e8;
const outputValue = 5.999 * BTC_TO_SATOSHI;
const fee = 0.001 * BTC_TO_SATOSHI;

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

psbt.addOutput({
address: taprootOutput.address,
tapInternalKey: taprootOutput.internalPubkey,
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();

return psbt.extractTransaction(true);
}
}

const toXOnly = (pubKey) =>
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);
Loading

0 comments on commit 09fcedd

Please sign in to comment.