Skip to content

Commit

Permalink
test: e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Oct 13, 2024
1 parent cbdb41d commit ba1a280
Show file tree
Hide file tree
Showing 10 changed files with 791 additions and 25 deletions.
2 changes: 1 addition & 1 deletion config/e2e.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ providerType: BITCOIN_CORE_RPC
bitcoinCore:
protocol: http
rpcHost: localhost
rpcPass: polarpass
rpcPass: password
rpcUser: polaruser
rpcPort: 18443
4 changes: 2 additions & 2 deletions e2e/helpers/api.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export class ApiHelper {
this.baseUrl = `http://localhost:${config.app.port}`;
}

async get<TResponseData = any>(path: string, params?: any) {
async get<TResponseData = any>(path: string, params?: AxiosRequestConfig) {
return this.makeRequest<TResponseData>({
method: 'get',
url: `${this.baseUrl}${path}`,
params,
validateStatus: () => true,
...params,
});
}

Expand Down
100 changes: 100 additions & 0 deletions e2e/helpers/common.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Payment, Transaction } from 'bitcoinjs-lib';
import { IndexerService } from '@/indexer/indexer.service';
import { SATS_PER_BTC } from '@/common/constants';
import * as currency from 'currency.js';
import { varIntSize } from '@/common/common';

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

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

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

const [scanTweak] = indexerService.computeScanTweak(txid, txin, txout);

return scanTweak.toString('hex');
}

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 interface SilentBlockTransaction {
txid: string;
outputs: {
value: number;
pubkey: string;
vout: number;
}[];
scanTweak: string;
}

export interface SilentBlock {
type: number;
transactions: SilentBlockTransaction[];
}

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

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 };
};

export const btcToSats = (amount: number): number => {
return currency(amount, { precision: 8 }).multiply(SATS_PER_BTC).value;
};
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
135 changes: 135 additions & 0 deletions e2e/helpers/rpc.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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.axiosConfig = {
method: 'POST',
auth: {
username: user,
password: password,
},
url: `${protocol}://${host}:${port}/`,
};
}

public async request(config: AxiosRequestConfig): Promise<any> {
try {
const response = await axios.request({
...this.axiosConfig,
...config,
});
return response.data?.result;
} catch (error) {
if (axios.isAxiosError(error)) {
return {
body: error.response?.data,
status: error.response?.status,
};
} else {
throw error;
}
}
}

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

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

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

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

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

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

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

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

getRawTransaction(txid: string): Promise<any> {
return this.request({
data: {
method: 'getrawtransaction',
params: [txid],
},
});
}
}
126 changes: 126 additions & 0 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { mnemonicToSeedSync } from 'bip39';
import { BIP32Interface, fromSeed } from 'bip32';
import * as ecc from 'tiny-secp256k1';
import {
initEccLib,
payments,
Psbt,
networks,
Payment,
Transaction,
} from 'bitcoinjs-lib';
import { btcToSats } from '@e2e/helpers/common.helper';

initEccLib(ecc);

export type UTXO = {
txid: string;
vout: number;
value: number;
rawTx: string;
};
export class WalletHelper {
private mnemonic: string;
private seed: Buffer;
private root: BIP32Interface;

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

generateAddresses(count: number, type: 'p2wpkh' | '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: Buffer.from(child.publicKey),
network: networks.regtest,
});
break;
case 'p2tr':
output = payments.p2tr({
internalPubkey: Buffer.from(toXOnly(child.publicKey)),
network: networks.regtest,
});
break;
default:
throw new Error('Unsupported address type');
}

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

/**
* Craft and sign a transaction sending 5.999 BTC to the provided Taproot address.
*
* @param utxos - Array of UTXOs to spend from.
* @param taprootOutput - The Taproot output to send to.
* @returns {Transaction} The raw signed transaction.
*/
craftTransaction(utxos: UTXO[], 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 outputValue = btcToSats(5.999);
const fee = btcToSats(0.001);

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

psbt.addOutput({
address: taprootOutput.address,
tapInternalKey: taprootOutput.internalPubkey,
value: 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) => {
if (pubKey.length === 32) {
return pubKey;
}

if (pubKey.length === 33 && (pubKey[0] === 0x02 || pubKey[0] === 0x03)) {
return pubKey.slice(1, 33);
}

throw new Error(
'Invalid public key format. Expected compressed public key or x-only key.',
);
};
Loading

0 comments on commit ba1a280

Please sign in to comment.