diff --git a/contrib/teamcity/build-configurations.yml b/contrib/teamcity/build-configurations.yml --- a/contrib/teamcity/build-configurations.yml +++ b/contrib/teamcity/build-configurations.yml @@ -65,6 +65,8 @@ # the mock-chronik-client library, otherwise unset # - DEPENDS_ECASH_LIB_WASM: "true" if these tests require ecash-lib-wasm # to be built, otherwise unset + # - DEPENDS_CHRONIK_CLIENT: "true" if these tests require chronik-client + # to be build, otherwise unset js-mocha: artifacts: coverage.tar.gz: coverage.tar.gz @@ -97,6 +99,14 @@ ./build-wasm.sh fi + if [ -z "${DEPENDS_CHRONIK_CLIENT+x}" ] ; then + echo "Test does not depend on chronik-client" + else + echo "Test depends on chronik-client. Building TypeScript..." + pushd "${TOPLEVEL}/modules/chronik-client" + npm ci + fi + pushd "${TOPLEVEL}/${JS_PROJECT_ROOT}" MOCHA_JUNIT_DIR="test_results" @@ -158,6 +168,23 @@ PROJECT_NAME="$(basename ${JS_PROJECT_ROOT})" TEST_SUITE_NAME="$(project_to_suite ${PROJECT_NAME})" + # Build ecash-lib-wasm for ecash-lib's WebAssembly part + if [ -z "${DEPENDS_ECASH_LIB_WASM+x}" ] ; then + echo "Test does not depend on ecash-lib-wasm, skipping" + else + echo "Test depends on ecash-lib-wasm. Building WebAssembly..." + pushd "${TOPLEVEL}/modules/ecash-lib-wasm" + ./build-wasm.sh + fi + + if [ -z "${DEPENDS_CHRONIK_CLIENT+x}" ] ; then + echo "Test does not depend on chronik-client" + else + echo "Test depends on chronik-client. Building TypeScript..." + pushd "${TOPLEVEL}/modules/chronik-client" + npm ci + fi + pushd "${TOPLEVEL}/${JS_PROJECT_ROOT}" MOCHA_JUNIT_DIR="test_results" @@ -836,9 +863,26 @@ JS_PROJECT_ROOT: modules/ecash-lib RUN_NPM_BUILD: "true" DEPENDS_ECASH_LIB_WASM: "true" + DEPENDS_CHRONIK_CLIENT: "true" templates: - js-mocha + ecash-lib-integration-tests: + cmake_flags: + - '-DBUILD_BITCOIN_CHRONIK=ON' + targets: + - - all + runOnDiffRegex: + - chronik/ + - modules/chronik-client/ + - modules/ecash-lib/ + env: + JS_PROJECT_ROOT: modules/ecash-lib + DEPENDS_ECASH_LIB_WASM: "true" + DEPENDS_CHRONIK_CLIENT: "true" + templates: + - js-mocha-integration-tests + chronik-client-integration-tests: cmake_flags: - '-DBUILD_BITCOIN_CHRONIK=ON' diff --git a/modules/ecash-lib/package-lock.json b/modules/ecash-lib/package-lock.json --- a/modules/ecash-lib/package-lock.json +++ b/modules/ecash-lib/package-lock.json @@ -1,18 +1,20 @@ { "name": "ecash-lib", - "version": "0.1.0rc", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ecash-lib", - "version": "0.1.0rc", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/chai": "^4.3.14", "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", "chai": "^5.1.0", + "chronik-client": "file:../chronik-client", "eslint-plugin-header": "^3.1.1", "mocha": "^10.4.0", "mocha-junit-reporter": "^2.2.1", @@ -24,6 +26,40 @@ "typescript-eslint": "^7.6.0" } }, + "../chronik-client": { + "version": "0.26.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ws": "^8.2.1", + "axios": "^1.6.3", + "ecashaddrjs": "^1.5.6", + "isomorphic-ws": "^4.0.1", + "protobufjs": "^6.8.8", + "ws": "^8.3.0" + }, + "devDependencies": { + "@types/chai": "^4.2.22", + "@types/chai-as-promised": "^7.1.4", + "@types/mocha": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "eslint": "^8.37.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-header": "^3.1.1", + "mocha": "^9.1.3", + "mocha-junit-reporter": "^2.2.0", + "mocha-suppress-logs": "^0.3.1", + "prettier": "^2.5.1", + "prettier-plugin-organize-imports": "^2.3.4", + "ts-node": "^10.4.0", + "ts-proto": "^1.92.1", + "typedoc": "^0.22.10", + "typescript": "^4.5.2" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1236,7 +1272,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1869,6 +1904,15 @@ "node": ">=12" } }, + "node_modules/chai/node_modules/check-error": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", + "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1906,15 +1950,6 @@ "node": "*" } }, - "node_modules/check-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", - "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1942,6 +1977,10 @@ "fsevents": "~2.3.2" } }, + "node_modules/chronik-client": { + "resolved": "../chronik-client", + "link": true + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4530,8 +4569,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -5571,7 +5609,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, - "peer": true, "requires": { "undici-types": "~5.26.4" } @@ -5986,6 +6023,14 @@ "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" + }, + "dependencies": { + "check-error": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", + "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", + "dev": true + } } }, "chalk": { @@ -6015,12 +6060,6 @@ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true }, - "check-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", - "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", - "dev": true - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6037,6 +6076,36 @@ "readdirp": "~3.6.0" } }, + "chronik-client": { + "version": "file:../chronik-client", + "requires": { + "@types/chai": "^4.2.22", + "@types/chai-as-promised": "^7.1.4", + "@types/mocha": "^9.0.0", + "@types/ws": "^8.2.1", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "axios": "^1.6.3", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "ecashaddrjs": "^1.5.6", + "eslint": "^8.37.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-header": "^3.1.1", + "isomorphic-ws": "^4.0.1", + "mocha": "^9.1.3", + "mocha-junit-reporter": "^2.2.0", + "mocha-suppress-logs": "^0.3.1", + "prettier": "^2.5.1", + "prettier-plugin-organize-imports": "^2.3.4", + "protobufjs": "^6.8.8", + "ts-node": "^10.4.0", + "ts-proto": "^1.92.1", + "typedoc": "^0.22.10", + "typescript": "^4.5.2", + "ws": "^8.3.0" + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7942,8 +8011,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "peer": true + "dev": true }, "update-browserslist-db": { "version": "1.0.13", diff --git a/modules/ecash-lib/package.json b/modules/ecash-lib/package.json --- a/modules/ecash-lib/package.json +++ b/modules/ecash-lib/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "tsc && cp -r ./src/ffi ./dist", "test": "mocha --import=tsx ./src/*.test.ts ./src/**/*.test.ts", + "integration-tests": "mocha --import=tsx ./tests/*.test.ts", "coverage": "nyc npm run test", "junit": "npm run test --reporter mocha-junit-reporter" }, @@ -32,6 +33,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^20.12.7", "chai": "^5.1.0", + "chronik-client": "file:../chronik-client", "eslint-plugin-header": "^3.1.1", "mocha": "^10.4.0", "mocha-junit-reporter": "^2.2.1", diff --git a/modules/ecash-lib/src/index.ts b/modules/ecash-lib/src/index.ts --- a/modules/ecash-lib/src/index.ts +++ b/modules/ecash-lib/src/index.ts @@ -9,6 +9,7 @@ export * from './script.js'; export * from './sighashtype.js'; export * from './tx.js'; +export * from './txBuilder.js'; export * from './unsignedTx.js'; export * from './io/bytes.js'; export * from './io/hex.js'; diff --git a/modules/ecash-lib/src/script.ts b/modules/ecash-lib/src/script.ts --- a/modules/ecash-lib/src/script.ts +++ b/modules/ecash-lib/src/script.ts @@ -55,6 +55,11 @@ return new ScriptOpIter(new Bytes(this.bytecode)); } + /** Create a deep copy of this Script */ + public copy(): Script { + return new Script(new Uint8Array(this.bytecode)); + } + /** * Find the n-th OP_CODESEPARATOR (0-based) and cut out the bytecode * following it. Required for signing BIP143 scripts that have an diff --git a/modules/ecash-lib/src/tx.ts b/modules/ecash-lib/src/tx.ts --- a/modules/ecash-lib/src/tx.ts +++ b/modules/ecash-lib/src/tx.ts @@ -127,3 +127,31 @@ writer.putU64(output.value); output.script.writeWithSize(writer); } + +/** Create a deep copy of the TxInput */ +export function copyTxInput(input: TxInput): TxInput { + return { + prevOut: { + txid: + typeof input.prevOut.txid === 'string' + ? input.prevOut.txid + : new Uint8Array(input.prevOut.txid), + outIdx: input.prevOut.outIdx, + }, + script: input.script.copy(), + sequence: input.sequence, + signData: input.signData && { + value: input.signData.value, + outputScript: input.signData.outputScript?.copy(), + redeemScript: input.signData.redeemScript?.copy(), + }, + }; +} + +/** Create a deep copy of the TxOutput */ +export function copyTxOutput(output: TxOutput): TxOutput { + return { + value: output.value, + script: output.script.copy(), + }; +} diff --git a/modules/ecash-lib/src/txBuilder.ts b/modules/ecash-lib/src/txBuilder.ts new file mode 100644 --- /dev/null +++ b/modules/ecash-lib/src/txBuilder.ts @@ -0,0 +1,226 @@ +// 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 { sha256d } from './hash.js'; +import { Ecc, EccDummy } from './ecc.js'; +import { Script } from './script.js'; +import { SigHashType } from './sighashtype.js'; +import { + Tx, + TxInput, + TxOutput, + copyTxInput, + copyTxOutput, + writeTxOutput, +} from './tx.js'; +import { UnsignedTx, UnsignedTxInput } from './unsignedTx.js'; +import { WriterLength } from './io/writerlength.js'; +import { WriterBytes } from './io/writerbytes.js'; +import { toHex } from './io/hex.js'; + +/** + * Function that contains all the required data to sign a given `input` and + * return the scriptSig. + * + * Use it by attaching a `Signatory` to a TxBuilderInput, e.g. like this for a + * P2PKH input: + * ```ts + * new TxBuilder({ + * inputs: [{ + * input: { prevOut: ... }, + * signatory: P2PKHSignatory(sk, pk, ALL_BIP143), + * }], + * ... + * }) + * ``` + **/ +export type Signatory = (ecc: Ecc, input: UnsignedTxInput) => Script; + +/** Builder input that bundles all the data required to sign a TxInput */ +export interface TxBuilderInput { + input: TxInput; + signatory?: Signatory; +} + +/** + * Output that can either be: + * - `TxOutput`: A full output with a fixed sats amount + * - `Script`: A Script which will receive the leftover sats after fees + */ +export type TxBuilderOutput = TxOutput | Script; + +/** Class that can be used to build and sign txs. */ +export class TxBuilder { + /** nVersion of the resulting Tx */ + public version: number; + /** Inputs that will be signed by the buider */ + public inputs: TxBuilderInput[]; + /** Outputs of the tx, can specify a single leftover output as a Script */ + public outputs: TxBuilderOutput[]; + /** nLockTime of the resulting Tx */ + public locktime: number; + + public constructor(params?: { + version?: number; + inputs?: TxBuilderInput[]; + outputs?: TxBuilderOutput[]; + locktime?: number; + }) { + this.version = params?.version ?? 1; + this.inputs = params?.inputs ?? []; + this.outputs = params?.outputs ?? []; + this.locktime = params?.locktime ?? 0; + } + + /** Calculte sum of all sats coming in, or `undefined` if some unknown. */ + private inputSum(): bigint | undefined { + let inputSum = 0n; + for (const input of this.inputs) { + if (input.input.signData === undefined) { + return undefined; + } + inputSum += BigInt(input.input.signData.value); + } + return inputSum; + } + + private prepareOutputs(): { + fixedOutputSum: bigint; + leftoverIdx: number | undefined; + outputs: TxOutput[]; + } { + let fixedOutputSum = 0n; + let leftoverIdx: number | undefined = undefined; + let outputs: TxOutput[] = new Array(this.outputs.length); + for (let idx = 0; idx < this.outputs.length; ++idx) { + const builderOutput = this.outputs[idx]; + if (builderOutput instanceof Script) { + if (leftoverIdx !== undefined) { + throw 'Multiple leftover outputs, can at most use one'; + } + leftoverIdx = idx; + outputs[idx] = { + value: 0, // placeholder + script: builderOutput.copy(), + }; + } else { + fixedOutputSum += BigInt(builderOutput.value); + outputs[idx] = copyTxOutput(builderOutput); + } + } + return { fixedOutputSum, leftoverIdx, outputs }; + } + + /** Sign the tx built by this builder and return a Tx */ + public sign(ecc: Ecc, feePerKb?: number, dustLimit?: number): Tx { + const { fixedOutputSum, leftoverIdx, outputs } = this.prepareOutputs(); + const inputs = this.inputs.map(input => copyTxInput(input.input)); + if (leftoverIdx !== undefined) { + const inputSum = this.inputSum(); + if (inputSum === undefined) { + throw new Error( + 'Using a leftover output requires setting SignData.value for all inputs', + ); + } + if (feePerKb === undefined) { + throw new Error( + 'Using a leftover output requires setting feePerKb', + ); + } + if (dustLimit === undefined) { + throw new Error( + 'Using a leftover output requires setting dustLimit', + ); + } + const dummyUnsignedTx = UnsignedTx.dummyFromTx( + new Tx({ + version: this.version, + inputs, + outputs, + locktime: this.locktime, + }), + ); + for (let inputIdx = 0; inputIdx < this.inputs.length; ++inputIdx) { + const signatory = this.inputs[inputIdx].signatory; + const input = inputs[inputIdx]; + if (signatory !== undefined) { + // Must use dummy here because ECDSA sigs could be too small + // for fee calc + input.script = signatory( + new EccDummy(), + new UnsignedTxInput({ + inputIdx, + unsignedTx: dummyUnsignedTx, + }), + ); + } + } + let txSize = dummyUnsignedTx.tx.serSize(); + let txFee = calcTxFee(txSize, feePerKb); + const leftoverValue = inputSum - (fixedOutputSum + txFee); + if (leftoverValue < dustLimit) { + // inputs cannot pay for a dust leftover -> deduct size & remove + const outputWriterLen = new WriterLength(); + writeTxOutput(outputs[leftoverIdx], outputWriterLen); + txSize -= outputWriterLen.length; + delete outputs[leftoverIdx]; + txFee = calcTxFee(txSize, feePerKb); + } else { + outputs[leftoverIdx].value = leftoverValue; + } + if (inputSum < fixedOutputSum + txFee) { + throw new Error( + `Insufficient input value (${inputSum}): Can only pay for ${ + inputSum - fixedOutputSum + } fees, but ${txFee} required`, + ); + } + } + const unsignedTx = UnsignedTx.fromTx( + new Tx({ + version: this.version, + inputs, + outputs, + locktime: this.locktime, + }), + ); + for (let inputIdx = 0; inputIdx < this.inputs.length; ++inputIdx) { + const signatory = this.inputs[inputIdx].signatory; + const input = inputs[inputIdx]; + if (signatory !== undefined) { + input.script = signatory( + ecc, + new UnsignedTxInput({ + inputIdx, + unsignedTx, + }), + ); + } + } + return unsignedTx.tx; + } +} + +/** Calculate the required tx fee for the given txSize and feePerKb, + * rounding up */ +export function calcTxFee(txSize: number, feePerKb: number): bigint { + return (BigInt(txSize) * BigInt(feePerKb) + 999n) / 1000n; +} + +/** Signatory for a P2PKH input */ +export const P2PKHSignatory = ( + sk: Uint8Array, + pk: Uint8Array, + sigHashType: SigHashType, +) => { + return (ecc: Ecc, input: UnsignedTxInput): Script => { + const preimage = input.sigHashPreimage(sigHashType); + const sighash = sha256d(preimage.bytes); + const sig = ecc.schnorrSign(sk, sighash); + const sigFlagged = new WriterBytes(sig.length + 1); + sigFlagged.putBytes(sig); + sigFlagged.putU8(sigHashType.toInt() & 0xff); + return Script.p2pkhSpend(pk, sigFlagged.data); + }; +}; diff --git a/modules/ecash-lib/tests/txBuilder.test.ts b/modules/ecash-lib/tests/txBuilder.test.ts new file mode 100644 --- /dev/null +++ b/modules/ecash-lib/tests/txBuilder.test.ts @@ -0,0 +1,216 @@ +// 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 { expect, use } from 'chai'; +import { spawn, ChildProcess } from 'node:child_process'; +import { once, EventEmitter } from 'node:events'; +import fs from 'node:fs/promises'; + +import { ChronikClientNode } from 'chronik-client'; + +import { + TxInput, + TxBuilder, + Script, + Tx, + OP_RETURN, + pushBytesOp, + fromHex, + initWasm, + shaRmd160, + P2PKHSignatory, + ALL_BIP143, + toHex, + ALL_ANYONECANPAY_BIP143, + NONE_BIP143, + NONE_ANYONECANPAY_BIP143, + SINGLE_BIP143, + SINGLE_ANYONECANPAY_BIP143, + EccWasm, + Ecc, +} from '../src/index.js'; + +describe('TxBuilder', () => { + let testRunner: ChildProcess; + let chronikUrl: string; + let ecc: Ecc; + + before(async () => { + const statusEvent = new EventEmitter(); + + testRunner = spawn( + 'python3', + [ + 'test/functional/test_runner.py', + // Place the setup in the python file + 'setup_scripts/ecash-lib_base', + ], + { + 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 + testRunner?.stdout?.pipe(process.stdout); + + testRunner.on('error', function (error) { + console.log('Test runner error, aborting: ' + error); + testRunner.kill(); + process.exit(-1); + }); + + testRunner.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 + }); + + testRunner.on('spawn', function () { + console.log('Test runner started'); + }); + + testRunner.on('message', function (message: any) { + if (message && message.test_info && message.test_info.chronik) { + console.log( + 'Setting chronik url to ', + message.test_info.chronik, + ); + chronikUrl = message.test_info.chronik; + } + + if (message && message.status) { + statusEvent.emit(message.status); + } + }); + + // Can't use `fetch` for local file so we have to read it using `fs` + await initWasm(fs.readFile('./src/ffi/ecash_lib_wasm_bg.wasm')); + ecc = new EccWasm(); + + await once(statusEvent, 'ready'); + }); + + after(() => { + testRunner.send('stop'); + }); + + it('TxBuilder', async () => { + const chronik = new ChronikClientNode([chronikUrl]); + const coinTx = (await chronik.blockTxs(1)).txs[0]; + const coinValue = coinTx.outputs[0].value; + + const sk = fromHex( + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + ); + const pk = ecc.derivePubkey(sk); + const pkh = shaRmd160(pk); + const p2pkh = Script.p2pkh(pkh); + + const tx = new Tx({ + inputs: [ + { + prevOut: { + txid: coinTx.txid, + outIdx: 0, + }, + script: Script.fromOps([pushBytesOp(fromHex('51'))]), + sequence: 0xffffffff, + }, + ], + outputs: [ + { + value: 10000, + script: p2pkh, + }, + { + value: 10000, + script: p2pkh, + }, + { + value: 10000, + script: p2pkh, + }, + { + value: 10000, + script: p2pkh, + }, + { + value: 10000, + script: p2pkh, + }, + { + value: 10000, + script: p2pkh, + }, + { + value: coinValue - 200000, + script: Script.fromOps([ + OP_RETURN, + pushBytesOp(new Uint8Array(100)), + ]), + }, + ], + }); + + const txid = (await chronik.broadcastTx(tx.ser())).txid; + + const sigHashTypes = [ + ALL_BIP143, + ALL_ANYONECANPAY_BIP143, + NONE_BIP143, + NONE_ANYONECANPAY_BIP143, + SINGLE_BIP143, + SINGLE_ANYONECANPAY_BIP143, + ]; + + for (let i = 0; i < sigHashTypes.length; ++i) { + console.log('testing', sigHashTypes[i]); + const txBuild = new TxBuilder({ + inputs: [ + { + input: { + prevOut: { + txid, + outIdx: i, + }, + script: new Script(), + sequence: 0xffffffff, + signData: { + value: 10000, + outputScript: p2pkh, + }, + }, + signatory: P2PKHSignatory(sk, pk, sigHashTypes[i]), + }, + ], + outputs: [ + { + value: 1000, + script: Script.fromOps([ + OP_RETURN, + pushBytesOp(new Uint8Array(100)), + ]), + }, + p2pkh, + ], + }); + const spendTx = txBuild.sign(ecc, 1000, 546); + await chronik.broadcastTx(spendTx.ser()); + } + }); +}); diff --git a/test/functional/setup_scripts/ecash-lib_base.py b/test/functional/setup_scripts/ecash-lib_base.py new file mode 100644 --- /dev/null +++ b/test/functional/setup_scripts/ecash-lib_base.py @@ -0,0 +1,35 @@ +# 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. +""" +Setup script to test the ecash-lib js library +""" + +import pathmagic # noqa +from setup_framework import SetupFramework +from test_framework.address import ADDRESS_ECREG_P2SH_OP_TRUE, ADDRESS_ECREG_UNSPENDABLE + + +class EcashLibSetup(SetupFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [["-chronik"]] + self.ipc_timeout = 10 + + def skip_test_if_missing_module(self): + self.skip_if_no_chronik() + + def run_test(self): + node = self.nodes[0] + mocktime = 1300000000 + node.setmocktime(mocktime) + + self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0] + self.generatetoaddress(node, 100, ADDRESS_ECREG_UNSPENDABLE) + + yield True + + +if __name__ == "__main__": + EcashLibSetup().main()