@ecash/lib
Version:
Library for eCash transaction building
210 lines (184 loc) • 7.04 kB
text/typescript
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import type { ChronikClient } from 'chronik-client';
import type { ChildProcess } from 'node:child_process';
import { Ecc } from '../ecc.js';
import { shaRmd160 } from '../hash.js';
import { fromHex, toHex } from '../io/hex.js';
import { pushBytesOp } from '../op.js';
import { OP_1, OP_RETURN } from '../opcode.js';
import { Script } from '../script.js';
import { OutPoint, Tx } from '../tx.js';
import { TxBuilder } from '../txBuilder.js';
const OP_TRUE_SCRIPT = Script.fromOps([OP_1]);
const OP_TRUE_SCRIPT_SIG = Script.fromOps([
pushBytesOp(OP_TRUE_SCRIPT.bytecode),
]);
// Like OP_TRUE_SCRIPT but much bigger to avoid undersize
const ANYONE_SCRIPT = Script.fromOps([pushBytesOp(fromHex('01'.repeat(100)))]);
const ANYONE_SCRIPT_SIG = Script.fromOps([pushBytesOp(ANYONE_SCRIPT.bytecode)]);
export class TestRunner {
public ecc: Ecc;
public runner: ChildProcess;
public chronik: ChronikClient;
private coinsTxid: string | undefined;
private coinValue: number | undefined;
private lastUsedOutIdx: number;
private constructor(
ecc: Ecc,
runner: ChildProcess,
chronik: ChronikClient,
) {
this.ecc = ecc;
this.runner = runner;
this.chronik = chronik;
this.coinsTxid = undefined;
this.lastUsedOutIdx = 0;
}
public static async setup(
setupScript: string = 'setup_scripts/ecash-lib_base',
): Promise<TestRunner> {
const { ChronikClient } = await import('chronik-client');
const { spawn } = await import('node:child_process');
const events = await import('node:events');
const statusEvent = new events.EventEmitter();
const runner = spawn(
'python3',
[
'test/functional/test_runner.py',
// Place the setup in the python file
setupScript,
],
{
stdio: ['ipc'],
// Needs to be set dynamically and the Bitcoin ABC
// node has to be built first.
cwd: process.env.BUILD_DIR || '.',
},
);
// Redirect stdout so we can see the messages from the test runner
runner.stdout?.pipe(process.stdout);
runner.stderr?.pipe(process.stderr);
runner.on('error', function (error) {
console.log('Test runner error, aborting: ' + error);
runner.kill();
process.exit(-1);
});
runner.on('exit', function (code, signal) {
// The test runner failed, make sure to propagate the error
if (code !== null && code !== undefined && code != 0) {
console.log('Test runner completed with code ' + code);
process.exit(code);
}
// The test runner was aborted by a signal, make sure to return an
// error
if (signal !== null && signal !== undefined) {
console.log('Test runner aborted by signal ' + signal);
process.exit(-2);
}
// In all other cases, let the test return its own status as
// expected
});
runner.on('spawn', function () {
console.log('Test runner started');
});
let chronik: ChronikClient | undefined = undefined;
runner.on('message', async function (message: any) {
if (message && message.test_info && message.test_info.chronik) {
console.log(
'Setting chronik url to ',
message.test_info.chronik,
);
chronik = new ChronikClient(message.test_info.chronik);
}
if (message && message.status) {
while (!statusEvent.emit(message.status)) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
});
const ecc = new Ecc();
// We got the coins, can fan out now
await (events as any).once(statusEvent, 'ready');
if (chronik === undefined) {
throw new Event('Chronik is undefined');
}
return new TestRunner(ecc, runner, chronik);
}
public async setupCoins(
numCoins: number,
coinValue: number,
): Promise<void> {
const opTrueScriptHash = shaRmd160(OP_TRUE_SCRIPT.bytecode);
const utxos = (
await this.chronik.script('p2sh', toHex(opTrueScriptHash)).utxos()
).utxos;
const anyoneScriptHash = shaRmd160(ANYONE_SCRIPT.bytecode);
const anyoneP2sh = Script.p2sh(anyoneScriptHash);
const tx = new Tx({
inputs: utxos.map(utxo => ({
prevOut: utxo.outpoint,
script: OP_TRUE_SCRIPT_SIG,
sequence: 0xffffffff,
})),
});
const utxosValue = utxos.reduce((a, b) => a + b.value, 0);
for (let i = 0; i < numCoins; ++i) {
tx.outputs.push({
value: coinValue,
script: anyoneP2sh,
});
}
tx.outputs.push({
value: 0,
script: Script.fromOps([OP_RETURN]),
});
tx.outputs[tx.outputs.length - 1].value =
utxosValue - numCoins * coinValue - tx.serSize();
this.coinsTxid = (await this.chronik.broadcastTx(tx.ser())).txid;
this.coinValue = coinValue;
}
public getOutpoint(): OutPoint {
if (this.coinsTxid === undefined) {
throw new Error('TestRunner.coinsTxid undefined, call setupCoins');
}
return {
txid: this.coinsTxid,
outIdx: this.lastUsedOutIdx++, // use value, then increment
};
}
public async sendToScript(
value: number | number[],
script: Script,
): Promise<string> {
const coinValue = this.coinValue!;
const values = Array.isArray(value) ? value : [value];
const setupTxBuilder = new TxBuilder({
inputs: [
{
input: {
prevOut: this.getOutpoint(),
script: ANYONE_SCRIPT_SIG,
sequence: 0xffffffff,
signData: {
value: coinValue,
},
},
},
],
outputs: [
...values.map(value => ({ value, script })),
Script.fromOps([OP_RETURN]), // burn leftover
],
});
const setupTx = setupTxBuilder.sign(this.ecc, 1000, 546);
return (await this.chronik.broadcastTx(setupTx.ser())).txid;
}
public generate() {
this.runner.send('generate');
}
public stop() {
this.runner.send('stop');
}
}