From e2f80487941ed3cba0f9200fc272586d06e81dd6 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 10:32:49 +0200 Subject: [PATCH 01/26] Change default of MockNetworkProvider updateUtxoSet to true --- packages/cashscript/src/network/MockNetworkProvider.ts | 4 +--- .../test/e2e/network/MockNetworkProvider.test.ts | 8 ++++---- website/docs/releases/release-notes.md | 5 +++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cashscript/src/network/MockNetworkProvider.ts b/packages/cashscript/src/network/MockNetworkProvider.ts index 667b2ca7..84ed2d43 100644 --- a/packages/cashscript/src/network/MockNetworkProvider.ts +++ b/packages/cashscript/src/network/MockNetworkProvider.ts @@ -13,8 +13,6 @@ interface MockNetworkProviderOptions { updateUtxoSet: boolean; } -// We are setting the default updateUtxoSet to 'false' so that it doesn't break the current behaviour -// TODO: in a future breaking release we want to set this to 'true' by default export default class MockNetworkProvider implements NetworkProvider { // we use lockingBytecode hex as the key for utxoMap to make cash addresses and token addresses interchangeable private utxoSet: Array<[string, Utxo]> = []; @@ -24,7 +22,7 @@ export default class MockNetworkProvider implements NetworkProvider { public options: MockNetworkProviderOptions; constructor(options?: Partial) { - this.options = { updateUtxoSet: false, ...options }; + this.options = { updateUtxoSet: true, ...options }; for (let i = 0; i < 3; i += 1) { this.addUtxo(aliceAddress, randomUtxo()); diff --git a/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts b/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts index e6f8aa19..6f9c2668 100644 --- a/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts +++ b/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts @@ -13,8 +13,8 @@ import { import { describeOrSkip } from '../../test-util.js'; describeOrSkip(!process.env.TESTS_USE_CHIPNET, 'MockNetworkProvider', () => { - describe('when updateUtxoSet is true', () => { - const provider = new MockNetworkProvider({ updateUtxoSet: true }); + describe('when updateUtxoSet is default (true)', () => { + const provider = new MockNetworkProvider(); let p2pkhInstance: Contract; @@ -69,8 +69,8 @@ describeOrSkip(!process.env.TESTS_USE_CHIPNET, 'MockNetworkProvider', () => { }); }); - describe('when updateUtxoSet is default (false)', () => { - const provider = new MockNetworkProvider(); + describe('when updateUtxoSet is set to false', () => { + const provider = new MockNetworkProvider({ updateUtxoSet: false }); let p2pkhInstance: Contract; diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index c0bc251e..121a0847 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -2,6 +2,11 @@ title: Release Notes --- +## v0.12.0 + +#### CashScript SDK +- :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. + ## v0.11.5 #### CashScript SDK From 5b05cacfa5ad3275b3a8abf2981d04b545af55fe Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 10:45:03 +0200 Subject: [PATCH 02/26] Make provider a required option in the Contract constructor --- packages/cashscript/README.md | 2 +- packages/cashscript/src/Contract.ts | 7 +- packages/cashscript/src/interfaces.ts | 2 +- .../test/types/Contract.types.test.ts | 67 ++++++++++--------- website/docs/releases/release-notes.md | 1 + 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/packages/cashscript/README.md b/packages/cashscript/README.md index 7fd1f807..5675a3d0 100644 --- a/packages/cashscript/README.md +++ b/packages/cashscript/README.md @@ -37,7 +37,7 @@ Using the CashScript SDK, you can import contract artifact files, create new ins const provider = new ElectrumNetworkProvider('mainnet'); // Create a new P2PKH contract with constructor arguments: { pkh: pkh } - const contract = new Contract(P2PKH, [pkh], provider); + const contract = new Contract(P2PKH, [pkh], { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index e2baf3d2..8ef7182f 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -22,7 +22,6 @@ import { addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress, } from './utils.js'; import SignatureTemplate from './SignatureTemplate.js'; -import { ElectrumNetworkProvider } from './network/index.js'; import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js'; import semver from 'semver'; @@ -57,10 +56,10 @@ export class Contract< constructor( public artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], - private options?: ContractOptions, + private options: ContractOptions, ) { - this.provider = this.options?.provider ?? new ElectrumNetworkProvider(); - this.addressType = this.options?.addressType ?? 'p2sh32'; + this.provider = this.options.provider; + this.addressType = this.options.addressType ?? 'p2sh32'; const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName', 'compiler']; if (!expectedProperties.every((property) => property in artifact)) { diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index d3876fd3..98544289 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -158,7 +158,7 @@ export interface TransactionDetails extends Transaction { } export interface ContractOptions { - provider?: NetworkProvider, + provider: NetworkProvider, addressType?: AddressType, } diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index e7eb0824..584575dc 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { Artifact, Contract, SignatureTemplate, Transaction, Unlocker } from 'cashscript'; +import { Artifact, Contract, MockNetworkProvider, SignatureTemplate, Transaction, Unlocker } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json' with { type: 'json' }; import announcementArtifact from '../fixture/announcement.artifact'; @@ -32,41 +32,48 @@ interface ManualArtifactType extends Artifact { ] } +// Create a MockNetworkProvider for the tests +const provider = new MockNetworkProvider(); + // describe('P2PKH contract | single constructor input | single function (2 args)') { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(p2pkhArtifact, [alicePkh]); - new Contract(p2pkhArtifact, [binToHex(alicePkh)]); + new Contract(p2pkhArtifact, [alicePkh], { provider }); + new Contract(p2pkhArtifact, [binToHex(alicePkh)], { provider }); // it('should give type errors when using empty constructor inputs') // @ts-expect-error - new Contract(p2pkhArtifact, []); + new Contract(p2pkhArtifact, [], { provider }); // it('should give type errors when using incorrect constructor input type') // @ts-expect-error - new Contract(p2pkhArtifact, [1000n]); + new Contract(p2pkhArtifact, [1000n], { provider }); // it('should give type errors when using incorrect constructor input length') // @ts-expect-error - new Contract(p2pkhArtifact, [alicePkh, 1000n]); + new Contract(p2pkhArtifact, [alicePkh, 1000n], { provider }); // it('should not perform type checking when cast to any') - new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); // it('should perform type checking when manually specifying a type // @ts-expect-error - new Contract(p2pkhArtifactJsonNotConst as any, [alicePkh, 1000n]); + new Contract(p2pkhArtifactJsonNotConst as any, [alicePkh, 1000n], { provider }); + + // it('requires a provider to be passed') + // @ts-expect-error + new Contract(p2pkhArtifact, [alicePkh]); } // describe('Contract unlockers') { - const contract = new Contract(p2pkhArtifact, [alicePkh]); + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); @@ -86,14 +93,14 @@ interface ManualArtifactType extends Artifact { contract.unlock.spend(alicePub); // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); contractAsAny.unlock.notAFunction(); contractAsAny.unlock.spend(); contractAsAny.unlock.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); contractFromUnknown.unlock.notAFunction(); contractFromUnknown.unlock.spend(); contractFromUnknown.unlock.spend(1000n, true); @@ -109,7 +116,7 @@ interface ManualArtifactType extends Artifact { // describe('Contract functions') { - const contract = new Contract(p2pkhArtifact, [alicePkh]); + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); // it('should not give type errors when using correct function inputs') contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)).build(); @@ -129,14 +136,14 @@ interface ManualArtifactType extends Artifact { contract.functions.spend(alicePub); // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); contractAsAny.functions.notAFunction().build(); contractAsAny.functions.spend(); contractAsAny.functions.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); contractFromUnknown.functions.notAFunction().build(); contractFromUnknown.functions.spend(); contractFromUnknown.functions.spend(1000n, true); @@ -152,7 +159,7 @@ interface ManualArtifactType extends Artifact { // describe('Contract unlockers') { - const contract = new Contract(p2pkhArtifact, [alicePkh]); + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)).generateLockingBytecode(); @@ -172,14 +179,14 @@ interface ManualArtifactType extends Artifact { contract.unlock.spend(alicePub); // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); contractAsAny.unlock.notAFunction().generateLockingBytecode(); contractAsAny.unlock.spend(); contractAsAny.unlock.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); contractFromUnknown.unlock.notAFunction().generateLockingBytecode(); contractFromUnknown.unlock.spend(); contractFromUnknown.unlock.spend(1000n, true); @@ -199,21 +206,21 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(announcementArtifact, []); + new Contract(announcementArtifact, [], { provider }); // it('should give type errors when using incorrect constructor input length') // @ts-expect-error - new Contract(announcementArtifact, [1000n]); + new Contract(announcementArtifact, [1000n], { provider }); // it('should give type errors when passing in completely incorrect type') // @ts-expect-error - new Contract(announcementArtifact, 'hello'); + new Contract(announcementArtifact, 'hello', { provider }); } // describe('Contract unlockers') { // it('should not give type errors when using correct function inputs') - const contract = new Contract(announcementArtifact, []); + const contract = new Contract(announcementArtifact, [], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.announce(); @@ -230,7 +237,7 @@ interface ManualArtifactType extends Artifact { // describe('Contract functions') { // it('should not give type errors when using correct function inputs') - const contract = new Contract(announcementArtifact, []); + const contract = new Contract(announcementArtifact, [], { provider }); // it('should not give type errors when using correct function inputs') contract.functions.announce(); @@ -250,17 +257,17 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n], { provider }); // it('should give type errors when using too few constructor inputs') // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)], { provider }); // it('should give type errors when using incorrect constructor input type') // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello']); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello'], { provider }); // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n], { provider }); } } @@ -270,12 +277,12 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); } // describe('Contract unlockers') { - const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.transfer(new SignatureTemplate(alicePriv)); @@ -300,7 +307,7 @@ interface ManualArtifactType extends Artifact { // describe('Contract functions') { - const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); // it('should not give type errors when using correct function inputs') contract.functions.transfer(new SignatureTemplate(alicePriv)); diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 121a0847..d3d8067a 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -6,6 +6,7 @@ title: Release Notes #### CashScript SDK - :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. +- :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. ## v0.11.5 From 6b914d3e812c1c394c6b11e2dd6f45aac6b93c57 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 11:06:05 +0200 Subject: [PATCH 03/26] Remove old transaction builder and remove related occurrances in tests --- packages/cashscript/README.md | 22 +- packages/cashscript/src/Contract.ts | 46 +- packages/cashscript/src/Transaction.ts | 515 ------- packages/cashscript/src/index.ts | 3 +- .../cashscript/src/test/JestExtensions.ts | 35 +- packages/cashscript/test/Contract.test.ts | 35 +- .../test/TransactionBuilder.test.ts | 96 +- packages/cashscript/test/debugging.test.ts | 19 +- .../cashscript/test/e2e/P2PKH-tokens.test.ts | 1 + .../fixture/libauth-template/old-fixtures.ts | 1183 ----------------- .../libauth-template/LibauthTemplate.test.ts | 10 - .../test/types/Contract.types.test.ts | 87 +- website/docs/sdk/transaction-builder.md | 2 - 13 files changed, 47 insertions(+), 2007 deletions(-) delete mode 100644 packages/cashscript/src/Transaction.ts delete mode 100644 packages/cashscript/test/fixture/libauth-template/old-fixtures.ts diff --git a/packages/cashscript/README.md b/packages/cashscript/README.md index 5675a3d0..f2db0b22 100644 --- a/packages/cashscript/README.md +++ b/packages/cashscript/README.md @@ -43,11 +43,23 @@ Using the CashScript SDK, you can import contract artifact files, create new ins console.log('contract address:', contract.address); console.log('contract balance:', await contract.getBalance()); - // Call the spend function with the owner's signature - // And use it to send 0. 000 100 00 BCH back to the contract's address - const txDetails = await contract.functions - .spend(pk, new SignatureTemplate(keypair)) - .to(contract.address, 10000) + const transactionBuilder = new TransactionBuilder({ provider }); + const contractUtxos = await contract.getUtxos(); + + const sendAmount = 10_000n; + const destinationAddress = '... some address ...'; + + // Calculate the change amount, accounting for a miner fee of 1000 satoshis + const changeAmount = contractUtxos[0].satoshis - sendAmount - 1000n; + + // Construct a transaction with the transaction builder + const txDetails = await transactionBuilder + // Add a contract input that spends from the contract using the 'spend' function + .addInput(contractUtxos[0], contract.unlock.spend(pk, new SignatureTemplate(keypair))) + // Add an output that sends 0. 000 100 00 BCH back to the destination address + .addOutput({ to: destinationAddress, amount: sendAmount }) + // Add a change output that sends the change back to the contract's address + .addOutput({ to: contract.address, amount: changeAmount }) .send(); console.log(txDetails); diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 8ef7182f..e145e6d5 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -10,9 +10,8 @@ import { Script, scriptToBytecode, } from '@cashscript/utils'; -import { Transaction } from './Transaction.js'; import { - ConstructorArgument, encodeFunctionArgument, encodeConstructorArguments, encodeFunctionArguments, FunctionArgument, + ConstructorArgument, encodeFunctionArgument, encodeConstructorArguments, FunctionArgument, } from './Argument.js'; import { Unlocker, ContractOptions, GenerateUnlockingBytecodeOptions, Utxo, AddressType, ContractUnlocker, @@ -29,12 +28,10 @@ export class Contract< TArtifact extends Artifact = Artifact, TResolved extends { constructorInputs: ConstructorArgument[]; - functions: Record; unlock: Record; } = { constructorInputs: ParamsToTuple; - functions: AbiToFunctionMap; unlock: AbiToFunctionMap; }, > { @@ -44,10 +41,7 @@ export class Contract< bytecode: string; bytesize: number; opcount: number; - - functions: TResolved['functions']; unlock: TResolved['unlock']; - redeemScript: Script; public provider: NetworkProvider; public addressType: AddressType; @@ -79,21 +73,7 @@ export class Contract< this.redeemScript = generateRedeemScript(asmToScript(this.artifact.bytecode), this.encodedConstructorArgs); - // Populate the functions object with the contract's functions - // (with a special case for single function, which has no "function selector") - this.functions = {}; - if (artifact.abi.length === 1) { - const f = artifact.abi[0]; - // @ts-ignore TODO: see if we can use generics to make TypeScript happy - this.functions[f.name] = this.createFunction(f); - } else { - artifact.abi.forEach((f, i) => { - // @ts-ignore TODO: see if we can use generics to make TypeScript happy - this.functions[f.name] = this.createFunction(f, i); - }); - } - - // Populate the functions object with the contract's functions + // Populate the 'unlock' object with the contract's functions // (with a special case for single function, which has no "function selector") this.unlock = {}; if (artifact.abi.length === 1) { @@ -124,27 +104,6 @@ export class Contract< return this.provider.getUtxos(this.address); } - private createFunction(abiFunction: AbiFunction, selector?: number): ContractFunction { - return (...args: FunctionArgument[]) => { - if (abiFunction.inputs.length !== args.length) { - throw new Error(`Incorrect number of arguments passed to function ${abiFunction.name}. Expected ${abiFunction.inputs.length} arguments (${abiFunction.inputs.map((input) => input.type)}) but got ${args.length}`); - } - - // Encode passed args (this also performs type checking) - const encodedArgs = encodeFunctionArguments(abiFunction, args); - - const unlocker = this.createUnlocker(abiFunction, selector)(...args); - - return new Transaction( - this, - unlocker, - abiFunction, - encodedArgs, - selector, - ); - }; - } - private createUnlocker(abiFunction: AbiFunction, selector?: number): ContractFunctionUnlocker { return (...args: FunctionArgument[]) => { if (abiFunction.inputs.length !== args.length) { @@ -179,5 +138,4 @@ export class Contract< } } -export type ContractFunction = (...args: FunctionArgument[]) => Transaction; type ContractFunctionUnlocker = (...args: FunctionArgument[]) => ContractUnlocker; diff --git a/packages/cashscript/src/Transaction.ts b/packages/cashscript/src/Transaction.ts deleted file mode 100644 index 92d1c3dd..00000000 --- a/packages/cashscript/src/Transaction.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { - hexToBin, - decodeTransaction, - Transaction as LibauthTransaction, - WalletTemplate, -} from '@bitauth/libauth'; -import { - AbiFunction, - encodeBip68, - placeholder, -} from '@cashscript/utils'; -import deepEqual from 'fast-deep-equal'; -import { - Utxo, - Output, - Recipient, - TokenDetails, - NftObject, - isUtxoP2PKH, - TransactionDetails, - Unlocker, - SignatureAlgorithm, -} from './interfaces.js'; -import { - createInputScript, - getInputSize, - createOpReturnOutput, - getTxSizeWithoutInputs, - validateOutput, - utxoComparator, - calculateDust, - getOutputSize, - utxoTokenComparator, - delay, -} from './utils.js'; -import SignatureTemplate from './SignatureTemplate.js'; -import { P2PKH_INPUT_SIZE } from './constants.js'; -import { TransactionBuilder } from './TransactionBuilder.js'; -import { Contract } from './Contract.js'; -import { buildTemplate, getBitauthUri } from './LibauthTemplate.js'; -import { debugTemplate, DebugResults } from './debugging.js'; -import { EncodedFunctionArgument } from './Argument.js'; -import { FailedTransactionError } from './Errors.js'; -import semver from 'semver'; - -export class Transaction { - public inputs: Utxo[] = []; - public outputs: Output[] = []; - - private sequence = 0xfffffffe; - private locktime: number; - private feePerByte: number = 1.0; - private hardcodedFee: bigint; - private minChange: bigint = 0n; - private tokenChange: boolean = true; - - constructor( - public contract: Contract, - private unlocker: Unlocker, - public abiFunction: AbiFunction, - public encodedFunctionArgs: EncodedFunctionArgument[], - private selector?: number, - ) { } - - from(input: Utxo): this; - from(inputs: Utxo[]): this; - - from(inputOrInputs: Utxo | Utxo[]): this { - if (!Array.isArray(inputOrInputs)) { - inputOrInputs = [inputOrInputs]; - } - - this.inputs = this.inputs.concat(inputOrInputs); - - return this; - } - - fromP2PKH(input: Utxo, template: SignatureTemplate): this; - fromP2PKH(inputs: Utxo[], template: SignatureTemplate): this; - - fromP2PKH(inputOrInputs: Utxo | Utxo[], template: SignatureTemplate): this { - if (!Array.isArray(inputOrInputs)) { - inputOrInputs = [inputOrInputs]; - } - - inputOrInputs = inputOrInputs.map((input) => ({ ...input, template })); - - this.inputs = this.inputs.concat(inputOrInputs); - - return this; - } - - to(to: string, amount: bigint, token?: TokenDetails): this; - to(outputs: Recipient[]): this; - - to(toOrOutputs: string | Recipient[], amount?: bigint, token?: TokenDetails): this { - if (typeof toOrOutputs === 'string' && typeof amount === 'bigint') { - const recipient = { to: toOrOutputs, amount, token }; - return this.to([recipient]); - } - - if (Array.isArray(toOrOutputs) && amount === undefined) { - toOrOutputs.forEach(validateOutput); - this.outputs = this.outputs.concat(toOrOutputs); - return this; - } - - throw new Error('Incorrect arguments passed to function \'to\''); - } - - withOpReturn(chunks: string[]): this { - this.outputs.push(createOpReturnOutput(chunks)); - return this; - } - - withAge(age: number): this { - this.sequence = encodeBip68({ blocks: age }); - return this; - } - - withTime(time: number): this { - this.locktime = time; - return this; - } - - withHardcodedFee(hardcodedFee: bigint): this { - this.hardcodedFee = hardcodedFee; - return this; - } - - withFeePerByte(feePerByte: number): this { - this.feePerByte = feePerByte; - return this; - } - - withMinChange(minChange: bigint): this { - this.minChange = minChange; - return this; - } - - withoutChange(): this { - return this.withMinChange(BigInt(Number.MAX_VALUE)); - } - - withoutTokenChange(): this { - this.tokenChange = false; - return this; - } - - async build(): Promise { - this.locktime = this.locktime ?? await this.contract.provider.getBlockHeight(); - await this.setInputsAndOutputs(); - - const builder = new TransactionBuilder({ provider: this.contract.provider }); - - this.inputs.forEach((utxo) => { - if (isUtxoP2PKH(utxo)) { - builder.addInput(utxo, utxo.template.unlockP2PKH(), { sequence: this.sequence }); - } else { - builder.addInput(utxo, this.unlocker, { sequence: this.sequence }); - } - }); - - builder.addOutputs(this.outputs); - builder.setLocktime(this.locktime); - - return builder.build(); - } - - async send(): Promise; - async send(raw: true): Promise; - - async send(raw?: true): Promise { - const tx = await this.build(); - - // Debug the transaction locally before sending so any errors are caught early - await this.debug(); - - try { - const txid = await this.contract.provider.sendRawTransaction(tx); - return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid); - } catch (error: any) { - const reason = error.error ?? error.message ?? error; - throw new FailedTransactionError(reason, await this.bitauthUri()); - } - } - - // method to debug the transaction with libauth VM, throws upon evaluation error - async debug(): Promise { - if (!semver.satisfies(this.contract.artifact.compiler.version, '>=0.11.0')) { - console.warn('For the best debugging experience, please recompile your contract with cashc version 0.11.0 or newer.'); - } - - const template = await this.getLibauthTemplate(); - return debugTemplate(template, [this.contract.artifact]); - } - - async bitauthUri(): Promise { - console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'); - const template = await this.getLibauthTemplate(); - return getBitauthUri(template); - } - - async getLibauthTemplate(): Promise { - return buildTemplate({ transaction: this }); - } - - private async getTxDetails(txid: string): Promise; - private async getTxDetails(txid: string, raw: true): Promise; - - private async getTxDetails(txid: string, raw?: true): Promise { - for (let retries = 0; retries < 1200; retries += 1) { - await delay(500); - try { - const hex = await this.contract.provider.getRawTransaction(txid); - - if (raw) return hex; - - const libauthTransaction = decodeTransaction(hexToBin(hex)) as LibauthTransaction; - return { ...libauthTransaction, txid, hex }; - } catch (ignored) { - // ignored - } - } - - // Should not happen - throw new Error('Could not retrieve transaction details for over 10 minutes'); - } - - private async setInputsAndOutputs(): Promise { - if (this.outputs.length === 0) { - throw new Error('Attempted to build a transaction without outputs'); - } - - // Fetched utxos are only used when no inputs are available, so only fetch in that case. - const allUtxos: Utxo[] = this.inputs.length === 0 - ? await this.contract.provider.getUtxos(this.contract.address) - : []; - - const tokenInputs = this.inputs.length > 0 - ? this.inputs.filter((input) => input.token) - : selectAllTokenUtxos(allUtxos, this.outputs); - - // This throws if the manually selected inputs are not enough to cover the outputs - if (this.inputs.length > 0) { - selectAllTokenUtxos(this.inputs, this.outputs); - } - - if (this.tokenChange) { - const tokenChangeOutputs = createFungibleTokenChangeOutputs( - tokenInputs, this.outputs, this.contract.tokenAddress, - ); - this.outputs.push(...tokenChangeOutputs); - } - - // Construct list with all nfts in inputs - const listNftsInputs: NftObject[] = []; - // If inputs are manually selected, add their tokens to balance - this.inputs.forEach((input) => { - if (!input.token) return; - if (input.token.nft) { - listNftsInputs.push({ ...input.token.nft, category: input.token.category }); - } - }); - // Construct list with all nfts in outputs - let listNftsOutputs: NftObject[] = []; - // Subtract all token outputs from the token balances - this.outputs.forEach((output) => { - if (!output.token) return; - if (output.token.nft) { - listNftsOutputs.push({ ...output.token.nft, category: output.token.category }); - } - }); - // If inputs are manually provided, check token balances - if (this.inputs.length > 0) { - // Compare nfts in- and outputs, check if inputs have nfts corresponding to outputs - // Keep list of nfts in inputs without matching output - // First check immutable nfts, then mutable & minting nfts together - // This is so an immutable input gets matched first and is removed from the list of unused nfts - let unusedNfts = listNftsInputs; - for (const nftInput of listNftsInputs) { - if (nftInput.capability === 'none') { - for (let i = 0; i < listNftsOutputs.length; i += 1) { - // Deep equality check token objects - if (deepEqual(listNftsOutputs[i], nftInput)) { - listNftsOutputs.splice(i, 1); - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - break; - } - } - } - } - for (const nftInput of listNftsInputs) { - if (nftInput.capability === 'minting') { - // eslint-disable-next-line max-len - const newListNftsOutputs: NftObject[] = listNftsOutputs.filter((nftOutput) => nftOutput.category !== nftInput.category); - if (newListNftsOutputs !== listNftsOutputs) { - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - listNftsOutputs = newListNftsOutputs; - } - } - if (nftInput.capability === 'mutable') { - for (let i = 0; i < listNftsOutputs.length; i += 1) { - if (listNftsOutputs[i].category === nftInput.category) { - listNftsOutputs.splice(i, 1); - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - break; - } - } - } - } - for (const nftOutput of listNftsOutputs) { - const genesisUtxo = getTokenGenesisUtxo(this.inputs, nftOutput.category); - if (genesisUtxo) { - listNftsOutputs = listNftsOutputs.filter((nft) => !deepEqual(nft, nftOutput)); - } - } - if (listNftsOutputs.length !== 0) { - throw new Error(`NFT output with token category ${listNftsOutputs[0].category} does not have corresponding input`); - } - if (this.tokenChange) { - for (const unusedNft of unusedNfts) { - const tokenDetails: TokenDetails = { - category: unusedNft.category, - amount: BigInt(0), - nft: { - capability: unusedNft.capability, - commitment: unusedNft.commitment, - }, - }; - const nftChangeOutput = { to: this.contract.tokenAddress, amount: BigInt(1000), token: tokenDetails }; - this.outputs.push(nftChangeOutput); - } - } - } - - // Replace all SignatureTemplate with placeholder Uint8Arrays - const placeholderArgs = this.encodedFunctionArgs.map((arg) => { - if (!(arg instanceof SignatureTemplate)) return arg; - - // Schnorr signatures are *always* 65 bytes: 64 for signature + 1 byte for hashtype. - if (arg.getSignatureAlgorithm() === SignatureAlgorithm.SCHNORR) return placeholder(65); - - // ECDSA signatures are at least 71 bytes: 64 bytes for signature + 1 byte for hashtype + 6 bytes for encoding - // overhead. But it may have up to 2 extra bytes for padding, so we overestimate by 2 bytes. - // (see https://transactionfee.info/charts/bitcoin-script-ecdsa-length/) - return placeholder(73); - }); - - // Create a placeholder input script for size calculation using the placeholder arguments - const placeholderScript = createInputScript( - this.contract.redeemScript, - placeholderArgs, - this.selector, - ); - - // Add one extra byte per input to over-estimate tx-in count - const contractInputSize = getInputSize(placeholderScript) + 1; - - // Note that we use the addPrecision function to add "decimal points" to BigInt numbers - - // Calculate amount to send and base fee (excluding additional fees per UTXO) - let amount = addPrecision(this.outputs.reduce((acc, output) => acc + output.amount, 0n)); - let fee = addPrecision(this.hardcodedFee ?? getTxSizeWithoutInputs(this.outputs) * this.feePerByte); - - // Select and gather UTXOs and calculate fees and available funds - let satsAvailable = 0n; - if (this.inputs.length > 0) { - // If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection - if (!this.hardcodedFee) { - const totalInputSize = this.inputs.reduce( - (acc, input) => acc + (isUtxoP2PKH(input) ? P2PKH_INPUT_SIZE : contractInputSize), - 0, - ); - fee += addPrecision(totalInputSize * this.feePerByte); - } - - satsAvailable = addPrecision(this.inputs.reduce((acc, input) => acc + input.satoshis, 0n)); - } else { - // If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection - const bchUtxos = allUtxos.filter((utxo) => !utxo.token); - - // We sort the UTXOs mainly so there is consistent behaviour between network providers - // even if they report UTXOs in a different order - bchUtxos.sort(utxoComparator).reverse(); - - // Add all automatically added token inputs to the transaction - for (const utxo of tokenInputs) { - this.inputs.push(utxo); - satsAvailable += addPrecision(utxo.satoshis); - if (!this.hardcodedFee) fee += addPrecision(contractInputSize * this.feePerByte); - } - - for (const utxo of bchUtxos) { - if (satsAvailable > amount + fee) break; - this.inputs.push(utxo); - satsAvailable += addPrecision(utxo.satoshis); - if (!this.hardcodedFee) fee += addPrecision(contractInputSize * this.feePerByte); - } - } - - // Remove "decimal points" from BigInt numbers (rounding up for fee, down for others) - satsAvailable = removePrecisionFloor(satsAvailable); - amount = removePrecisionFloor(amount); - fee = removePrecisionCeil(fee); - - // Calculate change and check available funds - let change = satsAvailable - amount - fee; - - if (change < 0) { - throw new Error(`Insufficient funds: available (${satsAvailable}) < needed (${amount + fee}).`); - } - - // Account for the fee of adding a change output - if (!this.hardcodedFee) { - const changeOutputSize = getOutputSize({ to: this.contract.address, amount: 0n }); - change -= BigInt(changeOutputSize * this.feePerByte); - } - - // Add a change output if applicable - const changeOutput = { to: this.contract.address, amount: change }; - if (change >= this.minChange && change >= calculateDust(changeOutput)) { - this.outputs.push(changeOutput); - } - } -} - -const getTokenGenesisUtxo = (utxos: Utxo[], tokenCategory: string): Utxo | undefined => { - const creationUtxo = utxos.find((utxo) => utxo.vout === 0 && utxo.txid === tokenCategory); - return creationUtxo; -}; - -const getTokenCategories = (outputs: Array): string[] => ( - outputs - .filter((output) => output.token) - .map((output) => output.token!.category) -); - -const calculateTotalTokenAmount = (outputs: Array, tokenCategory: string): bigint => ( - outputs - .filter((output) => output.token?.category === tokenCategory) - .reduce((acc, output) => acc + output.token!.amount, 0n) -); - -const selectTokenUtxos = (utxos: Utxo[], amountNeeded: bigint, tokenCategory: string): Utxo[] => { - const genesisUtxo = getTokenGenesisUtxo(utxos, tokenCategory); - if (genesisUtxo) return [genesisUtxo]; - - const tokenUtxos = utxos.filter((utxo) => utxo.token?.category === tokenCategory && utxo.token?.amount > 0n); - - // We sort the UTXOs mainly so there is consistent behaviour between network providers - // even if they report UTXOs in a different order - tokenUtxos.sort(utxoTokenComparator).reverse(); - - let amountAvailable = 0n; - const selectedUtxos: Utxo[] = []; - - // Add token UTXOs until we have enough to cover the amount needed (no fee calculation because it's a token) - for (const utxo of tokenUtxos) { - if (amountAvailable >= amountNeeded) break; - selectedUtxos.push(utxo); - amountAvailable += utxo.token!.amount; - } - - if (amountAvailable < amountNeeded) { - throw new Error(`Insufficient funds for token ${tokenCategory}: available (${amountAvailable}) < needed (${amountNeeded}).`); - } - - return selectedUtxos; -}; - -const selectAllTokenUtxos = (utxos: Utxo[], outputs: Output[]): Utxo[] => { - const tokenCategories = getTokenCategories(outputs); - return tokenCategories.flatMap( - (tokenCategory) => selectTokenUtxos(utxos, calculateTotalTokenAmount(outputs, tokenCategory), tokenCategory), - ); -}; - -const createFungibleTokenChangeOutputs = (utxos: Utxo[], outputs: Output[], address: string): Output[] => { - const tokenCategories = getTokenCategories(utxos); - - const changeOutputs = tokenCategories.map((tokenCategory) => { - const required = calculateTotalTokenAmount(outputs, tokenCategory); - const available = calculateTotalTokenAmount(utxos, tokenCategory); - const change = available - required; - - if (change === 0n) return undefined; - - return { to: address, amount: BigInt(1000), token: { category: tokenCategory, amount: change } }; - }); - - return changeOutputs.filter((output) => output !== undefined) as Output[]; -}; - -// Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers -// It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes -// Also note that multiplication and division between two "decimal" bigints is not supported - -// High precision may not work with some 'number' inputs, so we set the default to 6 "decimal places" -const addPrecision = (amount: number | bigint, precision: number = 6): bigint => { - if (typeof amount === 'number') { - return BigInt(Math.ceil(amount * 10 ** precision)); - } - - return amount * BigInt(10 ** precision); -}; - -const removePrecisionFloor = (amount: bigint, precision: number = 6): bigint => ( - amount / (10n ** BigInt(precision)) -); - -const removePrecisionCeil = (amount: bigint, precision: number = 6): bigint => { - const multiplier = 10n ** BigInt(precision); - return (amount + multiplier - 1n) / multiplier; -}; diff --git a/packages/cashscript/src/index.ts b/packages/cashscript/src/index.ts index a1e87529..4a5633f2 100644 --- a/packages/cashscript/src/index.ts +++ b/packages/cashscript/src/index.ts @@ -1,6 +1,5 @@ export { default as SignatureTemplate } from './SignatureTemplate.js'; -export { Contract, type ContractFunction } from './Contract.js'; -export { Transaction } from './Transaction.js'; +export { Contract } from './Contract.js'; export { TransactionBuilder } from './TransactionBuilder.js'; export { type ConstructorArgument, diff --git a/packages/cashscript/src/test/JestExtensions.ts b/packages/cashscript/src/test/JestExtensions.ts index 929bda94..5f30b703 100644 --- a/packages/cashscript/src/test/JestExtensions.ts +++ b/packages/cashscript/src/test/JestExtensions.ts @@ -33,11 +33,10 @@ expect.extend({ // silence actual stdout output loggerSpy.mockImplementation(() => { }); + // Run debug, ignoring any errors because we only care about the logs, even if the transaction fails try { - executeDebug(transaction); - } catch (error) { - if (error instanceof OldTransactionBuilderError) throw error; - } + transaction.debug(); + } catch (error) { } // We concatenate all the logs into a single string - if no logs are present, we set received to undefined const receivedBase = loggerSpy.mock.calls.reduce((acc, [log]) => `${acc}\n${log}`, '').trim(); @@ -75,14 +74,11 @@ expect.extend({ match: RegExp | string, ): SyncExpectationResult { try { - executeDebug(transaction); - + transaction.debug(); const matcherHint = this.utils.matcherHint('.toFailRequireWith', undefined, match.toString(), { isNot: this.isNot }); const message = (): string => `${matcherHint}\n\nContract function did not fail a require statement.`; return { message, pass: false }; } catch (transactionError: any) { - if (transactionError instanceof OldTransactionBuilderError) throw transactionError; - const matcherHint = this.utils.matcherHint('toFailRequireWith', 'received', 'expected', { isNot: this.isNot }); const expectedText = `Expected pattern: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(match)}`; const receivedText = `Received string: ${this.utils.printReceived(transactionError?.message ?? '')}`; @@ -101,34 +97,13 @@ expect.extend({ transaction: Debuggable, ): SyncExpectationResult { try { - executeDebug(transaction); + transaction.debug(); const message = (): string => 'Contract function did not fail a require statement.'; return { message, pass: false }; } catch (transactionError: any) { - if (transactionError instanceof OldTransactionBuilderError) throw transactionError; - const receivedText = `Received string: ${this.utils.printReceived(transactionError?.message ?? '')}`; const message = (): string => `Contract function failed a require statement.\n${receivedText}`; return { message, pass: true }; } }, }); - - -// Wrapper function with custom error in case people use it with the old transaction builder -// This is a temporary solution until we fully remove the old transaction builder from the SDK -const executeDebug = (transaction: Debuggable): void => { - const debugResults = transaction.debug(); - - if (debugResults instanceof Promise) { - debugResults.catch(() => { }); - throw new OldTransactionBuilderError(); - } -}; - -class OldTransactionBuilderError extends Error { - constructor() { - super('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); - this.name = 'OldTransactionBuilderError'; - } -} diff --git a/packages/cashscript/test/Contract.test.ts b/packages/cashscript/test/Contract.test.ts index a31a16d0..d41baccb 100644 --- a/packages/cashscript/test/Contract.test.ts +++ b/packages/cashscript/test/Contract.test.ts @@ -59,7 +59,7 @@ describe('Contract', () => { const instance = new Contract(p2pkhArtifact, [placeholder(20)], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.spend).toBe('function'); + expect(typeof instance.unlock.spend).toBe('function'); expect(instance.name).toEqual(p2pkhArtifact.contractName); }); @@ -68,8 +68,8 @@ describe('Contract', () => { const instance = new Contract(twtArtifact, [placeholder(65), placeholder(65), 1000000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.transfer).toBe('function'); - expect(typeof instance.functions.timeout).toBe('function'); + expect(typeof instance.unlock.transfer).toBe('function'); + expect(typeof instance.unlock.timeout).toBe('function'); expect(instance.name).toEqual(twtArtifact.contractName); }); @@ -78,7 +78,7 @@ describe('Contract', () => { const instance = new Contract(hodlVaultArtifact, [placeholder(65), placeholder(65), 1000000n, 10000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.spend).toBe('function'); + expect(typeof instance.unlock.spend).toBe('function'); expect(instance.name).toEqual(hodlVaultArtifact.contractName); }); @@ -87,8 +87,8 @@ describe('Contract', () => { const instance = new Contract(mecenasArtifact, [placeholder(20), placeholder(20), 1000000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.receive).toBe('function'); - expect(typeof instance.functions.reclaim).toBe('function'); + expect(typeof instance.unlock.receive).toBe('function'); + expect(typeof instance.unlock.reclaim).toBe('function'); expect(instance.name).toEqual(mecenasArtifact.contractName); }); @@ -136,9 +136,10 @@ describe('Contract', () => { }); }); - describe('Contract functions', () => { + describe('Contract unlockers', () => { let instance: Contract; let bbInstance: Contract; + beforeEach(() => { const provider = new ElectrumNetworkProvider(Network.CHIPNET); instance = new Contract(p2pkhArtifact, [alicePkh], { provider }); @@ -146,22 +147,22 @@ describe('Contract', () => { }); it('can\'t call spend with incorrect signature', () => { - expect(() => instance.functions.spend()).toThrow(); - expect(() => instance.functions.spend(0n, 1n)).toThrow(); - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(alicePriv), 0n)).toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e803'), 1000n)).toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e803000000'), 1000n)).toThrow(); + expect(() => instance.unlock.spend()).toThrow(); + expect(() => instance.unlock.spend(0n, 1n)).toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv), 0n)).toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e803'), 1000n)).toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e803000000'), 1000n)).toThrow(); }); it('can call spend with incorrect arguments', () => { - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(bobPriv))).not.toThrow(); - expect(() => instance.functions.spend(alicePkh, placeholder(65))).not.toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e8031234'), 1000n)).not.toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(bobPriv))).not.toThrow(); + expect(() => instance.unlock.spend(alicePkh, placeholder(65))).not.toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e8031234'), 1000n)).not.toThrow(); }); it('can call spend with correct arguments', () => { - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(alicePriv))).not.toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e8030000'), 1000n)).not.toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))).not.toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e8030000'), 1000n)).not.toThrow(); }); }); }); diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index e2321a5c..da1d7f9d 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -17,7 +17,7 @@ import { utxoComparator, calculateDust, randomUtxo, randomToken, isNonTokenUtxo, import p2pkhArtifact from './fixture/p2pkh.artifact.js'; import twtArtifact from './fixture/transfer_with_timeout.artifact.js'; import { TransactionBuilder } from '../src/TransactionBuilder.js'; -import { gatherUtxos, getTxOutputs } from './test-util.js'; +import { getTxOutputs } from './test-util.js'; import { generateWcTransactionObjectFixture } from './fixture/walletconnect/fixtures.js'; describe('Transaction Builder', () => { @@ -45,100 +45,6 @@ describe('Transaction Builder', () => { (provider as any).addUtxo?.(carolAddress, randomUtxo()); }); - describe('should return the same transaction as the simple transaction builder', () => { - it('for a single-output (+ change) transaction from a single type of contract', async () => { - // given - const to = p2pkhInstance.address; - const amount = 1000n; - const fee = 2000n; - - const utxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); - const { utxos: gathered, total } = gatherUtxos(utxos, { amount, fee }); - - const change = total - amount - fee; - const dustAmount = calculateDust({ to, amount: change }); - - if (change < 0) { - throw new Error('Not enough funds to send transaction'); - } - - // when - const simpleTransaction = await p2pkhInstance.functions - .spend(bobPub, new SignatureTemplate(bobPriv)) - .from(gathered) - .to(to, amount) - .to(change > dustAmount ? [{ to, amount: change }] : []) - .withoutChange() - .withoutTokenChange() - .withTime(0) - .build(); - - const advancedTransaction = new TransactionBuilder({ provider }) - .addInputs(gathered, p2pkhInstance.unlock.spend(bobPub, new SignatureTemplate(bobPriv))) - .addOutput({ to, amount }) - .addOutputs(change > dustAmount ? [{ to, amount: change }] : []) - .build(); - - const simpleDecoded = stringify(decodeTransactionUnsafe(hexToBin(simpleTransaction))); - const advancedDecoded = stringify(decodeTransactionUnsafe(hexToBin(advancedTransaction))); - - // then - expect(advancedDecoded).toEqual(simpleDecoded); - }); - - it('for a multi-output (+ change) transaction with P2SH and P2PKH inputs', async () => { - // given - const to = bobAddress; - const amount = 10000n; - const fee = 2000n; - - const contractUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); - const bobUtxos = await provider.getUtxos(bobAddress); - const bobTemplate = new SignatureTemplate(bobPriv); - - const totalInputUtxos = [...contractUtxos.slice(0, 2), ...bobUtxos.slice(0, 2)]; - const totalInputAmount = totalInputUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0n); - - const change = totalInputAmount - (amount * 2n) - fee; - const dustAmount = calculateDust({ to, amount: change }); - - if (change < 0) { - throw new Error('Not enough funds to send transaction'); - } - - // when - const simpleTransaction = await p2pkhInstance.functions - .spend(bobPub, bobTemplate) - .fromP2PKH(bobUtxos[0], bobTemplate) - .from(contractUtxos[0]) - .fromP2PKH(bobUtxos[1], bobTemplate) - .from(contractUtxos[1]) - .to(to, amount) - .to(to, amount) - .to(change > dustAmount ? [{ to, amount: change }] : []) - .withoutChange() - .withoutTokenChange() - .withTime(0) - .build(); - - const advancedTransaction = new TransactionBuilder({ provider }) - .addInput(bobUtxos[0], bobTemplate.unlockP2PKH()) - .addInput(contractUtxos[0], p2pkhInstance.unlock.spend(bobPub, bobTemplate)) - .addInput(bobUtxos[1], bobTemplate.unlockP2PKH()) - .addInput(contractUtxos[1], p2pkhInstance.unlock.spend(bobPub, bobTemplate)) - .addOutput({ to, amount }) - .addOutput({ to, amount }) - .addOutputs(change > dustAmount ? [{ to, amount: change }] : []) - .build(); - - const simpleDecoded = stringify(decodeTransactionUnsafe(hexToBin(simpleTransaction))); - const advancedDecoded = stringify(decodeTransactionUnsafe(hexToBin(advancedTransaction))); - - // then - expect(advancedDecoded).toEqual(simpleDecoded); - }); - }); - describe('test TransactionBuilder.build', () => { it('should build a transaction that can spend from 2 different contracts and P2PKH + OP_RETURN', async () => { const fee = 1000n; diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index c952140b..adc394c1 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -1,5 +1,5 @@ import { Contract, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; -import { aliceAddress, alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; +import { alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; import '../src/test/JestExtensions.js'; import { randomUtxo } from '../src/utils.js'; import { AuthenticationErrorCommon, binToHex, hexToBin } from '@bitauth/libauth'; @@ -619,22 +619,5 @@ describe('Debugging tests', () => { () => expect(transaction).not.toFailRequire(), ).toThrow(/Contract function failed a require statement\.*\nReceived string: (.|\n)*?1 should equal 2/); }); - - it('should throw an error if the old transaction builder is used', async () => { - const transaction = contractTestRequires.functions.test_require().to(aliceAddress, 1000n); - - // Note: We're wrapping the expect call in another expect, since we expect the inner expect to throw - expect( - () => expect(transaction).toFailRequire(), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); - - expect( - () => expect(transaction).toFailRequireWith('1 should equal 2'), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); - - expect( - () => expect(transaction).toLog('Hello World'), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); - }); }); }); diff --git a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts index e61dc0d7..2ab5f9ce 100644 --- a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts +++ b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts @@ -11,6 +11,7 @@ import { getTxOutputs } from '../test-util.js'; import { Network, TokenDetails, Utxo } from '../../src/interfaces.js'; import artifact from '../fixture/p2pkh.artifact.js'; +// TODO: Replace this with unlockers describe('P2PKH-tokens', () => { let p2pkhInstance: Contract; diff --git a/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts deleted file mode 100644 index ed6ef42a..00000000 --- a/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts +++ /dev/null @@ -1,1183 +0,0 @@ -import { Contract, HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, type Transaction, randomNFT, randomToken, randomUtxo } from '../../../src/index.js'; -import TransferWithTimeout from '../transfer_with_timeout.artifact.js'; -import Mecenas from '../mecenas.artifact.js'; -import P2PKH from '../p2pkh.artifact.js'; -import HoldVault from '../hodl_vault.artifact.js'; -import { aliceAddress, alicePkh, alicePriv, alicePub, bobPkh, bobPriv, bobPub, oracle, oraclePub } from '../vars.js'; -import { WalletTemplate, hexToBin } from '@bitauth/libauth'; - -const provider = new MockNetworkProvider(); - -export interface Fixture { - name: string; - transaction: Transaction; - template: WalletTemplate; -} - -export const fixtures: Fixture[] = [ - { - name: 'TransferWithTimeout (transfer function)', - transaction: (() => { - const contract = new Contract(TransferWithTimeout, [alicePub, bobPub, 100000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .transfer(new SignatureTemplate(bobPriv)) - .to(contract.address, 10000n) - .withoutChange(); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'TransferWithTimeout_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'TransferWithTimeout_parameters', - 'scripts': [ - 'TransferWithTimeout_lock', - 'TransferWithTimeout_unlock', - ], - 'variables': { - 'recipientSig': { - 'description': '"recipientSig" parameter of function "transfer"', - 'name': 'recipientSig', - 'type': 'Key', - }, - 'sender': { - 'description': '"sender" parameter of this contract', - 'name': 'sender', - 'type': 'WalletData', - }, - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'timeout': { - 'description': '"timeout" parameter of this contract', - 'name': 'timeout', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'TransferWithTimeout_unlock': { - 'passes': [ - 'TransferWithTimeout_evaluate', - ], - 'name': 'TransferWithTimeout_unlock', - 'script': '// "transfer" function parameters\n // sig\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'TransferWithTimeout_lock', - }, - 'TransferWithTimeout_lock': { - 'lockingType': 'p2sh32', - 'name': 'TransferWithTimeout_lock', - 'script': "// \"TransferWithTimeout\" contract constructor parameters\n // int = <0xa08601>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* contract TransferWithTimeout( */\n /* pubkey sender, */\n /* pubkey recipient, */\n /* int timeout */\n /* ) { */\n /* // Require recipient's signature to match */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function transfer(sig recipientSig) { */\nOP_4 OP_ROLL OP_ROT OP_CHECKSIG /* require(checkSig(recipientSig, recipient)); */\nOP_NIP OP_NIP OP_NIP OP_ELSE /* } */\n /* */\n /* // Require timeout time to be reached and sender's signature to match */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function timeout(sig senderSig) { */\nOP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, sender)); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */\nOP_2DROP OP_1 /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'TransferWithTimeout_evaluate': { - 'name': 'TransferWithTimeout_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'sender': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'recipient': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'timeout': '0xa08601', - 'function_index': '0', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': expect.any(Number), - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'TransferWithTimeout (timeout function)', - transaction: (() => { - const contract = new Contract(TransferWithTimeout, [alicePub, bobPub, 100000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .timeout(new SignatureTemplate(alicePriv)) - .to(contract.address, 10000n) - .withoutChange(); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'TransferWithTimeout_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'TransferWithTimeout_parameters', - 'scripts': [ - 'TransferWithTimeout_lock', - 'TransferWithTimeout_unlock', - ], - 'variables': { - 'senderSig': { - 'description': '"senderSig" parameter of function "timeout"', - 'name': 'senderSig', - 'type': 'Key', - }, - 'sender': { - 'description': '"sender" parameter of this contract', - 'name': 'sender', - 'type': 'WalletData', - }, - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'timeout': { - 'description': '"timeout" parameter of this contract', - 'name': 'timeout', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'TransferWithTimeout_unlock': { - 'passes': [ - 'TransferWithTimeout_evaluate', - ], - 'name': 'TransferWithTimeout_unlock', - 'script': '// "timeout" function parameters\n // sig\n\n// function index in contract\n // int = <1>\n', - 'unlocks': 'TransferWithTimeout_lock', - }, - 'TransferWithTimeout_lock': { - 'lockingType': 'p2sh32', - 'name': 'TransferWithTimeout_lock', - 'script': "// \"TransferWithTimeout\" contract constructor parameters\n // int = <0xa08601>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* contract TransferWithTimeout( */\n /* pubkey sender, */\n /* pubkey recipient, */\n /* int timeout */\n /* ) { */\n /* // Require recipient's signature to match */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function transfer(sig recipientSig) { */\nOP_4 OP_ROLL OP_ROT OP_CHECKSIG /* require(checkSig(recipientSig, recipient)); */\nOP_NIP OP_NIP OP_NIP OP_ELSE /* } */\n /* */\n /* // Require timeout time to be reached and sender's signature to match */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function timeout(sig senderSig) { */\nOP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, sender)); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */\nOP_2DROP OP_1 /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'TransferWithTimeout_evaluate': { - 'name': 'TransferWithTimeout_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'sender': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'recipient': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'timeout': '0xa08601', - 'function_index': '1', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'senderSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': expect.any(Number), - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'Mecenas', - transaction: (() => { - const contract = new Contract(Mecenas, [alicePkh, bobPkh, 10_000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .receive() - .to(aliceAddress, 10_000n) - .withHardcodedFee(1000n); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'Mecenas_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'Mecenas_parameters', - 'scripts': [ - 'Mecenas_lock', - 'Mecenas_unlock', - ], - 'variables': { - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'funder': { - 'description': '"funder" parameter of this contract', - 'name': 'funder', - 'type': 'WalletData', - }, - 'pledge': { - 'description': '"pledge" parameter of this contract', - 'name': 'pledge', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'Mecenas_unlock': { - 'passes': [ - 'Mecenas_evaluate', - ], - 'name': 'Mecenas_unlock', - 'script': '// "receive" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'Mecenas_lock', - }, - 'Mecenas_lock': { - 'lockingType': 'p2sh32', - 'name': 'Mecenas_lock', - 'script': "// \"Mecenas\" contract constructor parameters\n // int = <0x1027>\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.8.0; */\n /* */\n /* \\/* This is an unofficial CashScript port of Licho's Mecenas contract. It is */\n /* * not compatible with Licho's EC plugin, but rather meant as a demonstration */\n /* * of covenants in CashScript. */\n /* * The time checking has been removed so it can be tested without time requirements. */\n /* *\\/ */\n /* contract Mecenas(bytes20 recipient, bytes20 funder, int pledge\\/*, int period *\\/) { */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function receive() { */\n /* // require(this.age >= period); */\n /* */\n /* // Check that the first output sends to the recipient */\nOP_0 OP_OUTPUTBYTECODE <0x76a914> OP_ROT OP_CAT <0x88ac> OP_CAT OP_EQUALVERIFY /* require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(recipient)); */\n /* */\n<0xe803> /* int minerFee = 1000; */\nOP_INPUTINDEX OP_UTXOVALUE /* int currentValue = tx.inputs[this.activeInputIndex].value; */\nOP_DUP OP_4 OP_PICK OP_SUB OP_2 OP_PICK OP_SUB /* int changeValue = currentValue - pledge - minerFee; */\n /* */\n /* // If there is not enough left for *another* pledge after this one, we send the remainder to the recipient */\n /* // Otherwise we send the remainder to the recipient and the change back to the contract */\nOP_DUP OP_5 OP_PICK OP_4 OP_PICK OP_ADD OP_LESSTHANOREQUAL OP_IF /* if (changeValue <= pledge + minerFee) { */\nOP_0 OP_OUTPUTVALUE OP_2OVER OP_SWAP OP_SUB OP_NUMEQUALVERIFY /* require(tx.outputs[0].value == currentValue - minerFee); */\nOP_ELSE /* } else { */\nOP_0 OP_OUTPUTVALUE OP_5 OP_PICK OP_NUMEQUALVERIFY /* require(tx.outputs[0].value == pledge); */\nOP_1 OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY /* require(tx.outputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode); */\nOP_1 OP_OUTPUTVALUE OP_OVER OP_NUMEQUALVERIFY /* require(tx.outputs[1].value == changeValue); */\nOP_ENDIF /* } */\nOP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE /* } */\n /* */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function reclaim(pubkey pk, sig s) { */\nOP_3 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY /* require(hash160(pk) == funder); */\nOP_2SWAP OP_CHECKSIG /* require(checkSig(s, pk)); */\nOP_NIP OP_NIP /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'Mecenas_evaluate': { - 'name': 'Mecenas_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'recipient': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - 'funder': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', - 'pledge': '0x1027', - 'function_index': '0', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': {}, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', - 'valueSatoshis': 10000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (sending fungible tokens)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - const tokenUtxo = randomUtxo({ satoshis: 1000n, token: randomToken() }); - provider.addUtxo(contract.address, regularUtxo); - provider.addUtxo(contract.address, tokenUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .from(tokenUtxo) - .to(to, amount, tokenUtxo.token); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': {}, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'token': { - 'amount': expect.stringMatching(/^[0-9]+$/), - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - }, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - 'token': { - 'amount': expect.stringMatching(/^[0-9]+$/), - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - }, - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (hardcoded signature)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - provider.addUtxo(contract.address, regularUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const hardcodedSignature = new SignatureTemplate(alicePriv).generateSignature(hexToBin('c0ffee')); - const tx = contract.functions - .spend(alicePub, hardcodedSignature) - .from(regularUtxo) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'WalletData', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig = <0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 's': '0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': {}, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (sending NFTs)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - const nftUtxo = randomUtxo({ satoshis: 1000n, token: randomNFT() }); - provider.addUtxo(contract.address, regularUtxo); - provider.addUtxo(contract.address, nftUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .from(nftUtxo) - .to(to, amount, nftUtxo.token); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': {}, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'token': { - 'amount': '0', - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - 'nft': { - 'capability': 'none', - 'commitment': expect.stringMatching(/^[0-9a-f]{8}$/), - }, - }, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - 'token': { - 'amount': '0', - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - 'nft': { - 'capability': 'none', - 'commitment': expect.stringMatching(/^[0-9a-f]{8}$/), - }, - }, - }, - ], - }, - }, - }, - }, - { - name: 'HodlVault (datasig)', - transaction: (() => { - const contract = new Contract(HoldVault, [alicePub, oraclePub, 99000n, 30000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - // given - const message = oracle.createMessage(100000n, 30000n); - const oracleSig = oracle.signMessage(message); - const to = contract.address; - const amount = 10000n; - - // when - const tx = contract.functions - .spend(new SignatureTemplate(alicePriv), oracleSig, message) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'HodlVault_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'HodlVault_parameters', - 'scripts': [ - 'HodlVault_lock', - 'HodlVault_unlock', - ], - 'variables': { - 'ownerSig': { - 'description': '"ownerSig" parameter of function "spend"', - 'name': 'ownerSig', - 'type': 'Key', - }, - 'oracleSig': { - 'description': '"oracleSig" parameter of function "spend"', - 'name': 'oracleSig', - 'type': 'WalletData', - }, - 'oracleMessage': { - 'description': '"oracleMessage" parameter of function "spend"', - 'name': 'oracleMessage', - 'type': 'WalletData', - }, - 'ownerPk': { - 'description': '"ownerPk" parameter of this contract', - 'name': 'ownerPk', - 'type': 'WalletData', - }, - 'oraclePk': { - 'description': '"oraclePk" parameter of this contract', - 'name': 'oraclePk', - 'type': 'WalletData', - }, - 'minBlock': { - 'description': '"minBlock" parameter of this contract', - 'name': 'minBlock', - 'type': 'WalletData', - }, - 'priceTarget': { - 'description': '"priceTarget" parameter of this contract', - 'name': 'priceTarget', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'HodlVault_unlock': { - 'passes': [ - 'HodlVault_evaluate', - ], - 'name': 'HodlVault_unlock', - 'script': '// "spend" function parameters\n // bytes8 = <0xa086010030750000>\n // datasig = <0x569e137142ebdb96127b727787d605e427a858e8b17dc0605092d0019e5fc9d58810ee74c8ba9f9a5605268c9913e50f780f4c3780e06aea7f50766829895b4b>\n // sig\n', - 'unlocks': 'HodlVault_lock', - }, - 'HodlVault_lock': { - 'lockingType': 'p2sh32', - 'name': 'HodlVault_lock', - 'script': '// "HodlVault" contract constructor parameters\n // int = <0x3075>\n // int = <0xb88201>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* // This contract forces HODLing until a certain price target has been reached */\n /* // A minimum block is provided to ensure that oracle price entries from before this block are disregarded */\n /* // i.e. when the BCH price was $1000 in the past, an oracle entry with the old block number and price can not be used. */\n /* // Instead, a message with a block number and price from after the minBlock needs to be passed. */\n /* // This contract serves as a simple example for checkDataSig-based contracts. */\n /* contract HodlVault( */\n /* pubkey ownerPk, */\n /* pubkey oraclePk, */\n /* int minBlock, */\n /* int priceTarget */\n /* ) { */\n /* function spend(sig ownerSig, datasig oracleSig, bytes8 oracleMessage) { */\n /* // message: { blockHeight, price } */\nOP_6 OP_PICK OP_4 OP_SPLIT /* bytes4 blockHeightBin, bytes4 priceBin = oracleMessage.split(4); */\nOP_SWAP OP_BIN2NUM /* int blockHeight = int(blockHeightBin); */\nOP_SWAP OP_BIN2NUM /* int price = int(priceBin); */\n /* */\n /* // Check that blockHeight is after minBlock and not in the future */\nOP_OVER OP_5 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY /* require(blockHeight >= minBlock); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY OP_DROP /* require(tx.time >= blockHeight); */\n /* */\n /* // Check that current price is at least priceTarget */\nOP_3 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY /* require(price >= priceTarget); */\n /* */\n /* // Handle necessary signature checks */\nOP_3 OP_ROLL OP_4 OP_ROLL OP_3 OP_ROLL OP_CHECKDATASIGVERIFY /* require(checkDataSig(oracleSig, oracleMessage, oraclePk)); */\nOP_CHECKSIG /* require(checkSig(ownerSig, ownerPk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'HodlVault_evaluate': { - 'name': 'HodlVault_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'oracleSig': '0x569e137142ebdb96127b727787d605e427a858e8b17dc0605092d0019e5fc9d58810ee74c8ba9f9a5605268c9913e50f780f4c3780e06aea7f50766829895b4b', - 'oracleMessage': '0xa086010030750000', - 'ownerPk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'oraclePk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'minBlock': '0xb88201', - 'priceTarget': '0x3075', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - // TODO: Make it work with different hashtypes and signature algorithms - // { - // name: 'P2PKH (sending NFTs)', - // transaction: (() => { - // const contract = new Contract(P2PKH, [alicePkh], { provider }); - // provider.addUtxo(contract.address, randomUtxo()); - - // const to = contract.address; - // const amount = 1000n; - - // const hashtype = HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY; - // const signatureAlgorithm = SignatureAlgorithm.ECDSA; - - // const tx = contract.functions - // .spend(alicePub, new SignatureTemplate(alicePriv, hashtype, signatureAlgorithm)) - // .to(to, amount); - - // return tx; - // })(), - // template: {} as any, - // }, - { - name: 'P2PKH (with P2PKH inputs & P2SH20 address type & ECDSA signature algorithm)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider, addressType: 'p2sh20' }); - - const regularUtxo = randomUtxo(); - provider.addUtxo(contract.address, regularUtxo); - - const p2pkhUtxo = randomUtxo(); - provider.addUtxo(aliceAddress, p2pkhUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv, HashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA)) - .fromP2PKH(p2pkhUtxo, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .fromP2PKH(p2pkhUtxo, new SignatureTemplate(bobPriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA)) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - 'p2pkh_placeholder_lock_0', - 'p2pkh_placeholder_unlock_0', - 'p2pkh_placeholder_lock_2', - 'p2pkh_placeholder_unlock_2', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - 'placeholder_key_0': { - 'description': 'placeholder_key_0', - 'name': 'placeholder_key_0', - 'type': 'Key', - }, - 'placeholder_key_2': { - 'description': 'placeholder_key_2', - 'name': 'placeholder_key_2', - 'type': 'Key', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh20', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - 'p2pkh_placeholder_unlock_0': { - 'name': 'p2pkh_placeholder_unlock_0', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_0', - }, - 'p2pkh_placeholder_lock_0': { - 'lockingType': 'standard', - 'name': 'p2pkh_placeholder_lock_0', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_2': { - 'name': 'p2pkh_placeholder_unlock_2', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_2', - }, - 'p2pkh_placeholder_lock_2': { - 'lockingType': 'standard', - 'name': 'p2pkh_placeholder_lock_2', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': { - 'script': 'p2pkh_placeholder_unlock_0', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - }, - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': { - 'script': 'p2pkh_placeholder_unlock_2', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - }, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': { - 'script': 'p2pkh_placeholder_lock_0', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - }, - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': { - 'script': 'p2pkh_placeholder_lock_2', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - }, - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, -]; diff --git a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts index fce99dc7..0a95ca7e 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts @@ -1,5 +1,4 @@ import { fixtures } from '../fixture/libauth-template/fixtures.js'; -import { fixtures as oldFixtures } from '../fixture/libauth-template/old-fixtures.js'; describe('Libauth Template generation tests (single-contract)', () => { fixtures.forEach((fixture) => { @@ -10,13 +9,4 @@ describe('Libauth Template generation tests (single-contract)', () => { expect(generatedTemplate).toEqual(fixture.template); }); }); - // old-fixtures using the deprecated simple transaction builder - oldFixtures.forEach((fixture) => { - it(`should generate a valid libauth template for old-fixture ${fixture.name}`, async () => { - const generatedTemplate = await fixture.transaction.getLibauthTemplate(); - // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(fixture.transaction.bitauthUri()); - expect(generatedTemplate).toEqual(fixture.template); - }); - }); }); diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index 584575dc..587f2be2 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { Artifact, Contract, MockNetworkProvider, SignatureTemplate, Transaction, Unlocker } from 'cashscript'; +import { Artifact, Contract, MockNetworkProvider, SignatureTemplate } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json' with { type: 'json' }; import announcementArtifact from '../fixture/announcement.artifact'; @@ -114,49 +114,6 @@ const provider = new MockNetworkProvider(); contractFromUnknown.unlock.spend().notAFunction(); } - // describe('Contract functions') - { - const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); - - // it('should not give type errors when using correct function inputs') - contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)).build(); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input types') - // @ts-expect-error - contract.functions.spend(1000n, true); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.spend(alicePub, new SignatureTemplate(alicePriv), 100n); - // @ts-expect-error - contract.functions.spend(alicePub); - - // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); - contractAsAny.functions.notAFunction().build(); - contractAsAny.functions.spend(); - contractAsAny.functions.spend(1000n, true); - - // it('should not perform type checking when cannot infer type') - // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); - contractFromUnknown.functions.notAFunction().build(); - contractFromUnknown.functions.spend(); - contractFromUnknown.functions.spend(1000n, true); - - // it('should give type errors when calling methods that do not exist on the returned object') - // @ts-expect-error - contract.functions.spend().notAFunction(); - // @ts-expect-error - contractAsAny.functions.spend().notAFunction(); - // @ts-expect-error - contractFromUnknown.functions.spend().notAFunction(); - } - // describe('Contract unlockers') { const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); @@ -233,23 +190,6 @@ const provider = new MockNetworkProvider(); // @ts-expect-error contract.unlock.announce('hello world'); } - - // describe('Contract functions') - { - // it('should not give type errors when using correct function inputs') - const contract = new Contract(announcementArtifact, [], { provider }); - - // it('should not give type errors when using correct function inputs') - contract.functions.announce(); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.announce('hello world'); - } } // describe('HodlVault contract | 4 constructor inputs | single function (3 args)') @@ -304,29 +244,4 @@ const provider = new MockNetworkProvider(); // @ts-expect-error contract.unlock.timeout(); } - - // describe('Contract functions') - { - const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); - - // it('should not give type errors when using correct function inputs') - contract.functions.transfer(new SignatureTemplate(alicePriv)); - contract.functions.timeout(new SignatureTemplate(alicePriv)); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input types') - // @ts-expect-error - contract.functions.transfer(1000n); - // @ts-expect-error - contract.functions.timeout(true); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.transfer(new SignatureTemplate(alicePub), 100n); - // @ts-expect-error - contract.functions.timeout(); - } } diff --git a/website/docs/sdk/transaction-builder.md b/website/docs/sdk/transaction-builder.md index b3261eb1..009bdd84 100644 --- a/website/docs/sdk/transaction-builder.md +++ b/website/docs/sdk/transaction-builder.md @@ -4,8 +4,6 @@ title: Transaction Builder The CashScript Transaction Builder generalizes transaction building to allow for complex transactions combining multiple different smart contracts within a single transaction or to create basic P2PKH transactions. The Transaction Builder works by adding inputs and outputs to fully specify the transaction shape. -For the documentation for the old and deprecated transaction builder API, refer to [this docs page instead](/docs/sdk/transactions). - :::info Defining the inputs and outputs requires careful consideration because the difference in Bitcoin Cash value between in- and outputs is what's paid in transaction fees to the miners. ::: From 9e30e359cfabfe303fb553661d67c1fa11e5a4f4 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 12:07:01 +0200 Subject: [PATCH 04/26] Merge LibauthTemplate.ts and advanced/Libauthtemplate.ts and remove Transaction.ts imports --- packages/cashscript/src/LibauthTemplate.ts | 502 ------------------ packages/cashscript/src/TransactionBuilder.ts | 3 +- .../src/advanced/LibauthTemplate.ts | 244 ++++++++- packages/cashscript/src/debugging.ts | 2 +- 4 files changed, 217 insertions(+), 534 deletions(-) delete mode 100644 packages/cashscript/src/LibauthTemplate.ts diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts deleted file mode 100644 index dc4ae1e0..00000000 --- a/packages/cashscript/src/LibauthTemplate.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { - AbiFunction, - AbiInput, - Artifact, - bytecodeToScript, - formatBitAuthScript, -} from '@cashscript/utils'; -import { - hexToBin, - WalletTemplateScenarioTransactionOutput, - WalletTemplateScenario, - decodeTransaction, - binToHex, - WalletTemplate, - WalletTemplateScenarioInput, - TransactionBCH, - binToBase64, - utf8ToBin, - isHex, - WalletTemplateScenarioOutput, - WalletTemplateVariable, - WalletTemplateScriptLocking, - WalletTemplateScriptUnlocking, - WalletTemplateScenarioBytecode, -} from '@bitauth/libauth'; -import { deflate } from 'pako'; -import { - Utxo, - isUtxoP2PKH, - TokenDetails, - LibauthTokenDetails, - Output, - AddressType, - SignatureAlgorithm, - HashType, - isUnlockableUtxo, - isStandardUnlockableUtxo, -} from './interfaces.js'; -import SignatureTemplate from './SignatureTemplate.js'; -import { Transaction } from './Transaction.js'; -import { EncodedConstructorArgument, EncodedFunctionArgument } from './Argument.js'; -import { addressToLockScript, extendedStringify, zip } from './utils.js'; -import { Contract } from './Contract.js'; -import { generateUnlockingScriptParams } from './advanced/LibauthTemplate.js'; - -interface BuildTemplateOptions { - transaction: Transaction; - transactionHex?: string; -} - -export const buildTemplate = async ({ - transaction, - transactionHex = undefined, // set this argument to prevent unnecessary call `transaction.build()` -}: BuildTemplateOptions): Promise => { - const contract = transaction.contract; - const txHex = transactionHex ?? await transaction.build(); - - const template = { - $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', - description: 'Imported from cashscript', - name: 'CashScript Generated Debugging Template', - supported: ['BCH_2025_05'], - version: 0, - entities: generateTemplateEntities(contract.artifact, transaction.abiFunction, transaction.encodedFunctionArgs), - scripts: generateTemplateScripts( - contract.artifact, - contract.addressType, - transaction.abiFunction, - transaction.encodedFunctionArgs, - contract.encodedConstructorArgs, - ), - scenarios: generateTemplateScenarios( - contract, - transaction, - txHex, - contract.artifact, - transaction.abiFunction, - transaction.encodedFunctionArgs, - contract.encodedConstructorArgs, - ), - } as WalletTemplate; - - transaction.inputs - .forEach((input, index) => { - if (!isUtxoP2PKH(input)) return; - - const lockScriptName = `p2pkh_placeholder_lock_${index}`; - const unlockScriptName = `p2pkh_placeholder_unlock_${index}`; - const placeholderKeyName = `placeholder_key_${index}`; - - const signatureAlgorithmName = getSignatureAlgorithmName(input.template.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(input.template.getHashType(false)); - const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`; - - template.entities[contract.name + '_parameters'].scripts!.push(lockScriptName, unlockScriptName); - template.entities[contract.name + '_parameters'].variables = { - ...template.entities[contract.name + '_parameters'].variables, - [placeholderKeyName]: { - description: placeholderKeyName, - name: placeholderKeyName, - type: 'Key', - }, - }; - - // add extra unlocking and locking script for P2PKH inputs spent alongside our contract - // this is needed for correct cross-references in the template - template.scripts[unlockScriptName] = { - name: unlockScriptName, - script: - `<${signatureString}>\n<${placeholderKeyName}.public_key>`, - unlocks: lockScriptName, - }; - template.scripts[lockScriptName] = { - lockingType: 'standard', - name: lockScriptName, - script: - `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, - }; - }); - - return template; -}; - -export const getBitauthUri = (template: WalletTemplate): string => { - const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); - const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); - return `https://ide.bitauth.com/import-template/${payload}`; -}; - - -const generateTemplateEntities = ( - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], -): WalletTemplate['entities'] => { - const functionParameters = Object.fromEntries( - abiFunction.inputs.map((input, index) => ([ - input.name, - { - description: `"${input.name}" parameter of function "${abiFunction.name}"`, - name: input.name, - type: encodedFunctionArgs[index] instanceof SignatureTemplate ? 'Key' : 'WalletData', - }, - ])), - ); - - const constructorParameters = Object.fromEntries( - artifact.constructorInputs.map((input) => ([ - input.name, - { - description: `"${input.name}" parameter of this contract`, - name: input.name, - type: 'WalletData', - }, - ])), - ); - - const entities = { - [artifact.contractName + '_parameters']: { - description: 'Contract creation and function parameters', - name: artifact.contractName + '_parameters', - scripts: [ - artifact.contractName + '_lock', - artifact.contractName + '_unlock', - ], - variables: { - ...functionParameters, - ...constructorParameters, - }, - }, - }; - - // function_index is a special variable that indicates the function to execute - if (artifact.abi.length > 1) { - entities[artifact.contractName + '_parameters'].variables.function_index = { - description: 'Script function index to execute', - name: 'function_index', - type: 'WalletData', - }; - } - - return entities; -}; - -const generateTemplateScripts = ( - artifact: Artifact, - addressType: AddressType, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - encodedConstructorArgs: EncodedConstructorArgument[], -): WalletTemplate['scripts'] => { - // definition of locking scripts and unlocking scripts with their respective bytecode - return { - [artifact.contractName + '_unlock']: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs), - [artifact.contractName + '_lock']: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs), - }; -}; - -const generateTemplateLockScript = ( - artifact: Artifact, - addressType: AddressType, - constructorArguments: EncodedFunctionArgument[], -): WalletTemplateScriptLocking => { - return { - lockingType: addressType, - name: artifact.contractName + '_lock', - script: [ - `// "${artifact.contractName}" contract constructor parameters`, - formatParametersForDebugging(artifact.constructorInputs, constructorArguments), - '', - '// bytecode', - formatBytecodeForDebugging(artifact), - ].join('\n'), - }; -}; - -const generateTemplateUnlockScript = ( - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], -): WalletTemplateScriptUnlocking => { - const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name); - - const functionIndexString = artifact.abi.length > 1 - ? ['// function index in contract', ` // int = <${functionIndex}>`, ''] - : []; - - return { - // this unlocking script must pass our only scenario - passes: [artifact.contractName + '_evaluate'], - name: artifact.contractName + '_unlock', - script: [ - `// "${abiFunction.name}" function parameters`, - formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs), - '', - ...functionIndexString, - ].join('\n'), - unlocks: artifact.contractName + '_lock', - }; -}; - -const generateTemplateScenarios = ( - contract: Contract, - transaction: Transaction, - transactionHex: string, - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - encodedConstructorArgs: EncodedConstructorArgument[], -): WalletTemplate['scenarios'] => { - const libauthTransaction = decodeTransaction(hexToBin(transactionHex)); - if (typeof libauthTransaction === 'string') throw Error(libauthTransaction); - - const scenarios = { - // single scenario to spend out transaction under test given the CashScript parameters provided - [artifact.contractName + '_evaluate']: { - name: artifact.contractName + '_evaluate', - description: 'An example evaluation where this script execution passes.', - data: { - // encode values for the variables defined above in `entities` property - bytecode: { - ...generateTemplateScenarioParametersFunctionIndex(abiFunction, artifact.abi), - ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), - ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), - }, - currentBlockHeight: 2, - currentBlockTime: Math.round(+new Date() / 1000), - keys: { - privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), - }, - }, - transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, transaction), - sourceOutputs: generateTemplateScenarioSourceOutputs(transaction), - }, - }; - - return scenarios; -}; - -const generateTemplateScenarioTransaction = ( - contract: Contract, - libauthTransaction: TransactionBCH, - csTransaction: Transaction, -): WalletTemplateScenario['transaction'] => { - const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input)); - - const inputs = libauthTransaction.inputs.map((input, inputIndex) => { - const csInput = csTransaction.inputs[inputIndex] as Utxo; - - return { - outpointIndex: input.outpointIndex, - outpointTransactionHash: binToHex(input.outpointTransactionHash), - sequenceNumber: input.sequenceNumber, - unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', inputIndex === slotIndex), - } as WalletTemplateScenarioInput; - }); - - const locktime = libauthTransaction.locktime; - - const outputs = libauthTransaction.outputs.map((output, index) => { - const csOutput = csTransaction.outputs[index]; - - return { - lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), - token: serialiseTokenDetails(output.token), - valueSatoshis: Number(output.valueSatoshis), - } as WalletTemplateScenarioTransactionOutput; - }); - - const version = libauthTransaction.version; - - return { inputs, locktime, outputs, version }; -}; - -export const generateTemplateScenarioTransactionOutputLockingBytecode = ( - csOutput: Output, - contract: Contract, -): string | {} => { - if (csOutput.to instanceof Uint8Array) return binToHex(csOutput.to); - if ([contract.address, contract.tokenAddress].includes(csOutput.to)) return {}; - return binToHex(addressToLockScript(csOutput.to)); -}; - -const generateTemplateScenarioSourceOutputs = ( - csTransaction: Transaction, -): Array> => { - const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input)); - - return csTransaction.inputs.map((input, inputIndex) => { - return { - lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), - valueSatoshis: Number(input.satoshis), - token: serialiseTokenDetails(input.token), - }; - }); -}; - -// Used for generating the locking / unlocking bytecode for source outputs and inputs -export const generateTemplateScenarioBytecode = ( - input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean, -): WalletTemplateScenarioBytecode | ['slot'] => { - if (insertSlot) return ['slot']; - - const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`; - const placeholderKeyName = `placeholder_key_${inputIndex}`; - - // This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder) - if (isUtxoP2PKH(input)) { - return { - script: p2pkhScriptName, - overrides: { - keys: { - privateKeys: { - [placeholderKeyName]: binToHex(input.template.privateKey), - }, - }, - }, - }; - } - - if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) { - return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex); - } - - // 'slot' means that we are currently evaluating this specific input, - // {} means that it is the same script type, but not being evaluated - return {}; -}; - -export const generateTemplateScenarioParametersValues = ( - types: readonly AbiInput[], - encodedArgs: EncodedFunctionArgument[], -): Record => { - const typesAndArguments = zip(types, encodedArgs); - - const entries = typesAndArguments - // SignatureTemplates are handled by the 'keys' object in the scenario - .filter(([, arg]) => !(arg instanceof SignatureTemplate)) - .map(([input, arg]) => { - const encodedArgumentHex = binToHex(arg as Uint8Array); - const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex); - return [input.name, prefixedEncodedArgument] as const; - }); - - return Object.fromEntries(entries); -}; - -export const generateTemplateScenarioParametersFunctionIndex = ( - abiFunction: AbiFunction, - abi: readonly AbiFunction[], -): Record => { - const functionIndex = abi.length > 1 - ? abi.findIndex((func) => func.name === abiFunction.name) - : undefined; - - return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; -}; - -export const addHexPrefixExceptEmpty = (value: string): string => { - return value.length > 0 ? `0x${value}` : ''; -}; - -export const generateTemplateScenarioKeys = ( - types: readonly AbiInput[], - encodedArgs: EncodedFunctionArgument[], -): Record => { - const typesAndArguments = zip(types, encodedArgs); - - const entries = typesAndArguments - .filter(([, arg]) => arg instanceof SignatureTemplate) - .map(([input, arg]) => ([input.name, binToHex((arg as SignatureTemplate).privateKey)] as const)); - - return Object.fromEntries(entries); -}; - -export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { - if (types.length === 0) return '// none'; - - // We reverse the arguments because the order of the arguments in the bytecode is reversed - const typesAndArguments = zip(types, args).reverse(); - - return typesAndArguments.map(([input, arg]) => { - if (arg instanceof SignatureTemplate) { - const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(arg.getHashType(false)); - return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; - } - - const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; - - // we output these values as pushdata, comment will contain the type and the value of the variable - // e.g. // int = <0xa08601> - return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; - }).join('\n'); -}; - -export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { - const signatureAlgorithmNames = { - [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', - [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', - }; - - return signatureAlgorithmNames[signatureAlgorithm]; -}; - -export const getHashTypeName = (hashType: HashType): string => { - const hashtypeNames = { - [HashType.SIGHASH_ALL]: 'all_outputs', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', - [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', - [HashType.SIGHASH_SINGLE]: 'corresponding_output', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', - [HashType.SIGHASH_NONE]: 'no_outputs', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', - [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', - }; - - return hashtypeNames[hashType]; -}; - -export const formatBytecodeForDebugging = (artifact: Artifact): string => { - if (!artifact.debug) { - return artifact.bytecode - .split(' ') - .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) - .join('\n'); - } - - return formatBitAuthScript( - bytecodeToScript(hexToBin(artifact.debug.bytecode)), - artifact.debug.sourceMap, - artifact.source, - ); -}; - -export const serialiseTokenDetails = ( - token?: TokenDetails | LibauthTokenDetails, -): LibauthTemplateTokenDetails | undefined => { - if (!token) return undefined; - - return { - amount: token.amount.toString(), - category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, - nft: token.nft ? { - capability: token.nft.capability, - commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, - } : undefined, - }; -}; - -export interface LibauthTemplateTokenDetails { - amount: string; - category: string; - nft?: { - capability: 'none' | 'mutable' | 'minting'; - commitment: string; - }; -} diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 2be2413b..27ab2019 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -30,8 +30,7 @@ import { } from './utils.js'; import { FailedTransactionError } from './Errors.js'; import { DebugResults } from './debugging.js'; -import { getBitauthUri } from './LibauthTemplate.js'; -import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js'; +import { debugLibauthTemplate, getLibauthTemplates, getBitauthUri } from './advanced/LibauthTemplate.js'; import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js'; import semver from 'semver'; import { WcTransactionObject } from './walletconnect-utils.js'; diff --git a/packages/cashscript/src/advanced/LibauthTemplate.ts b/packages/cashscript/src/advanced/LibauthTemplate.ts index 5cbeb711..b07b8333 100644 --- a/packages/cashscript/src/advanced/LibauthTemplate.ts +++ b/packages/cashscript/src/advanced/LibauthTemplate.ts @@ -1,7 +1,11 @@ import { + binToBase64, binToHex, decodeCashAddress, + hexToBin, + isHex, TransactionBch, + utf8ToBin, WalletTemplate, WalletTemplateEntity, WalletTemplateScenario, @@ -16,34 +20,32 @@ import { } from '@bitauth/libauth'; import { AbiFunction, + AbiInput, Artifact, + bytecodeToScript, + formatBitAuthScript, } from '@cashscript/utils'; import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from '../Argument.js'; import { Contract } from '../Contract.js'; import { DebugResults, debugTemplate } from '../debugging.js'; import { + HashType, isP2PKHUnlocker, isStandardUnlockableUtxo, + isUnlockableUtxo, + isUtxoP2PKH, + LibauthTokenDetails, + Output, + SignatureAlgorithm, StandardUnlockableUtxo, + TokenDetails, + UnlockableUtxo, Utxo, } from '../interfaces.js'; -import { - addHexPrefixExceptEmpty, - formatBytecodeForDebugging, - formatParametersForDebugging, - generateTemplateScenarioBytecode, - generateTemplateScenarioKeys, - generateTemplateScenarioParametersFunctionIndex, - generateTemplateScenarioParametersValues, - generateTemplateScenarioTransactionOutputLockingBytecode, - getHashTypeName, - getSignatureAlgorithmName, - serialiseTokenDetails, -} from '../LibauthTemplate.js'; import SignatureTemplate from '../SignatureTemplate.js'; -import { Transaction } from '../Transaction.js'; -import { addressToLockScript } from '../utils.js'; +import { addressToLockScript, extendedStringify, zip } from '../utils.js'; import { TransactionBuilder } from '../TransactionBuilder.js'; +import { deflate } from 'pako'; /** @@ -264,7 +266,7 @@ const generateTemplateUnlockScript = ( export const generateTemplateScenarios = ( contract: Contract, libauthTransaction: TransactionBch, - csTransaction: Transaction, + csTransaction: TransactionType, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], inputIndex: number, @@ -295,6 +297,9 @@ export const generateTemplateScenarios = ( }, }; + // TODO: understand exactly what this does, and refactor + // Looks similar to code in generateTemplateScenarioParametersFunctionIndex + // Looks like we just want to use that function and spread in the scenarios data bytecode field if (artifact.abi.length > 1) { const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name); scenarios![scenarioIdentifier].data!.bytecode!.function_index = functionIndex.toString(); @@ -306,7 +311,7 @@ export const generateTemplateScenarios = ( const generateTemplateScenarioTransaction = ( contract: Contract, libauthTransaction: TransactionBch, - csTransaction: Transaction, + csTransaction: TransactionType, slotIndex: number, ): WalletTemplateScenario['transaction'] => { const inputs = libauthTransaction.inputs.map((input, inputIndex) => { @@ -338,7 +343,7 @@ const generateTemplateScenarioTransaction = ( }; const generateTemplateScenarioSourceOutputs = ( - csTransaction: Transaction, + csTransaction: TransactionType, slotIndex: number, ): Array> => { return csTransaction.inputs.map((input, inputIndex) => { @@ -350,15 +355,7 @@ const generateTemplateScenarioSourceOutputs = ( }); }; - -/** - * Creates a transaction object from a TransactionBuilder instance - * - * @param txn - The TransactionBuilder instance to convert - * @returns A transaction object containing inputs, outputs, locktime and version - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const createCSTransaction = (txn: TransactionBuilder) => { +const createTransactionTypeFromTransactionBuilder = (txn: TransactionBuilder): TransactionType => { const csTransaction = { inputs: txn.inputs, locktime: txn.locktime, @@ -369,6 +366,13 @@ const createCSTransaction = (txn: TransactionBuilder) => { return csTransaction; }; +interface TransactionType { + inputs: UnlockableUtxo[]; + locktime: number; + outputs: Output[]; + version: number; +} + export const getLibauthTemplates = ( txn: TransactionBuilder, ): WalletTemplate => { @@ -377,7 +381,7 @@ export const getLibauthTemplates = ( } const libauthTransaction = txn.buildLibauthTransaction(); - const csTransaction = createCSTransaction(txn); + const csTransaction = createTransactionTypeFromTransactionBuilder(txn); const baseTemplate: WalletTemplate = { $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', @@ -435,7 +439,7 @@ export const getLibauthTemplates = ( generateTemplateScenarios( contract, libauthTransaction, - csTransaction as any, + csTransaction, abiFunction, encodedArgs, inputIndex, @@ -617,3 +621,185 @@ const getLockScriptName = (contract: Contract): string => { const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, inputIndex: number): string => { return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; }; + +export const getBitauthUri = (template: WalletTemplate): string => { + const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); + const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); + return `https://ide.bitauth.com/import-template/${payload}`; +}; + +export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { + const signatureAlgorithmNames = { + [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', + [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', + }; + + return signatureAlgorithmNames[signatureAlgorithm]; +}; + +export const getHashTypeName = (hashType: HashType): string => { + const hashtypeNames = { + [HashType.SIGHASH_ALL]: 'all_outputs', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', + [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', + [HashType.SIGHASH_SINGLE]: 'corresponding_output', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', + [HashType.SIGHASH_NONE]: 'no_outputs', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', + [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', + }; + + return hashtypeNames[hashType]; +}; + +export const addHexPrefixExceptEmpty = (value: string): string => { + return value.length > 0 ? `0x${value}` : ''; +}; + + +export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { + if (types.length === 0) return '// none'; + + // We reverse the arguments because the order of the arguments in the bytecode is reversed + const typesAndArguments = zip(types, args).reverse(); + + return typesAndArguments.map(([input, arg]) => { + if (arg instanceof SignatureTemplate) { + const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); + const hashtypeName = getHashTypeName(arg.getHashType(false)); + return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; + } + + const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; + + // we output these values as pushdata, comment will contain the type and the value of the variable + // e.g. // int = <0xa08601> + return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; + }).join('\n'); +}; + +export const formatBytecodeForDebugging = (artifact: Artifact): string => { + if (!artifact.debug) { + return artifact.bytecode + .split(' ') + .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) + .join('\n'); + } + + return formatBitAuthScript( + bytecodeToScript(hexToBin(artifact.debug.bytecode)), + artifact.debug.sourceMap, + artifact.source, + ); +}; + +export const serialiseTokenDetails = ( + token?: TokenDetails | LibauthTokenDetails, +): LibauthTemplateTokenDetails | undefined => { + if (!token) return undefined; + + return { + amount: token.amount.toString(), + category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, + nft: token.nft ? { + capability: token.nft.capability, + commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, + } : undefined, + }; +}; + +export const generateTemplateScenarioParametersValues = ( + types: readonly AbiInput[], + encodedArgs: EncodedFunctionArgument[], +): Record => { + const typesAndArguments = zip(types, encodedArgs); + + const entries = typesAndArguments + // SignatureTemplates are handled by the 'keys' object in the scenario + .filter(([, arg]) => !(arg instanceof SignatureTemplate)) + .map(([input, arg]) => { + const encodedArgumentHex = binToHex(arg as Uint8Array); + const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex); + return [input.name, prefixedEncodedArgument] as const; + }); + + return Object.fromEntries(entries); +}; + +export const generateTemplateScenarioKeys = ( + types: readonly AbiInput[], + encodedArgs: EncodedFunctionArgument[], +): Record => { + const typesAndArguments = zip(types, encodedArgs); + + const entries = typesAndArguments + .filter(([, arg]) => arg instanceof SignatureTemplate) + .map(([input, arg]) => ([input.name, binToHex((arg as SignatureTemplate).privateKey)] as const)); + + return Object.fromEntries(entries); +}; + +// Used for generating the locking / unlocking bytecode for source outputs and inputs +export const generateTemplateScenarioBytecode = ( + input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean, +): WalletTemplateScenarioBytecode | ['slot'] => { + if (insertSlot) return ['slot']; + + const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`; + const placeholderKeyName = `placeholder_key_${inputIndex}`; + + // This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder) + if (isUtxoP2PKH(input)) { + return { + script: p2pkhScriptName, + overrides: { + keys: { + privateKeys: { + [placeholderKeyName]: binToHex(input.template.privateKey), + }, + }, + }, + }; + } + + if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) { + return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex); + } + + // 'slot' means that we are currently evaluating this specific input, + // {} means that it is the same script type, but not being evaluated + return {}; +}; + +export const generateTemplateScenarioTransactionOutputLockingBytecode = ( + csOutput: Output, + contract: Contract, +): string | {} => { + if (csOutput.to instanceof Uint8Array) return binToHex(csOutput.to); + if ([contract.address, contract.tokenAddress].includes(csOutput.to)) return {}; + return binToHex(addressToLockScript(csOutput.to)); +}; + +export const generateTemplateScenarioParametersFunctionIndex = ( + abiFunction: AbiFunction, + abi: readonly AbiFunction[], +): Record => { + const functionIndex = abi.length > 1 + ? abi.findIndex((func) => func.name === abiFunction.name) + : undefined; + + return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; +}; + +export interface LibauthTemplateTokenDetails { + amount: string; + category: string; + nft?: { + capability: 'none' | 'mutable' | 'minting'; + commitment: string; + }; +} diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index 15a6daba..f15041ab 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -2,7 +2,7 @@ import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationPro import { Artifact, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils'; import { findLastIndex, toRegExp } from './utils.js'; import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js'; -import { getBitauthUri } from './LibauthTemplate.js'; +import { getBitauthUri } from './advanced/LibauthTemplate.js'; export type DebugResult = AuthenticationProgramStateCommon[]; export type DebugResults = Record; From ba8fa22c680922dc095ce539a7c44638573c03c6 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 12:09:09 +0200 Subject: [PATCH 05/26] Move LibauthTemplate.ts from src/advanced/ to src/ --- .../src/{advanced => }/LibauthTemplate.ts | 14 +++++++------- packages/cashscript/src/TransactionBuilder.ts | 2 +- packages/cashscript/src/debugging.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/cashscript/src/{advanced => }/LibauthTemplate.ts (98%) diff --git a/packages/cashscript/src/advanced/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts similarity index 98% rename from packages/cashscript/src/advanced/LibauthTemplate.ts rename to packages/cashscript/src/LibauthTemplate.ts index b07b8333..3dbeae33 100644 --- a/packages/cashscript/src/advanced/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -25,9 +25,9 @@ import { bytecodeToScript, formatBitAuthScript, } from '@cashscript/utils'; -import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from '../Argument.js'; -import { Contract } from '../Contract.js'; -import { DebugResults, debugTemplate } from '../debugging.js'; +import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from './Argument.js'; +import { Contract } from './Contract.js'; +import { DebugResults, debugTemplate } from './debugging.js'; import { HashType, isP2PKHUnlocker, @@ -41,10 +41,10 @@ import { TokenDetails, UnlockableUtxo, Utxo, -} from '../interfaces.js'; -import SignatureTemplate from '../SignatureTemplate.js'; -import { addressToLockScript, extendedStringify, zip } from '../utils.js'; -import { TransactionBuilder } from '../TransactionBuilder.js'; +} from './interfaces.js'; +import SignatureTemplate from './SignatureTemplate.js'; +import { addressToLockScript, extendedStringify, zip } from './utils.js'; +import { TransactionBuilder } from './TransactionBuilder.js'; import { deflate } from 'pako'; diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 27ab2019..4a8303d3 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -30,7 +30,7 @@ import { } from './utils.js'; import { FailedTransactionError } from './Errors.js'; import { DebugResults } from './debugging.js'; -import { debugLibauthTemplate, getLibauthTemplates, getBitauthUri } from './advanced/LibauthTemplate.js'; +import { debugLibauthTemplate, getLibauthTemplates, getBitauthUri } from './LibauthTemplate.js'; import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js'; import semver from 'semver'; import { WcTransactionObject } from './walletconnect-utils.js'; diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index f15041ab..15a6daba 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -2,7 +2,7 @@ import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationPro import { Artifact, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils'; import { findLastIndex, toRegExp } from './utils.js'; import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js'; -import { getBitauthUri } from './advanced/LibauthTemplate.js'; +import { getBitauthUri } from './LibauthTemplate.js'; export type DebugResult = AuthenticationProgramStateCommon[]; export type DebugResults = Record; From 37c91a9f010634b6dd36e95f4f4e91b6def288f3 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 9 Sep 2025 12:11:05 +0200 Subject: [PATCH 06/26] Add removed deprecated transaction builder to release notes --- website/docs/releases/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index d3d8067a..4ab9c2ba 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -7,6 +7,7 @@ title: Release Notes #### CashScript SDK - :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. - :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. +- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). ## v0.11.5 From a6726f4d9d3df9078c787c3303b1da42982ca90b Mon Sep 17 00:00:00 2001 From: mainnet-pat <74184164+mainnet-pat@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:31:18 +0300 Subject: [PATCH 07/26] Rework P2PKH template building (#357) Co-authored-by: Rosco Kalis --- packages/cashscript/src/LibauthTemplate.ts | 139 +- packages/cashscript/src/TransactionBuilder.ts | 6 - packages/cashscript/src/debugging.ts | 42 +- packages/cashscript/src/interfaces.ts | 8 - .../src/network/MockNetworkProvider.ts | 1 + packages/cashscript/src/utils.ts | 17 + packages/cashscript/test/debugging.test.ts | 29 +- .../test/fixture/libauth-template/fixtures.ts | 274 +++- .../multi-contract-fixtures.ts | 1158 +++++++++++++---- website/docs/releases/release-notes.md | 1 + 10 files changed, 1344 insertions(+), 331 deletions(-) diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index 3dbeae33..12f07f0b 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -3,6 +3,7 @@ import { binToHex, decodeCashAddress, hexToBin, + Input, isHex, TransactionBch, utf8ToBin, @@ -30,10 +31,10 @@ import { Contract } from './Contract.js'; import { DebugResults, debugTemplate } from './debugging.js'; import { HashType, + isContractUnlocker, isP2PKHUnlocker, isStandardUnlockableUtxo, isUnlockableUtxo, - isUtxoP2PKH, LibauthTokenDetails, Output, SignatureAlgorithm, @@ -43,11 +44,10 @@ import { Utxo, } from './interfaces.js'; import SignatureTemplate from './SignatureTemplate.js'; -import { addressToLockScript, extendedStringify, zip } from './utils.js'; +import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from './utils.js'; import { TransactionBuilder } from './TransactionBuilder.js'; import { deflate } from 'pako'; - /** * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts. * @@ -61,16 +61,22 @@ export const generateTemplateEntitiesP2PKH = ( const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; + // TODO: Add descriptions return { [`signer_${inputIndex}`]: { scripts: [lockScriptName, unlockScriptName], - description: `placeholder_key_${inputIndex}`, + description: `P2PKH data for input ${inputIndex}`, name: `P2PKH Signer (input #${inputIndex})`, variables: { - [`placeholder_key_${inputIndex}`]: { + [`signature_${inputIndex}`]: { + description: '', + name: `P2PKH Signature (input #${inputIndex})`, + type: 'WalletData', + }, + [`public_key_${inputIndex}`]: { description: '', - name: `P2PKH Placeholder Key (input #${inputIndex})`, - type: 'Key', + name: `P2PKH public key (input #${inputIndex})`, + type: 'WalletData', }, }, }, @@ -152,30 +158,29 @@ const createWalletTemplateVariables = ( * */ export const generateTemplateScriptsP2PKH = ( - template: SignatureTemplate, inputIndex: number, ): WalletTemplate['scripts'] => { const scripts: WalletTemplate['scripts'] = {}; const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; - const placeholderKeyName = `placeholder_key_${inputIndex}`; - const signatureAlgorithmName = getSignatureAlgorithmName(template.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(template.getHashType(false)); - const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`; + const signatureString = `signature_${inputIndex}`; + const publicKeyString = `public_key_${inputIndex}`; + // add extra unlocking and locking script for P2PKH inputs spent alongside our contract // this is needed for correct cross-references in the template scripts[unlockScriptName] = { + passes: [`P2PKH_spend_input${inputIndex}_evaluate`], name: `P2PKH Unlock (input #${inputIndex})`, script: - `<${signatureString}>\n<${placeholderKeyName}.public_key>`, + `<${signatureString}>\n<${publicKeyString}>`, unlocks: lockScriptName, }; scripts[lockScriptName] = { lockingType: 'standard', name: `P2PKH Lock (input #${inputIndex})`, script: - `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, + `OP_DUP\nOP_HASH160 <$(<${publicKeyString}> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, }; return scripts; @@ -275,6 +280,7 @@ export const generateTemplateScenarios = ( const encodedConstructorArgs = contract.encodedConstructorArgs; const scenarioIdentifier = `${artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; + // TODO: Update scenario descriptions const scenarios = { // single scenario to spend out transaction under test given the CashScript parameters provided [scenarioIdentifier]: { @@ -288,12 +294,13 @@ export const generateTemplateScenarios = ( }, currentBlockHeight: 2, currentBlockTime: Math.round(+new Date() / 1000), + // TODO: remove usage of private keys in P2SH scenarios as well keys: { privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, }, transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, csTransaction, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex), }, }; @@ -308,20 +315,53 @@ export const generateTemplateScenarios = ( return scenarios; }; +export const generateTemplateScenariosP2PKH = ( + libauthTransaction: TransactionBch, + csTransaction: TransactionType, + inputIndex: number, +): WalletTemplate['scenarios'] => { + const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`; + + const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthTransaction.inputs[inputIndex]); + + // TODO: Update scenario descriptions + const scenarios = { + // single scenario to spend out transaction under test given the CashScript parameters provided + [scenarioIdentifier]: { + name: `Evaluate P2PKH spend (input #${inputIndex})`, + description: 'An example evaluation where this script execution passes.', + data: { + // encode values for the variables defined above in `entities` property + bytecode: { + [`signature_${inputIndex}`]: `0x${binToHex(signature)}`, + [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, + }, + currentBlockHeight: 2, + currentBlockTime: Math.round(+new Date() / 1000), + }, + transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, csTransaction, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex), + }, + }; + + return scenarios; +}; + const generateTemplateScenarioTransaction = ( - contract: Contract, + contract: Contract | undefined, libauthTransaction: TransactionBch, csTransaction: TransactionType, slotIndex: number, ): WalletTemplateScenario['transaction'] => { const inputs = libauthTransaction.inputs.map((input, inputIndex) => { const csInput = csTransaction.inputs[inputIndex] as Utxo; + const libauthInput = libauthTransaction.inputs[inputIndex]; return { outpointIndex: input.outpointIndex, outpointTransactionHash: binToHex(input.outpointTransactionHash), sequenceNumber: input.sequenceNumber, - unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), + unlockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), } as WalletTemplateScenarioInput; }); @@ -330,8 +370,16 @@ const generateTemplateScenarioTransaction = ( const outputs = libauthTransaction.outputs.map((output, index) => { const csOutput = csTransaction.outputs[index]; + if (csOutput && contract) { + return { + lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), + token: serialiseTokenDetails(output.token), + valueSatoshis: Number(output.valueSatoshis), + } as WalletTemplateScenarioTransactionOutput; + } + return { - lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), + lockingBytecode: `${binToHex(output.lockingBytecode)}`, token: serialiseTokenDetails(output.token), valueSatoshis: Number(output.valueSatoshis), } as WalletTemplateScenarioTransactionOutput; @@ -344,11 +392,14 @@ const generateTemplateScenarioTransaction = ( const generateTemplateScenarioSourceOutputs = ( csTransaction: TransactionType, + libauthTransaction: TransactionBch, slotIndex: number, ): Array> => { return csTransaction.inputs.map((input, inputIndex) => { + const libauthInput = libauthTransaction.inputs[inputIndex]; + return { - lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), + lockingBytecode: generateTemplateScenarioBytecode(input, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), valueSatoshis: Number(input.satoshis), token: serialiseTokenDetails(input.token), }; @@ -409,18 +460,14 @@ export const getLibauthTemplates = ( // We can typecast this because we check that all inputs are standard unlockable at the top of this function for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) { - // If template exists on the input, it indicates this is a P2PKH (Pay to Public Key Hash) input - if ('template' in input.unlocker) { - // @ts-ignore TODO: Remove UtxoP2PKH type and only use UnlockableUtxo in Libauth Template generation - input.template = input.unlocker?.template; // Added to support P2PKH inputs in buildTemplate + if (isP2PKHUnlocker(input.unlocker)) { Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex)); - Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(input.unlocker.template, inputIndex)); - + Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(inputIndex)); + Object.assign(scenarios, generateTemplateScenariosP2PKH(libauthTransaction, csTransaction, inputIndex)); continue; } - // If contract exists on the input, it indicates this is a contract input - if ('contract' in input.unlocker) { + if (isContractUnlocker(input.unlocker)) { const contract = input.unlocker?.contract; const abiFunction = input.unlocker?.abiFunction; @@ -537,7 +584,7 @@ export const getLibauthTemplates = ( export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { const allArtifacts = transaction.inputs - .map(input => 'contract' in input.unlocker ? input.unlocker.contract : undefined) + .map(input => isContractUnlocker(input.unlocker) ? input.unlocker.contract : undefined) .filter((contract): contract is Contract => Boolean(contract)) .map(contract => contract.artifact); @@ -575,17 +622,19 @@ const generateLockingScriptParams = ( export const generateUnlockingScriptParams = ( csInput: StandardUnlockableUtxo, + libauthInput: Input, p2pkhScriptNameTemplate: string, inputIndex: number, ): WalletTemplateScenarioBytecode => { if (isP2PKHUnlocker(csInput.unlocker)) { + const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthInput); + return { script: `${p2pkhScriptNameTemplate}_${inputIndex}`, overrides: { - keys: { - privateKeys: { - [`placeholder_key_${inputIndex}`]: binToHex(csInput.unlocker.template.privateKey), - }, + bytecode: { + [`signature_${inputIndex}`]: `0x${binToHex(signature)}`, + [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, }, }, }; @@ -604,6 +653,7 @@ export const generateUnlockingScriptParams = ( ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), ...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs), }, + // TODO: remove usage of private keys in P2SH scenarios as well keys: { privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, @@ -745,29 +795,16 @@ export const generateTemplateScenarioKeys = ( // Used for generating the locking / unlocking bytecode for source outputs and inputs export const generateTemplateScenarioBytecode = ( - input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean, + input: Utxo, + libauthInput: Input, + inputIndex: number, + p2pkhScriptNameTemplate: string, + insertSlot?: boolean, ): WalletTemplateScenarioBytecode | ['slot'] => { if (insertSlot) return ['slot']; - const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`; - const placeholderKeyName = `placeholder_key_${inputIndex}`; - - // This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder) - if (isUtxoP2PKH(input)) { - return { - script: p2pkhScriptName, - overrides: { - keys: { - privateKeys: { - [placeholderKeyName]: binToHex(input.template.privateKey), - }, - }, - }, - }; - } - if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) { - return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex); + return generateUnlockingScriptParams(input, libauthInput, p2pkhScriptNameTemplate, inputIndex); } // 'slot' means that we are currently evaluating this specific input, diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 4a8303d3..32cac564 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -17,7 +17,6 @@ import { isUnlockableUtxo, isStandardUnlockableUtxo, StandardUnlockableUtxo, - isP2PKHUnlocker, } from './interfaces.js'; import { NetworkProvider } from './network/index.js'; import { @@ -157,11 +156,6 @@ export class TransactionBuilder { } debug(): DebugResults { - // do not debug a pure P2PKH-spend transaction - if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) { - return {}; - } - if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) { throw new Error('Cannot debug a transaction with custom unlocker'); } diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index 15a6daba..82023d5d 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -25,15 +25,9 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): for (const unlockingScriptId of unlockingScriptIds) { const scenarioIds = (template.scripts[unlockingScriptId] as WalletTemplateScriptUnlocking).passes ?? []; - // There are no scenarios defined for P2PKH placeholder scripts, so we skip them - if (scenarioIds.length === 0) continue; const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName)); - if (!matchingArtifact) { - throw new Error(`No artifact found for unlocking script ${unlockingScriptId}`); - } - for (const scenarioId of scenarioIds) { results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId); } @@ -45,7 +39,7 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): }; const debugSingleScenario = ( - template: WalletTemplate, artifact: Artifact, unlockingScriptId: string, scenarioId: string, + template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string, ): DebugResult => { const { vm, program } = createProgram(template, unlockingScriptId, scenarioId); @@ -60,12 +54,15 @@ const debugSingleScenario = ( const executedDebugSteps = lockingScriptDebugResult .filter((debugStep) => debugStep.controlStack.every(item => item === true)); - const executedLogs = (artifact.debug?.logs ?? []) - .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip)); + // P2PKH inputs do not have an artifact, so we skip the console.log handling + if (artifact) { + const executedLogs = (artifact.debug?.logs ?? []) + .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip)); - for (const log of executedLogs) { - const inputIndex = extractInputIndexFromScenario(scenarioId); - logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex); + for (const log of executedLogs) { + const inputIndex = extractInputIndexFromScenario(scenarioId); + logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex); + } } const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1]; @@ -87,11 +84,18 @@ const debugSingleScenario = ( const isNullFail = lastExecutedDebugStep.error.includes(AuthenticationErrorCommon.nonNullSignatureFailure); const requireStatementIp = failingIp + (isNullFail && isSignatureCheckWithoutVerify(failingInstruction) ? 1 : 0); + const { program: { inputIndex }, error } = lastExecutedDebugStep; + + // If there is no artifact, this is a P2PKH debug error, error can occur when final CHECKSIG fails with NULLFAIL or when + // public key does not match pkh in EQUALVERIFY + // Note: due to P2PKHUnlocker implementation, the CHECKSIG cannot fail in practice, only the EQUALVERIFY can fail + if (!artifact) { + throw new FailedTransactionError(error, getBitauthUri(template)); + } + const requireStatement = (artifact.debug?.requires ?? []) .find((statement) => statement.ip === requireStatementIp); - const { program: { inputIndex }, error } = lastExecutedDebugStep; - if (requireStatement) { // Note that we use failingIp here rather than requireStatementIp, see comment above throw new FailedRequireError( @@ -121,11 +125,17 @@ const debugSingleScenario = ( // console.warn('message', finalExecutedVerifyIp); // console.warn(artifact.debug?.requires); + const { program: { inputIndex } } = lastExecutedDebugStep; + + // If there is no artifact, this is a P2PKH debug error, final verify can only occur when final CHECKSIG failed + // Note: due to P2PKHUnlocker implementation, this cannot happen in practice + if (!artifact) { + throw new FailedTransactionError(evaluationResult, getBitauthUri(template)); + } + const requireStatement = (artifact.debug?.requires ?? []) .find((message) => message.ip === finalExecutedVerifyIp); - const { program: { inputIndex } } = lastExecutedDebugStep; - if (requireStatement) { throw new FailedRequireError( artifact, sourcemapInstructionPointer, requireStatement, inputIndex, getBitauthUri(template), diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index 98544289..d5a88c48 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -75,14 +75,6 @@ export function isPlaceholderUnlocker(unlocker: Unlocker): unlocker is Placehold return 'placeholder' in unlocker; } -export interface UtxoP2PKH extends Utxo { - template: SignatureTemplate; -} - -export function isUtxoP2PKH(utxo: Utxo): utxo is UtxoP2PKH { - return 'template' in utxo; -} - export interface Recipient { to: string; amount: bigint; diff --git a/packages/cashscript/src/network/MockNetworkProvider.ts b/packages/cashscript/src/network/MockNetworkProvider.ts index 84ed2d43..fd0f8c5f 100644 --- a/packages/cashscript/src/network/MockNetworkProvider.ts +++ b/packages/cashscript/src/network/MockNetworkProvider.ts @@ -25,6 +25,7 @@ export default class MockNetworkProvider implements NetworkProvider { this.options = { updateUtxoSet: true, ...options }; for (let i = 0; i < 3; i += 1) { + // TODO: Don't seed the MockNetworkProvider with any UTXOs this.addUtxo(aliceAddress, randomUtxo()); this.addUtxo(bobAddress, randomUtxo()); this.addUtxo(carolAddress, randomUtxo()); diff --git a/packages/cashscript/src/utils.ts b/packages/cashscript/src/utils.ts index 8b23ff41..23c61df3 100644 --- a/packages/cashscript/src/utils.ts +++ b/packages/cashscript/src/utils.ts @@ -14,6 +14,11 @@ import { bigIntToCompactUint, NonFungibleTokenCapability, bigIntToVmNumber, + assertSuccess, + AuthenticationInstructionPush, + AuthenticationInstructions, + decodeAuthenticationInstructions, + Input, } from '@bitauth/libauth'; import { encodeInt, @@ -368,3 +373,15 @@ export const isFungibleTokenUtxo = (utxo: Utxo): boolean => ( export const isNonTokenUtxo = (utxo: Utxo): boolean => utxo.token === undefined; export const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +export const getSignatureAndPubkeyFromP2PKHInput = ( + libauthInput: Input, +): { signature: Uint8Array; publicKey: Uint8Array } => { + const inputData = (assertSuccess( + decodeAuthenticationInstructions(libauthInput.unlockingBytecode)) as AuthenticationInstructions + ) as AuthenticationInstructionPush[]; + const signature = inputData[0].data; + const publicKey = inputData[1].data; + + return { signature, publicKey }; +}; diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index adc394c1..8e5942d7 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -1,5 +1,5 @@ -import { Contract, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; -import { alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; +import { Contract, FailedTransactionError, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { aliceAddress, alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; import '../src/test/JestExtensions.js'; import { randomUtxo } from '../src/utils.js'; import { AuthenticationErrorCommon, binToHex, hexToBin } from '@bitauth/libauth'; @@ -620,4 +620,29 @@ describe('Debugging tests', () => { ).toThrow(/Contract function failed a require statement\.*\nReceived string: (.|\n)*?1 should equal 2/); }); }); + + describe('P2PKH only transaction', () => { + it('should succeed when spending from P2PKH inputs with the corresponding unlocker', async () => { + const provider = new MockNetworkProvider(); + + const result = new TransactionBuilder({ provider }) + .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(alicePriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 5000n }) + .debug(); + + expect(Object.keys(result).length).toBeGreaterThan(0); + }); + + // We currently don't have a way to properly handle non-matching UTXOs and unlockers + // Note: that also goes for Contract UTXOs where a user uses an unlocker of a different contract + it.skip('should fail when spending from P2PKH inputs with an unlocker for a different public key', async () => { + const provider = new MockNetworkProvider(); + + const transactionBuilder = new TransactionBuilder({ provider }) + .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(bobPriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 5000n }); + + expect(() => transactionBuilder.debug()).toThrow(FailedTransactionError); + }); + }); }); diff --git a/packages/cashscript/test/fixture/libauth-template/fixtures.ts b/packages/cashscript/test/fixture/libauth-template/fixtures.ts index de27e2b3..55e0fbbf 100644 --- a/packages/cashscript/test/fixture/libauth-template/fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/fixtures.ts @@ -1368,13 +1368,18 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_0', 'p2pkh_placeholder_unlock_0', ], - 'description': 'placeholder_key_0', + 'description': 'P2PKH data for input 0', 'name': 'P2PKH Signer (input #0)', 'variables': { - 'placeholder_key_0': { + 'signature_0': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #0)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #0)', + 'type': 'WalletData', + }, + 'public_key_0': { + 'description': '', + 'name': 'P2PKH public key (input #0)', + 'type': 'WalletData', }, }, }, @@ -1383,13 +1388,18 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_2', 'p2pkh_placeholder_unlock_2', ], - 'description': 'placeholder_key_2', + 'description': 'P2PKH data for input 2', 'name': 'P2PKH Signer (input #2)', 'variables': { - 'placeholder_key_2': { + 'signature_2': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #2)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #2)', + 'type': 'WalletData', + }, + 'public_key_2': { + 'description': '', + 'name': 'P2PKH public key (input #2)', + 'type': 'WalletData', }, }, }, @@ -1409,27 +1419,136 @@ export const fixtures: Fixture[] = [ 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', }, 'p2pkh_placeholder_unlock_0': { + 'passes': [ + 'P2PKH_spend_input0_evaluate', + ], 'name': 'P2PKH Unlock (input #0)', - 'script': '\n', + 'script': '\n', 'unlocks': 'p2pkh_placeholder_lock_0', }, 'p2pkh_placeholder_lock_0': { 'lockingType': 'standard', 'name': 'P2PKH Lock (input #0)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, 'p2pkh_placeholder_unlock_2': { + 'passes': [ + 'P2PKH_spend_input2_evaluate', + ], 'name': 'P2PKH Unlock (input #2)', - 'script': '\n', + 'script': '\n', 'unlocks': 'p2pkh_placeholder_lock_2', }, 'p2pkh_placeholder_lock_2': { 'lockingType': 'standard', 'name': 'P2PKH Lock (input #2)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, }, 'scenarios': { + 'P2PKH_spend_input0_evaluate': { + 'name': 'Evaluate P2PKH spend (input #0)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + 'currentBlockHeight': expect.any(Number), + 'currentBlockTime': expect.any(Number), + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'P2PKH_spend_input1_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + }, + ], + 'locktime': expect.any(Number), + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': 1000, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, 'P2PKH_spend_input1_evaluate': { 'name': 'Evaluate P2PKH spend (input #1)', 'description': 'An example evaluation where this script execution passes.', @@ -1455,10 +1574,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1478,16 +1596,15 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', }, }, }, }, ], - 'locktime': 0, + 'locktime': expect.any(Number), 'outputs': [ { 'lockingBytecode': { @@ -1508,10 +1625,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1527,13 +1643,115 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input2_evaluate': { + 'name': 'Evaluate P2PKH spend (input #2)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + 'currentBlockHeight': expect.any(Number), + 'currentBlockTime': expect.any(Number), + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'P2PKH_spend_input1_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, }, }, }, }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + ], + 'locktime': expect.any(Number), + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': 1000, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': [ + 'slot', + ], 'valueSatoshis': expect.any(Number), }, ], diff --git a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts index 1631b1dc..f4488599 100644 --- a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts @@ -167,122 +167,866 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_0', 'p2pkh_placeholder_unlock_0', ], - 'description': 'placeholder_key_0', + 'description': 'P2PKH data for input 0', 'name': 'P2PKH Signer (input #0)', 'variables': { - 'placeholder_key_0': { + 'signature_0': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #0)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #0)', + 'type': 'WalletData', + }, + 'public_key_0': { + 'description': '', + 'name': 'P2PKH public key (input #0)', + 'type': 'WalletData', + }, + }, + }, + 'signer_1': { + 'scripts': [ + 'p2pkh_placeholder_lock_1', + 'p2pkh_placeholder_unlock_1', + ], + 'description': 'P2PKH data for input 1', + 'name': 'P2PKH Signer (input #1)', + 'variables': { + 'signature_1': { + 'description': '', + 'name': 'P2PKH Signature (input #1)', + 'type': 'WalletData', + }, + 'public_key_1': { + 'description': '', + 'name': 'P2PKH public key (input #1)', + 'type': 'WalletData', + }, + }, + }, + 'signer_2': { + 'scripts': [ + 'p2pkh_placeholder_lock_2', + 'p2pkh_placeholder_unlock_2', + ], + 'description': 'P2PKH data for input 2', + 'name': 'P2PKH Signer (input #2)', + 'variables': { + 'signature_2': { + 'description': '', + 'name': 'P2PKH Signature (input #2)', + 'type': 'WalletData', + }, + 'public_key_2': { + 'description': '', + 'name': 'P2PKH public key (input #2)', + 'type': 'WalletData', + }, + }, + }, + }, + 'scripts': { + 'Bar_funcA_input3_unlock': { + 'passes': [ + 'Bar_funcA_input3_evaluate', + ], + 'name': 'funcA (input #3)', + 'script': '// "funcA" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock': { + 'lockingType': 'p2sh32', + 'name': 'Bar', + 'script': "// \"Bar\" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Bar(bytes20 pkh_bar) { */\nOP_OVER OP_0 OP_NUMEQUAL OP_IF /* function funcA() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_OVER OP_1 OP_NUMEQUAL OP_IF /* function funcB() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_SWAP OP_2 OP_NUMEQUALVERIFY /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Bar 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_bar); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\nOP_ENDIF OP_ENDIF /* } */\n /* */", + }, + 'Bar_execute_input4_unlock': { + 'passes': [ + 'Bar_execute_input4_evaluate', + ], + 'name': 'execute (input #4)', + 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// function index in contract\n // int = <2>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'Foo_execute_input5_unlock': { + 'passes': [ + 'Foo_execute_input5_evaluate', + ], + 'name': 'execute (input #5)', + 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n', + 'unlocks': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + }, + 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock': { + 'lockingType': 'p2sh32', + 'name': 'Foo', + 'script': "// \"Foo\" contract constructor parameters\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Foo(bytes20 pkh_foo) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Foo 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_foo); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */", + }, + 'Bar_funcB_input6_unlock': { + 'passes': [ + 'Bar_funcB_input6_evaluate', + ], + 'name': 'funcB (input #6)', + 'script': '// "funcB" function parameters\n// none\n\n// function index in contract\n // int = <1>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'p2pkh_placeholder_unlock_0': { + 'passes': [ + 'P2PKH_spend_input0_evaluate', + ], + 'name': 'P2PKH Unlock (input #0)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_0', + }, + 'p2pkh_placeholder_lock_0': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #0)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + 'p2pkh_placeholder_unlock_1': { + 'passes': [ + 'P2PKH_spend_input1_evaluate', + ], + 'name': 'P2PKH Unlock (input #1)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_1', + }, + 'p2pkh_placeholder_lock_1': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #1)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + 'p2pkh_placeholder_unlock_2': { + 'passes': [ + 'P2PKH_spend_input2_evaluate', + ], + 'name': 'P2PKH Unlock (input #2)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_2', + }, + 'p2pkh_placeholder_lock_2': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #2)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + }, + 'scenarios': { + 'P2PKH_spend_input0_evaluate': { + 'name': 'Evaluate P2PKH spend (input #0)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + 'currentBlockHeight': 2, + 'currentBlockTime': expect.any(Number), + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input1_evaluate': { + 'name': 'Evaluate P2PKH spend (input #1)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + 'currentBlockHeight': 2, + 'currentBlockTime': expect.any(Number), + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input2_evaluate': { + 'name': 'Evaluate P2PKH spend (input #2)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + 'currentBlockHeight': 2, + 'currentBlockTime': expect.any(Number), + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - 'signer_1': { - 'scripts': [ - 'p2pkh_placeholder_lock_1', - 'p2pkh_placeholder_unlock_1', - ], - 'description': 'placeholder_key_1', - 'name': 'P2PKH Signer (input #1)', - 'variables': { - 'placeholder_key_1': { - 'description': '', - 'name': 'P2PKH Placeholder Key (input #1)', - 'type': 'Key', + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - 'signer_2': { - 'scripts': [ - 'p2pkh_placeholder_lock_2', - 'p2pkh_placeholder_unlock_2', - ], - 'description': 'placeholder_key_2', - 'name': 'P2PKH Signer (input #2)', - 'variables': { - 'placeholder_key_2': { - 'description': '', - 'name': 'P2PKH Placeholder Key (input #2)', - 'type': 'Key', + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - }, - 'scripts': { - 'Bar_funcA_input3_unlock': { - 'passes': [ - 'Bar_funcA_input3_evaluate', - ], - 'name': 'funcA (input #3)', - 'script': '// "funcA" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock': { - 'lockingType': 'p2sh32', - 'name': 'Bar', - 'script': "// \"Bar\" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Bar(bytes20 pkh_bar) { */\nOP_OVER OP_0 OP_NUMEQUAL OP_IF /* function funcA() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_OVER OP_1 OP_NUMEQUAL OP_IF /* function funcB() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_SWAP OP_2 OP_NUMEQUALVERIFY /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Bar 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_bar); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\nOP_ENDIF OP_ENDIF /* } */\n /* */", - }, - 'Bar_execute_input4_unlock': { - 'passes': [ - 'Bar_execute_input4_evaluate', - ], - 'name': 'execute (input #4)', - 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// function index in contract\n // int = <2>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'Foo_execute_input5_unlock': { - 'passes': [ - 'Foo_execute_input5_evaluate', - ], - 'name': 'execute (input #5)', - 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n', - 'unlocks': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', - }, - 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock': { - 'lockingType': 'p2sh32', - 'name': 'Foo', - 'script': "// \"Foo\" contract constructor parameters\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Foo(bytes20 pkh_foo) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Foo 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_foo); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */", - }, - 'Bar_funcB_input6_unlock': { - 'passes': [ - 'Bar_funcB_input6_evaluate', ], - 'name': 'funcB (input #6)', - 'script': '// "funcB" function parameters\n// none\n\n// function index in contract\n // int = <1>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'p2pkh_placeholder_unlock_0': { - 'name': 'P2PKH Unlock (input #0)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_0', - }, - 'p2pkh_placeholder_lock_0': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #0)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_1': { - 'name': 'P2PKH Unlock (input #1)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_1', - }, - 'p2pkh_placeholder_lock_1': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #1)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_2': { - 'name': 'P2PKH Unlock (input #2)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_2', - }, - 'p2pkh_placeholder_lock_2': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #2)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, - }, - 'scenarios': { 'Bar_funcA_input3_evaluate': { 'name': 'Evaluate Bar funcA (input #3)', 'description': 'An example evaluation where this script execution passes.', @@ -291,7 +1035,7 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), + 'currentBlockHeight': 2, 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, @@ -306,10 +1050,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -321,10 +1064,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -336,10 +1078,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -357,6 +1098,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -369,7 +1111,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -377,6 +1118,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -388,7 +1130,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -396,6 +1137,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', @@ -405,11 +1147,10 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, }, ], - 'locktime': expect.any(Number), + 'locktime': 0, 'outputs': [ { 'lockingBytecode': { @@ -454,10 +1195,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -467,10 +1207,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -480,10 +1219,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -556,10 +1294,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -571,10 +1308,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -586,10 +1322,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -599,6 +1334,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -608,7 +1344,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -624,6 +1359,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -635,7 +1371,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -643,6 +1378,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', @@ -652,9 +1388,7 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, - }, ], 'locktime': 0, @@ -702,10 +1436,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -715,10 +1448,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -728,10 +1460,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -803,10 +1534,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -818,10 +1548,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -833,10 +1562,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -846,6 +1574,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -855,7 +1584,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -863,6 +1591,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -875,7 +1604,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -891,17 +1619,16 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, 'keys': { 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, }, ], @@ -950,10 +1677,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -963,10 +1689,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -976,10 +1701,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1049,10 +1773,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1064,10 +1787,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1079,10 +1801,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1092,6 +1813,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -1101,7 +1823,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -1109,6 +1830,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -1121,7 +1843,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -1129,6 +1850,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -1140,7 +1862,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -1197,10 +1918,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1210,10 +1930,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1223,10 +1942,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 4ab9c2ba..38797b94 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -8,6 +8,7 @@ title: Release Notes - :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. - :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). +- :hammer_and_wrench: Improve libauth template generation. ## v0.11.5 From 7185deda80ce43a3c07d8be4a5cd268a1dda688d Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 10:38:18 +0200 Subject: [PATCH 08/26] Make bitauthUri required on FailedTransactionError --- packages/cashscript/src/Errors.ts | 2 +- packages/cashscript/src/TransactionBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cashscript/src/Errors.ts b/packages/cashscript/src/Errors.ts index ca03fec7..d717253c 100644 --- a/packages/cashscript/src/Errors.ts +++ b/packages/cashscript/src/Errors.ts @@ -37,7 +37,7 @@ export class NoDebugInformationInArtifactError extends Error { } export class FailedTransactionError extends Error { - constructor(public reason: string, public bitauthUri?: string) { + constructor(public reason: string, public bitauthUri: string) { const warning = 'WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'; super(`${reason}${bitauthUri ? `\n\n${warning}\n\nBitauth URI: ${bitauthUri}` : ''}`); } diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 32cac564..a52f3838 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -197,7 +197,7 @@ export class TransactionBuilder { return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid); } catch (e: any) { const reason = e.error ?? e.message; - throw new FailedTransactionError(reason); + throw new FailedTransactionError(reason, this.bitauthUri()); } } From 0ef29ab770aa086459d0ca5489114e0c6f9c5ad3 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 10:49:35 +0200 Subject: [PATCH 09/26] Remove currentBlockHeight and currentBlockTime from template scenarios --- packages/cashscript/src/LibauthTemplate.ts | 4 --- .../test/fixture/libauth-template/fixtures.ts | 24 -------------- .../multi-contract-fixtures.ts | 32 ------------------- 3 files changed, 60 deletions(-) diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index 12f07f0b..4fd4d68b 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -292,8 +292,6 @@ export const generateTemplateScenarios = ( ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), }, - currentBlockHeight: 2, - currentBlockTime: Math.round(+new Date() / 1000), // TODO: remove usage of private keys in P2SH scenarios as well keys: { privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), @@ -336,8 +334,6 @@ export const generateTemplateScenariosP2PKH = ( [`signature_${inputIndex}`]: `0x${binToHex(signature)}`, [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, }, - currentBlockHeight: 2, - currentBlockTime: Math.round(+new Date() / 1000), }, transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, csTransaction, inputIndex), sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex), diff --git a/packages/cashscript/test/fixture/libauth-template/fixtures.ts b/packages/cashscript/test/fixture/libauth-template/fixtures.ts index 55e0fbbf..d0a7857f 100644 --- a/packages/cashscript/test/fixture/libauth-template/fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/fixtures.ts @@ -99,8 +99,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -233,8 +231,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -366,8 +362,6 @@ export const fixtures: Fixture[] = [ 'pledge': '0x1027', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -528,8 +522,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -618,8 +610,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -781,8 +771,6 @@ export const fixtures: Fixture[] = [ 's': '0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -937,8 +925,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1035,8 +1021,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1233,8 +1217,6 @@ export const fixtures: Fixture[] = [ 'minBlock': '0xb88201', 'priceTarget': '0x3075', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1454,8 +1436,6 @@ export const fixtures: Fixture[] = [ 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), }, 'transaction': { 'inputs': [ @@ -1557,8 +1537,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1661,8 +1639,6 @@ export const fixtures: Fixture[] = [ 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), }, 'transaction': { 'inputs': [ diff --git a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts index f4488599..e88dae8f 100644 --- a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts @@ -315,8 +315,6 @@ export const fixtures: Fixture[] = [ 'signature_0': expect.any(String), 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), }, 'transaction': { 'inputs': [ @@ -555,8 +553,6 @@ export const fixtures: Fixture[] = [ 'signature_1': expect.any(String), 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), }, 'transaction': { 'inputs': [ @@ -795,8 +791,6 @@ export const fixtures: Fixture[] = [ 'signature_2': expect.any(String), 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), }, 'transaction': { 'inputs': [ @@ -1035,8 +1029,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '0', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -1277,8 +1269,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1517,8 +1507,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -1758,8 +1746,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -2063,8 +2049,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -2261,8 +2245,6 @@ export const fixtures: Fixture[] = [ 'minBlock': '0xb88201', 'priceTarget': '0x3075', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -2353,8 +2335,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -2662,8 +2642,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -2820,8 +2798,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '81597823a901865622658cbf6d50c0286aa1d70fa1af98f897e34a0623a828ff', @@ -2978,8 +2954,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '81597823a901865622658cbf6d50c0286aa1d70fa1af98f897e34a0623a828ff', @@ -3136,8 +3110,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -3408,8 +3380,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -3492,8 +3462,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', From f44934ecbfacf915c3cf64d71fe2440dda8f0fdd Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 10:54:49 +0200 Subject: [PATCH 10/26] Use zip() to loop over libauth & cs inputs/outputs --- packages/cashscript/src/LibauthTemplate.ts | 38 ++++++++++------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index 4fd4d68b..82318365 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -349,35 +349,32 @@ const generateTemplateScenarioTransaction = ( csTransaction: TransactionType, slotIndex: number, ): WalletTemplateScenario['transaction'] => { - const inputs = libauthTransaction.inputs.map((input, inputIndex) => { - const csInput = csTransaction.inputs[inputIndex] as Utxo; - const libauthInput = libauthTransaction.inputs[inputIndex]; - + const zippedInputs = zip(csTransaction.inputs, libauthTransaction.inputs); + const inputs = zippedInputs.map(([csInput, libauthInput], inputIndex) => { return { - outpointIndex: input.outpointIndex, - outpointTransactionHash: binToHex(input.outpointTransactionHash), - sequenceNumber: input.sequenceNumber, + outpointIndex: libauthInput.outpointIndex, + outpointTransactionHash: binToHex(libauthInput.outpointTransactionHash), + sequenceNumber: libauthInput.sequenceNumber, unlockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), } as WalletTemplateScenarioInput; }); const locktime = libauthTransaction.locktime; - const outputs = libauthTransaction.outputs.map((output, index) => { - const csOutput = csTransaction.outputs[index]; - + const zippedOutputs = zip(csTransaction.outputs, libauthTransaction.outputs); + const outputs = zippedOutputs.map(([csOutput, libauthOutput]) => { if (csOutput && contract) { return { lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), - token: serialiseTokenDetails(output.token), - valueSatoshis: Number(output.valueSatoshis), + token: serialiseTokenDetails(libauthOutput.token), + valueSatoshis: Number(libauthOutput.valueSatoshis), } as WalletTemplateScenarioTransactionOutput; } return { - lockingBytecode: `${binToHex(output.lockingBytecode)}`, - token: serialiseTokenDetails(output.token), - valueSatoshis: Number(output.valueSatoshis), + lockingBytecode: `${binToHex(libauthOutput.lockingBytecode)}`, + token: serialiseTokenDetails(libauthOutput.token), + valueSatoshis: Number(libauthOutput.valueSatoshis), } as WalletTemplateScenarioTransactionOutput; }); @@ -391,13 +388,12 @@ const generateTemplateScenarioSourceOutputs = ( libauthTransaction: TransactionBch, slotIndex: number, ): Array> => { - return csTransaction.inputs.map((input, inputIndex) => { - const libauthInput = libauthTransaction.inputs[inputIndex]; - + const zippedInputs = zip(csTransaction.inputs, libauthTransaction.inputs); + return zippedInputs.map(([csInput, libauthInput], inputIndex) => { return { - lockingBytecode: generateTemplateScenarioBytecode(input, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), - valueSatoshis: Number(input.satoshis), - token: serialiseTokenDetails(input.token), + lockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), + valueSatoshis: Number(csInput.satoshis), + token: serialiseTokenDetails(csInput.token), }; }); }; From f41025d804cb5e0583a446136065e227100714a9 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 11:04:00 +0200 Subject: [PATCH 11/26] Remove unnecessary exports from LibauthTemplate --- packages/cashscript/src/LibauthTemplate.ts | 426 ++++++++++----------- 1 file changed, 192 insertions(+), 234 deletions(-) diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index 82318365..566e80ae 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -48,14 +48,180 @@ import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHI import { TransactionBuilder } from './TransactionBuilder.js'; import { deflate } from 'pako'; -/** - * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts. - * - * Follows the WalletTemplateEntity specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateEntitiesP2PKH = ( +export const getLibauthTemplates = ( + txn: TransactionBuilder, +): WalletTemplate => { + if (txn.inputs.some((input) => !isStandardUnlockableUtxo(input))) { + throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); + } + + const libauthTransaction = txn.buildLibauthTransaction(); + const csTransaction = createTransactionTypeFromTransactionBuilder(txn); + + const baseTemplate: WalletTemplate = { + $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', + description: 'Imported from cashscript', + name: 'CashScript Generated Debugging Template', + supported: ['BCH_2025_05'], + version: 0, + entities: {}, + scripts: {}, + scenarios: {}, + }; + + // Initialize collections for entities, scripts, and scenarios + const entities: Record = {}; + const scripts: Record = {}; + const scenarios: Record = {}; + + // Initialize collections for P2PKH entities and scripts + const p2pkhEntities: Record = {}; + const p2pkhScripts: Record = {}; + + // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts + const unlockingBytecodeToLockingBytecodeParams: Record = {}; + const lockingBytecodeToLockingBytecodeParams: Record = {}; + + // We can typecast this because we check that all inputs are standard unlockable at the top of this function + for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) { + if (isP2PKHUnlocker(input.unlocker)) { + Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex)); + Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(inputIndex)); + Object.assign(scenarios, generateTemplateScenariosP2PKH(libauthTransaction, csTransaction, inputIndex)); + continue; + } + + if (isContractUnlocker(input.unlocker)) { + const contract = input.unlocker?.contract; + const abiFunction = input.unlocker?.abiFunction; + + if (!abiFunction) { + throw new Error('No ABI function found in unlocker'); + } + + // Encode the function arguments for this contract input + const encodedArgs = encodeFunctionArguments( + abiFunction, + input.unlocker.params ?? [], + ); + + // Generate a scenario object for this contract input + Object.assign(scenarios, + generateTemplateScenarios( + contract, + libauthTransaction, + csTransaction, + abiFunction, + encodedArgs, + inputIndex, + ), + ); + + // Generate entities for this contract input + const entity = generateTemplateEntitiesP2SH( + contract, + abiFunction, + encodedArgs, + inputIndex, + ); + + // Generate scripts for this contract input + const script = generateTemplateScriptsP2SH( + contract, + abiFunction, + encodedArgs, + contract.encodedConstructorArgs, + inputIndex, + ); + + // Find the lock script name for this contract input + const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock')); + if (lockScriptName) { + // Generate bytecodes for this contract input + const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); + const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); + + // Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios + unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; + // Assign a name to the locking bytecode so later it can be used to replace with bytecode/slot in scenarios + lockingBytecodeToLockingBytecodeParams[binToHex(addressToLockScript(contract.address))] = lockingScriptParams; + } + + // Add entities and scripts to the base template and repeat the process for the next input + Object.assign(entities, entity); + Object.assign(scripts, script); + } + } + + Object.assign(entities, p2pkhEntities); + Object.assign(scripts, p2pkhScripts); + + const finalTemplate = { ...baseTemplate, entities, scripts, scenarios }; + + // Loop through all scenarios and map the locking and unlocking scripts to the scenarios + // Replace the script tag with the identifiers we created earlier + + // For Inputs + for (const scenario of Object.values(scenarios)) { + for (const [idx, input] of libauthTransaction.inputs.entries()) { + const unlockingBytecode = binToHex(input.unlockingBytecode); + + // If false then it stays lockingBytecode: {} + if (unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]) { + // ['slot'] this identifies the source output in which the locking script under test will be placed + if (Array.isArray(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; + + // If true then assign a name to the locking bytecode script. + if (scenario.sourceOutputs && scenario.sourceOutputs[idx]) { + scenario.sourceOutputs[idx] = { + ...scenario.sourceOutputs[idx], + lockingBytecode: unlockingBytecodeToLockingBytecodeParams[unlockingBytecode], + }; + } + } + } + + // For Outputs + for (const [idx, output] of libauthTransaction.outputs.entries()) { + const lockingBytecode = binToHex(output.lockingBytecode); + + // If false then it stays lockingBytecode: {} + if (lockingBytecodeToLockingBytecodeParams[lockingBytecode]) { + + // ['slot'] this identifies the source output in which the locking script under test will be placed + if (Array.isArray(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; + + // If true then assign a name to the locking bytecode script. + if (scenario?.transaction && scenario?.transaction?.outputs && scenario?.transaction?.outputs[idx]) { + scenario.transaction.outputs[idx] = { + ...scenario.transaction.outputs[idx], + lockingBytecode: lockingBytecodeToLockingBytecodeParams[lockingBytecode], + }; + } + } + } + + } + + return finalTemplate; +}; + +export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { + const allArtifacts = transaction.inputs + .map(input => isContractUnlocker(input.unlocker) ? input.unlocker.contract : undefined) + .filter((contract): contract is Contract => Boolean(contract)) + .map(contract => contract.artifact); + + return debugTemplate(template, allArtifacts); +}; + +export const getBitauthUri = (template: WalletTemplate): string => { + const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); + const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); + return `https://ide.bitauth.com/import-template/${payload}`; +}; + +const generateTemplateEntitiesP2PKH = ( inputIndex: number, ): WalletTemplate['entities'] => { const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; @@ -83,14 +249,7 @@ export const generateTemplateEntitiesP2PKH = ( }; }; -/** - * Generates template entities for P2SH (Pay to Script Hash) placeholder scripts. - * - * Follows the WalletTemplateEntity specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateEntitiesP2SH = ( +const generateTemplateEntitiesP2SH = ( contract: Contract, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], @@ -150,14 +309,7 @@ const createWalletTemplateVariables = ( return { ...functionParameters, ...constructorParameters }; }; -/** - * Generates template scripts for P2PKH (Pay to Public Key Hash) placeholder scripts. - * - * Follows the WalletTemplateScript specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateScriptsP2PKH = ( +const generateTemplateScriptsP2PKH = ( inputIndex: number, ): WalletTemplate['scripts'] => { const scripts: WalletTemplate['scripts'] = {}; @@ -186,14 +338,7 @@ export const generateTemplateScriptsP2PKH = ( return scripts; }; -/** - * Generates template scripts for P2SH (Pay to Script Hash) placeholder scripts. - * - * Follows the WalletTemplateScript specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateScriptsP2SH = ( +const generateTemplateScriptsP2SH = ( contract: Contract, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], @@ -210,13 +355,6 @@ export const generateTemplateScriptsP2SH = ( }; }; -/** - * Generates a template lock script for a P2SH (Pay to Script Hash) placeholder script. - * - * Follows the WalletTemplateScriptLocking specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ const generateTemplateLockScript = ( contract: Contract, constructorArguments: EncodedFunctionArgument[], @@ -234,13 +372,6 @@ const generateTemplateLockScript = ( }; }; -/** - * Generates a template unlock script for a P2SH (Pay to Script Hash) placeholder script. - * - * Follows the WalletTemplateScriptUnlocking specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ const generateTemplateUnlockScript = ( contract: Contract, abiFunction: AbiFunction, @@ -268,7 +399,7 @@ const generateTemplateUnlockScript = ( }; }; -export const generateTemplateScenarios = ( +const generateTemplateScenarios = ( contract: Contract, libauthTransaction: TransactionBch, csTransaction: TransactionType, @@ -313,7 +444,7 @@ export const generateTemplateScenarios = ( return scenarios; }; -export const generateTemplateScenariosP2PKH = ( +const generateTemplateScenariosP2PKH = ( libauthTransaction: TransactionBch, csTransaction: TransactionType, inputIndex: number, @@ -416,173 +547,6 @@ interface TransactionType { version: number; } -export const getLibauthTemplates = ( - txn: TransactionBuilder, -): WalletTemplate => { - if (txn.inputs.some((input) => !isStandardUnlockableUtxo(input))) { - throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); - } - - const libauthTransaction = txn.buildLibauthTransaction(); - const csTransaction = createTransactionTypeFromTransactionBuilder(txn); - - const baseTemplate: WalletTemplate = { - $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', - description: 'Imported from cashscript', - name: 'CashScript Generated Debugging Template', - supported: ['BCH_2025_05'], - version: 0, - entities: {}, - scripts: {}, - scenarios: {}, - }; - - // Initialize collections for entities, scripts, and scenarios - const entities: Record = {}; - const scripts: Record = {}; - const scenarios: Record = {}; - - // Initialize collections for P2PKH entities and scripts - const p2pkhEntities: Record = {}; - const p2pkhScripts: Record = {}; - - // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts - const unlockingBytecodeToLockingBytecodeParams: Record = {}; - const lockingBytecodeToLockingBytecodeParams: Record = {}; - - // We can typecast this because we check that all inputs are standard unlockable at the top of this function - for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) { - if (isP2PKHUnlocker(input.unlocker)) { - Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex)); - Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(inputIndex)); - Object.assign(scenarios, generateTemplateScenariosP2PKH(libauthTransaction, csTransaction, inputIndex)); - continue; - } - - if (isContractUnlocker(input.unlocker)) { - const contract = input.unlocker?.contract; - const abiFunction = input.unlocker?.abiFunction; - - if (!abiFunction) { - throw new Error('No ABI function found in unlocker'); - } - - // Encode the function arguments for this contract input - const encodedArgs = encodeFunctionArguments( - abiFunction, - input.unlocker.params ?? [], - ); - - // Generate a scenario object for this contract input - Object.assign(scenarios, - generateTemplateScenarios( - contract, - libauthTransaction, - csTransaction, - abiFunction, - encodedArgs, - inputIndex, - ), - ); - - // Generate entities for this contract input - const entity = generateTemplateEntitiesP2SH( - contract, - abiFunction, - encodedArgs, - inputIndex, - ); - - // Generate scripts for this contract input - const script = generateTemplateScriptsP2SH( - contract, - abiFunction, - encodedArgs, - contract.encodedConstructorArgs, - inputIndex, - ); - - // Find the lock script name for this contract input - const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock')); - if (lockScriptName) { - // Generate bytecodes for this contract input - const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); - const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); - - // Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios - unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; - // Assign a name to the locking bytecode so later it can be used to replace with bytecode/slot in scenarios - lockingBytecodeToLockingBytecodeParams[binToHex(addressToLockScript(contract.address))] = lockingScriptParams; - } - - // Add entities and scripts to the base template and repeat the process for the next input - Object.assign(entities, entity); - Object.assign(scripts, script); - } - } - - Object.assign(entities, p2pkhEntities); - Object.assign(scripts, p2pkhScripts); - - const finalTemplate = { ...baseTemplate, entities, scripts, scenarios }; - - // Loop through all scenarios and map the locking and unlocking scripts to the scenarios - // Replace the script tag with the identifiers we created earlier - - // For Inputs - for (const scenario of Object.values(scenarios)) { - for (const [idx, input] of libauthTransaction.inputs.entries()) { - const unlockingBytecode = binToHex(input.unlockingBytecode); - - // If false then it stays lockingBytecode: {} - if (unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]) { - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; - - // If true then assign a name to the locking bytecode script. - if (scenario.sourceOutputs && scenario.sourceOutputs[idx]) { - scenario.sourceOutputs[idx] = { - ...scenario.sourceOutputs[idx], - lockingBytecode: unlockingBytecodeToLockingBytecodeParams[unlockingBytecode], - }; - } - } - } - - // For Outputs - for (const [idx, output] of libauthTransaction.outputs.entries()) { - const lockingBytecode = binToHex(output.lockingBytecode); - - // If false then it stays lockingBytecode: {} - if (lockingBytecodeToLockingBytecodeParams[lockingBytecode]) { - - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; - - // If true then assign a name to the locking bytecode script. - if (scenario?.transaction && scenario?.transaction?.outputs && scenario?.transaction?.outputs[idx]) { - scenario.transaction.outputs[idx] = { - ...scenario.transaction.outputs[idx], - lockingBytecode: lockingBytecodeToLockingBytecodeParams[lockingBytecode], - }; - } - } - } - - } - - return finalTemplate; -}; - -export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { - const allArtifacts = transaction.inputs - .map(input => isContractUnlocker(input.unlocker) ? input.unlocker.contract : undefined) - .filter((contract): contract is Contract => Boolean(contract)) - .map(contract => contract.artifact); - - return debugTemplate(template, allArtifacts); -}; - const generateLockingScriptParams = ( contract: Contract, { unlocker }: StandardUnlockableUtxo, @@ -612,7 +576,7 @@ const generateLockingScriptParams = ( }; }; -export const generateUnlockingScriptParams = ( +const generateUnlockingScriptParams = ( csInput: StandardUnlockableUtxo, libauthInput: Input, p2pkhScriptNameTemplate: string, @@ -664,13 +628,8 @@ const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, input return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; }; -export const getBitauthUri = (template: WalletTemplate): string => { - const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); - const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); - return `https://ide.bitauth.com/import-template/${payload}`; -}; -export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { +const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { const signatureAlgorithmNames = { [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', @@ -679,7 +638,7 @@ export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm return signatureAlgorithmNames[signatureAlgorithm]; }; -export const getHashTypeName = (hashType: HashType): string => { +const getHashTypeName = (hashType: HashType): string => { const hashtypeNames = { [HashType.SIGHASH_ALL]: 'all_outputs', [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', @@ -698,12 +657,11 @@ export const getHashTypeName = (hashType: HashType): string => { return hashtypeNames[hashType]; }; -export const addHexPrefixExceptEmpty = (value: string): string => { +const addHexPrefixExceptEmpty = (value: string): string => { return value.length > 0 ? `0x${value}` : ''; }; - -export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { +const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { if (types.length === 0) return '// none'; // We reverse the arguments because the order of the arguments in the bytecode is reversed @@ -724,7 +682,7 @@ export const formatParametersForDebugging = (types: readonly AbiInput[], args: E }).join('\n'); }; -export const formatBytecodeForDebugging = (artifact: Artifact): string => { +const formatBytecodeForDebugging = (artifact: Artifact): string => { if (!artifact.debug) { return artifact.bytecode .split(' ') @@ -739,7 +697,7 @@ export const formatBytecodeForDebugging = (artifact: Artifact): string => { ); }; -export const serialiseTokenDetails = ( +const serialiseTokenDetails = ( token?: TokenDetails | LibauthTokenDetails, ): LibauthTemplateTokenDetails | undefined => { if (!token) return undefined; @@ -754,7 +712,7 @@ export const serialiseTokenDetails = ( }; }; -export const generateTemplateScenarioParametersValues = ( +const generateTemplateScenarioParametersValues = ( types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[], ): Record => { @@ -772,7 +730,7 @@ export const generateTemplateScenarioParametersValues = ( return Object.fromEntries(entries); }; -export const generateTemplateScenarioKeys = ( +const generateTemplateScenarioKeys = ( types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[], ): Record => { @@ -786,7 +744,7 @@ export const generateTemplateScenarioKeys = ( }; // Used for generating the locking / unlocking bytecode for source outputs and inputs -export const generateTemplateScenarioBytecode = ( +const generateTemplateScenarioBytecode = ( input: Utxo, libauthInput: Input, inputIndex: number, @@ -804,7 +762,7 @@ export const generateTemplateScenarioBytecode = ( return {}; }; -export const generateTemplateScenarioTransactionOutputLockingBytecode = ( +const generateTemplateScenarioTransactionOutputLockingBytecode = ( csOutput: Output, contract: Contract, ): string | {} => { @@ -813,7 +771,7 @@ export const generateTemplateScenarioTransactionOutputLockingBytecode = ( return binToHex(addressToLockScript(csOutput.to)); }; -export const generateTemplateScenarioParametersFunctionIndex = ( +const generateTemplateScenarioParametersFunctionIndex = ( abiFunction: AbiFunction, abi: readonly AbiFunction[], ): Record => { @@ -824,7 +782,7 @@ export const generateTemplateScenarioParametersFunctionIndex = ( return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; }; -export interface LibauthTemplateTokenDetails { +interface LibauthTemplateTokenDetails { amount: string; category: string; nft?: { From 8b540848980ee018bce815631c6067a225fa59de Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 11:08:47 +0200 Subject: [PATCH 12/26] Address some TODOs in LibauthTemplate --- packages/cashscript/src/LibauthTemplate.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index 566e80ae..e09652eb 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -48,6 +48,8 @@ import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHI import { TransactionBuilder } from './TransactionBuilder.js'; import { deflate } from 'pako'; +// TODO: Add / improve descriptions throughout the template generation + export const getLibauthTemplates = ( txn: TransactionBuilder, ): WalletTemplate => { @@ -227,7 +229,6 @@ const generateTemplateEntitiesP2PKH = ( const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; - // TODO: Add descriptions return { [`signer_${inputIndex}`]: { scripts: [lockScriptName, unlockScriptName], @@ -411,7 +412,6 @@ const generateTemplateScenarios = ( const encodedConstructorArgs = contract.encodedConstructorArgs; const scenarioIdentifier = `${artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; - // TODO: Update scenario descriptions const scenarios = { // single scenario to spend out transaction under test given the CashScript parameters provided [scenarioIdentifier]: { @@ -420,10 +420,10 @@ const generateTemplateScenarios = ( data: { // encode values for the variables defined above in `entities` property bytecode: { + ...generateTemplateScenarioParametersFunctionIndex(abiFunction, contract.artifact.abi), ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), }, - // TODO: remove usage of private keys in P2SH scenarios as well keys: { privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, @@ -433,14 +433,6 @@ const generateTemplateScenarios = ( }, }; - // TODO: understand exactly what this does, and refactor - // Looks similar to code in generateTemplateScenarioParametersFunctionIndex - // Looks like we just want to use that function and spread in the scenarios data bytecode field - if (artifact.abi.length > 1) { - const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name); - scenarios![scenarioIdentifier].data!.bytecode!.function_index = functionIndex.toString(); - } - return scenarios; }; @@ -450,10 +442,8 @@ const generateTemplateScenariosP2PKH = ( inputIndex: number, ): WalletTemplate['scenarios'] => { const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`; - const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthTransaction.inputs[inputIndex]); - // TODO: Update scenario descriptions const scenarios = { // single scenario to spend out transaction under test given the CashScript parameters provided [scenarioIdentifier]: { @@ -609,7 +599,6 @@ const generateUnlockingScriptParams = ( ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), ...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs), }, - // TODO: remove usage of private keys in P2SH scenarios as well keys: { privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, From 3ad11e470794a27d040fd83463ea01f417591278 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 18 Sep 2025 11:25:19 +0200 Subject: [PATCH 13/26] No longer seed MockNetworkProvider with any test UTXOs --- .../src/network/MockNetworkProvider.ts | 14 +--- .../test/TransactionBuilder.test.ts | 4 +- packages/cashscript/test/debugging.test.ts | 4 + .../multi-contract-fixtures.ts | 84 +++++++++++++++++++ website/docs/releases/release-notes.md | 1 + 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/packages/cashscript/src/network/MockNetworkProvider.ts b/packages/cashscript/src/network/MockNetworkProvider.ts index fd0f8c5f..52e9d02b 100644 --- a/packages/cashscript/src/network/MockNetworkProvider.ts +++ b/packages/cashscript/src/network/MockNetworkProvider.ts @@ -2,12 +2,7 @@ import { binToHex, decodeTransactionUnsafe, hexToBin, isHex } from '@bitauth/lib import { sha256 } from '@cashscript/utils'; import { Utxo, Network } from '../interfaces.js'; import NetworkProvider from './NetworkProvider.js'; -import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails, randomUtxo } from '../utils.js'; - -// redeclare the addresses from vars.ts instead of importing them -const aliceAddress = 'bchtest:qpgjmwev3spwlwkgmyjrr2s2cvlkkzlewq62mzgjnp'; -const bobAddress = 'bchtest:qz6q5gqnxdldkr07xpls5474mmzmlesd6qnux4skuc'; -const carolAddress = 'bchtest:qqsr7nqwe6rq5crj63gy5gdqchpnwmguusmr7tfmsj'; +import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails } from '../utils.js'; interface MockNetworkProviderOptions { updateUtxoSet: boolean; @@ -23,13 +18,6 @@ export default class MockNetworkProvider implements NetworkProvider { constructor(options?: Partial) { this.options = { updateUtxoSet: true, ...options }; - - for (let i = 0; i < 3; i += 1) { - // TODO: Don't seed the MockNetworkProvider with any UTXOs - this.addUtxo(aliceAddress, randomUtxo()); - this.addUtxo(bobAddress, randomUtxo()); - this.addUtxo(carolAddress, randomUtxo()); - } } async getUtxos(address: string): Promise { diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index da1d7f9d..fc12e53d 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -39,6 +39,8 @@ describe('Transaction Builder', () => { (provider as any).addUtxo?.(p2pkhInstance.address, randomUtxo({ token: randomToken() })); (provider as any).addUtxo?.(twtInstance.address, randomUtxo()); (provider as any).addUtxo?.(twtInstance.address, randomUtxo()); + (provider as any).addUtxo?.(aliceAddress, randomUtxo()); + (provider as any).addUtxo?.(aliceAddress, randomUtxo()); (provider as any).addUtxo?.(bobAddress, randomUtxo()); (provider as any).addUtxo?.(bobAddress, randomUtxo()); (provider as any).addUtxo?.(carolAddress, randomUtxo()); @@ -206,7 +208,7 @@ describe('Transaction Builder', () => { const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); const sigTemplate = new SignatureTemplate(alicePriv); - expect(aliceUtxos.length).toBeGreaterThan(2); + expect(aliceUtxos.length).toBe(2); const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index 8e5942d7..c174f473 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -624,6 +624,8 @@ describe('Debugging tests', () => { describe('P2PKH only transaction', () => { it('should succeed when spending from P2PKH inputs with the corresponding unlocker', async () => { const provider = new MockNetworkProvider(); + provider.addUtxo(aliceAddress, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); const result = new TransactionBuilder({ provider }) .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(alicePriv).unlockP2PKH()) @@ -637,6 +639,8 @@ describe('Debugging tests', () => { // Note: that also goes for Contract UTXOs where a user uses an unlocker of a different contract it.skip('should fail when spending from P2PKH inputs with an unlocker for a different public key', async () => { const provider = new MockNetworkProvider(); + provider.addUtxo(aliceAddress, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); const transactionBuilder = new TransactionBuilder({ provider }) .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(bobPriv).unlockP2PKH()) diff --git a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts index e88dae8f..e22dfaf9 100644 --- a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts @@ -485,6 +485,10 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -497,6 +501,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -723,6 +735,10 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': [ 'slot', ], + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -735,6 +751,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -967,12 +991,24 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { 'lockingBytecode': [ 'slot', ], + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1205,6 +1241,10 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -1217,6 +1257,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1444,6 +1492,10 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -1456,6 +1508,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1683,6 +1743,10 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -1695,6 +1759,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1922,6 +1994,10 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { @@ -1934,6 +2010,14 @@ export const fixtures: Fixture[] = [ }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 38797b94..703479ab 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -8,6 +8,7 @@ title: Release Notes - :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. - :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). +- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. - :hammer_and_wrench: Improve libauth template generation. ## v0.11.5 From a7a37e1ae45a45c3e7df027011fe21ebb4bf8e64 Mon Sep 17 00:00:00 2001 From: mainnet-pat <74184164+mainnet-pat@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:15:19 +0300 Subject: [PATCH 14/26] Update libauth, allow to set debugger's target VM version (#353) Co-authored-by: Rosco Kalis --- packages/cashc/package.json | 2 +- packages/cashscript/package.json | 2 +- packages/cashscript/src/LibauthTemplate.ts | 6 ++- packages/cashscript/src/debugging.ts | 46 +++++++++++++------ packages/cashscript/src/interfaces.ts | 9 ++++ .../src/network/MockNetworkProvider.ts | 7 ++- packages/cashscript/test/debugging.test.ts | 30 +++++++++++- packages/utils/package.json | 2 +- website/docs/releases/release-notes.md | 1 + yarn.lock | 5 ++ 10 files changed, 90 insertions(+), 20 deletions(-) diff --git a/packages/cashc/package.json b/packages/cashc/package.json index 0c592984..0e90a460 100644 --- a/packages/cashc/package.json +++ b/packages/cashc/package.json @@ -51,7 +51,7 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", + "@bitauth/libauth": "^3.1.0-next.8", "@cashscript/utils": "^0.11.5", "antlr4": "^4.13.2", "commander": "^14.0.0", diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index d200fc41..c8722e06 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -45,7 +45,7 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", + "@bitauth/libauth": "^3.1.0-next.8", "@cashscript/utils": "^0.11.5", "@electrum-cash/network": "^4.1.3", "@mr-zwets/bchn-api-wrapper": "^1.0.1", diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index e09652eb..979e7357 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -42,11 +42,13 @@ import { TokenDetails, UnlockableUtxo, Utxo, + VmTarget, } from './interfaces.js'; import SignatureTemplate from './SignatureTemplate.js'; import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from './utils.js'; import { TransactionBuilder } from './TransactionBuilder.js'; import { deflate } from 'pako'; +import MockNetworkProvider from './network/MockNetworkProvider.js'; // TODO: Add / improve descriptions throughout the template generation @@ -60,11 +62,13 @@ export const getLibauthTemplates = ( const libauthTransaction = txn.buildLibauthTransaction(); const csTransaction = createTransactionTypeFromTransactionBuilder(txn); + const vmTarget = txn.provider instanceof MockNetworkProvider ? txn.provider.vmTarget : VmTarget.BCH_2025_05; + const baseTemplate: WalletTemplate = { $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', description: 'Imported from cashscript', name: 'CashScript Generated Debugging Template', - supported: ['BCH_2025_05'], + supported: [vmTarget], version: 0, entities: {}, scripts: {}, diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index 82023d5d..c138e819 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -1,12 +1,37 @@ -import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, AuthenticationVirtualMachine, ResolvedTransactionCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, createVirtualMachineBch2025, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth'; +import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, AuthenticationVirtualMachine, ResolvedTransactionCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, createVirtualMachineBch2023, createVirtualMachineBch2025, createVirtualMachineBch2026, createVirtualMachineBchSpec, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth'; import { Artifact, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils'; import { findLastIndex, toRegExp } from './utils.js'; import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js'; import { getBitauthUri } from './LibauthTemplate.js'; +import { VmTarget } from './interfaces.js'; export type DebugResult = AuthenticationProgramStateCommon[]; export type DebugResults = Record; +/* eslint-disable @typescript-eslint/indent */ +type VM = AuthenticationVirtualMachine< + ResolvedTransactionCommon, + AuthenticationProgramCommon, + AuthenticationProgramStateCommon +>; +/* eslint-enable @typescript-eslint/indent */ + +const createVirtualMachine = (vmTarget: VmTarget): VM => { + switch (vmTarget) { + case 'BCH_2023_05': + return createVirtualMachineBch2023(); + case 'BCH_2025_05': + return createVirtualMachineBch2025(); + case 'BCH_2026_05': + return createVirtualMachineBch2026(); + case 'BCH_SPEC': + // TODO: This typecast is shitty, but it's hard to fix + return createVirtualMachineBchSpec() as unknown as VM; + default: + throw new Error(`Debugging is not supported for the ${vmTarget} virtual machine.`); + } +}; + // debugs the template, optionally logging the execution data export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): DebugResults => { // If a contract has the same name, but a different bytecode, then it is considered a name collision @@ -61,7 +86,7 @@ const debugSingleScenario = ( for (const log of executedLogs) { const inputIndex = extractInputIndexFromScenario(scenarioId); - logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex); + logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex, vm); } } @@ -157,21 +182,13 @@ const extractInputIndexFromScenario = (scenarioId: string): number => { return parseInt(match[1]); }; -/* eslint-disable @typescript-eslint/indent */ -type VM = AuthenticationVirtualMachine< - ResolvedTransactionCommon, - AuthenticationProgramCommon, - AuthenticationProgramStateCommon ->; -/* eslint-enable @typescript-eslint/indent */ - type Program = AuthenticationProgramCommon; type CreateProgramResult = { vm: VM, program: Program }; // internal util. instantiates the virtual machine and compiles the template into a program const createProgram = (template: WalletTemplate, unlockingScriptId: string, scenarioId: string): CreateProgramResult => { const configuration = walletTemplateToCompilerConfiguration(template); - const vm = createVirtualMachineBch2025(); + const vm = createVirtualMachine(template.supported[0] as VmTarget); const compiler = createCompiler(configuration); if (!template.scripts[unlockingScriptId]) { @@ -204,13 +221,14 @@ const logConsoleLogStatement = ( debugSteps: AuthenticationProgramStateCommon[], artifact: Artifact, inputIndex: number, + vm: VM, ): void => { let line = `${artifact.contractName}.cash:${log.line}`; const decodedData = log.data.map((element) => { if (typeof element === 'string') return element; const debugStep = debugSteps.find((step) => step.ip === element.ip)!; - const transformedDebugStep = applyStackItemTransformations(element, debugStep); + const transformedDebugStep = applyStackItemTransformations(element, debugStep, vm); return decodeStackItem(element, transformedDebugStep.stack); }); console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`); @@ -219,6 +237,7 @@ const logConsoleLogStatement = ( const applyStackItemTransformations = ( element: StackItem, debugStep: AuthenticationProgramStateCommon, + vm: VM, ): AuthenticationProgramStateCommon => { if (!element.transformations) return debugStep; @@ -236,9 +255,10 @@ const applyStackItemTransformations = ( instructions: transformationsAuthenticationInstructions, signedMessages: [], program: { ...debugStep.program }, + functionTable: debugStep.functionTable ?? {}, + functionCount: debugStep.functionCount ?? 0, }; - const vm = createVirtualMachineBch2025(); const transformationsEndState = vm.stateEvaluate(transformationsStartState); return transformationsEndState; diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index d5a88c48..8e89e39e 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -144,6 +144,15 @@ export const Network = { export type Network = (typeof Network)[keyof typeof Network]; +export const VmTarget = { + BCH_2023_05: literal('BCH_2023_05'), + BCH_2025_05: literal('BCH_2025_05'), + BCH_2026_05: literal('BCH_2026_05'), + BCH_SPEC: literal('BCH_SPEC'), +}; + +export type VmTarget = (typeof VmTarget)[keyof typeof VmTarget]; + export interface TransactionDetails extends Transaction { txid: string; hex: string; diff --git a/packages/cashscript/src/network/MockNetworkProvider.ts b/packages/cashscript/src/network/MockNetworkProvider.ts index 52e9d02b..2af9b763 100644 --- a/packages/cashscript/src/network/MockNetworkProvider.ts +++ b/packages/cashscript/src/network/MockNetworkProvider.ts @@ -1,11 +1,12 @@ import { binToHex, decodeTransactionUnsafe, hexToBin, isHex } from '@bitauth/libauth'; import { sha256 } from '@cashscript/utils'; -import { Utxo, Network } from '../interfaces.js'; +import { Utxo, Network, VmTarget } from '../interfaces.js'; import NetworkProvider from './NetworkProvider.js'; import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails } from '../utils.js'; -interface MockNetworkProviderOptions { +export interface MockNetworkProviderOptions { updateUtxoSet: boolean; + vmTarget?: VmTarget; } export default class MockNetworkProvider implements NetworkProvider { @@ -15,9 +16,11 @@ export default class MockNetworkProvider implements NetworkProvider { public network: Network = Network.MOCKNET; public blockHeight: number = 133700; public options: MockNetworkProviderOptions; + public vmTarget: VmTarget; constructor(options?: Partial) { this.options = { updateUtxoSet: true, ...options }; + this.vmTarget = this.options.vmTarget ?? VmTarget.BCH_2025_05; } async getUtxos(address: string): Promise { diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index c174f473..3183bb64 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -1,4 +1,4 @@ -import { Contract, FailedTransactionError, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { Contract, FailedTransactionError, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, VmTarget } from '../src/index.js'; import { aliceAddress, alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; import '../src/test/JestExtensions.js'; import { randomUtxo } from '../src/utils.js'; @@ -649,4 +649,32 @@ describe('Debugging tests', () => { expect(() => transactionBuilder.debug()).toThrow(FailedTransactionError); }); }); + + describe('VmTargets', () => { + const vmTargets = [ + undefined, + VmTarget.BCH_2023_05, + VmTarget.BCH_2025_05, + VmTarget.BCH_2026_05, + VmTarget.BCH_SPEC, + ] as const; + + for (const vmTarget of vmTargets) { + it(`should execute and log correctly with vmTarget ${vmTarget}`, async () => { + const provider = new MockNetworkProvider({ vmTarget }); + const contractTestLogs = new Contract(artifactTestLogs, [alicePub], { provider }); + const contractUtxo = randomUtxo(); + provider.addUtxo(contractTestLogs.address, contractUtxo); + + const transaction = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contractTestLogs.unlock.transfer(new SignatureTemplate(alicePriv), 1000n)) + .addOutput({ to: contractTestLogs.address, amount: 10000n }); + + expect(transaction.getLibauthTemplate().supported[0]).toBe(vmTarget ?? 'BCH_2025_05'); + + const expectedLog = new RegExp(`^\\[Input #0] Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 1000 0xbeef 1 test true$`); + expect(transaction).toLog(expectedLog); + }); + } + }); }); diff --git a/packages/utils/package.json b/packages/utils/package.json index 76a99341..c29a3171 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -44,7 +44,7 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2" + "@bitauth/libauth": "^3.1.0-next.8" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 703479ab..9576a66d 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -9,6 +9,7 @@ title: Release Notes - :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. +- :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :hammer_and_wrench: Improve libauth template generation. ## v0.11.5 diff --git a/yarn.lock b/yarn.lock index 0daa8efd..ad42ae0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -630,6 +630,11 @@ resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.2.tgz#121782b38774d9fba8226406db9b9af0c8d8e464" integrity sha512-XRtk9p8UHvtjSPS38rsfHXzaPHG5j9FpN4qHqqGLoAuZYy675PBiOy9zP6ah8lTnnIVaCFl2ekct8w0Hy1oefw== +"@bitauth/libauth@^3.1.0-next.8": + version "3.1.0-next.8" + resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz#d130e5db6c3c8b24731c8d04c4091be07f48b0ee" + integrity sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA== + "@chris.troutner/bip32-utils@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@chris.troutner/bip32-utils/-/bip32-utils-1.0.5.tgz#b6722aeaad5fcda6fba69cbeda7e2a5e8afbd692" From d349ebfd690c16ba8d2b308db66ddfca61edc1c7 Mon Sep 17 00:00:00 2001 From: mainnet-pat <74184164+mainnet-pat@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:57:35 +0300 Subject: [PATCH 15/26] Better support for ECDSA signatures in ABI arguments (#319) Co-authored-by: Rosco Kalis --- packages/cashscript/src/Argument.ts | 20 ++-- .../cashscript/test/e2e/HodlVault.test.ts | 110 +++++++++++++++++- .../cashscript/test/fixture/PriceOracle.ts | 8 +- website/docs/releases/release-notes.md | 1 + 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/packages/cashscript/src/Argument.ts b/packages/cashscript/src/Argument.ts index 915fe84d..9871ac99 100644 --- a/packages/cashscript/src/Argument.ts +++ b/packages/cashscript/src/Argument.ts @@ -59,16 +59,20 @@ export function encodeFunctionArgument(argument: FunctionArgument, typeStr: stri throw Error(`Value for type ${type} should be a Uint8Array or hex string`); } - // Redefine SIG as a bytes65 so it is included in the size checks below - // Note that ONLY Schnorr signatures are accepted - if (type === PrimitiveType.SIG && argument.byteLength !== 0) { - type = new BytesType(65); + // Redefine SIG as a bytes65 (Schnorr) or bytes71, bytes72, bytes73 (ECDSA) or bytes0 (for NULLFAIL) + if (type === PrimitiveType.SIG) { + if (![0, 65, 71, 72, 73].includes(argument.byteLength)) { + throw new TypeError(`bytes${argument.byteLength}`, type); + } + type = new BytesType(argument.byteLength); } - // Redefine DATASIG as a bytes64 so it is included in the size checks below - // Note that ONLY Schnorr signatures are accepted - if (type === PrimitiveType.DATASIG && argument.byteLength !== 0) { - type = new BytesType(64); + // Redefine DATASIG as a bytes64 (Schnorr) or bytes70, bytes71, bytes72 (ECDSA) or bytes0 (for NULLFAIL) + if (type === PrimitiveType.DATASIG) { + if (![0, 64, 70, 71, 72].includes(argument.byteLength)) { + throw new TypeError(`bytes${argument.byteLength}`, type); + } + type = new BytesType(argument.byteLength); } // Bounded bytes types require a correctly sized argument diff --git a/packages/cashscript/test/e2e/HodlVault.test.ts b/packages/cashscript/test/e2e/HodlVault.test.ts index cf2316b7..922cc200 100644 --- a/packages/cashscript/test/e2e/HodlVault.test.ts +++ b/packages/cashscript/test/e2e/HodlVault.test.ts @@ -5,6 +5,8 @@ import { ElectrumNetworkProvider, Network, TransactionBuilder, + SignatureAlgorithm, + HashType, } from '../../src/index.js'; import { alicePriv, @@ -12,10 +14,11 @@ import { oracle, oraclePub, } from '../fixture/vars.js'; -import { gatherUtxos, getTxOutputs } from '../test-util.js'; +import { gatherUtxos, getTxOutputs, itOrSkip } from '../test-util.js'; import { FailedRequireError } from '../../src/Errors.js'; import artifact from '../fixture/hodl_vault.artifact.js'; import { randomUtxo } from '../../src/utils.js'; +import { placeholder } from '@cashscript/utils'; describe('HodlVault', () => { const provider = process.env.TESTS_USE_CHIPNET @@ -95,5 +98,110 @@ describe('HodlVault', () => { const txOutputs = getTxOutputs(tx); expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); }); + + it('should succeed when price is high enough, ECDSA sig and datasig', async () => { + // given + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = hodlVault.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n }); + + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA); + + // when + const tx = await new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send(); + + // then + const txOutputs = getTxOutputs(tx); + expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); + }); + + itOrSkip(!Boolean(process.env.TESTS_USE_CHIPNET), 'should succeed with precomputed ECDSA signature', async () => { + // given + const cleanProvider = new MockNetworkProvider(); + const contract = new Contract(artifact, [alicePub, oraclePub, 99000n, 30000n], { provider: cleanProvider }); + cleanProvider.addUtxo(contract.address, { + satoshis: 100000n, + txid: '11'.repeat(32), + vout: 0, + }); + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = contract.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await contract.getUtxos(), { amount, fee: 2000n }); + const signature = '3045022100aa004a425c0c911594c0333164f990c760991b7f84272f35d98c9c6617d9c53602207dfe4729224d4e61496dff11963982cf79f05d623a6e4004b5f50b7cefa7175241'; + + // when + const tx = await new TransactionBuilder({ provider: cleanProvider }) + .addInputs(utxos, contract.unlock.spend(signature, oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send(); + + // then + const txOutputs = getTxOutputs(tx); + expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); + }); + + it('should fail to accept wrong signature lengths', async () => { + // given + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = hodlVault.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n }); + + // sig: unlocker should throw when given an improper length + expect(() => hodlVault.unlock.spend(placeholder(100), oracleSig, message)).toThrow("Found type 'bytes100' where type 'sig' was expected"); + + // sig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig + // Note that this fails with "FailedTransactionEvaluationError" because an invalid signature encoding is NOT a failed + // require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(placeholder(71), oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:27 Error in transaction at input 0 in contract HodlVault.cash at line 27'); + + // sig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement + // Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(placeholder(0), oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:27 Require statement failed at input 0 in contract HodlVault.cash at line 27'); + + // datasig: unlocker should throw when given an improper length + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA); + expect(() => hodlVault.unlock.spend(signatureTemplate, placeholder(100), message)).toThrow("Found type 'bytes100' where type 'datasig' was expected"); + + // datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig + // TODO: This somehow fails with "FailedRequireError" instead of "FailedTransactionEvaluationError", check why + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(64), message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26'); + + // datasig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement + // Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(0), message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26'); + }); }); }); diff --git a/packages/cashscript/test/fixture/PriceOracle.ts b/packages/cashscript/test/fixture/PriceOracle.ts index eb174099..f8643ac4 100644 --- a/packages/cashscript/test/fixture/PriceOracle.ts +++ b/packages/cashscript/test/fixture/PriceOracle.ts @@ -1,5 +1,6 @@ import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm } from '../../src/index.js'; export class PriceOracle { constructor(public privateKey: Uint8Array) {} @@ -12,8 +13,11 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signature = signatureAlgorithm === SignatureAlgorithm.SCHNORR ? + secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)) : + secp256k1.signMessageHashDER(this.privateKey, sha256(message)); + if (typeof signature === 'string') throw new Error(); return signature; } diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 9576a66d..7533629c 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -10,6 +10,7 @@ title: Release Notes - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. +- :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. - :hammer_and_wrench: Improve libauth template generation. ## v0.11.5 From fa4785ad69312bfdb4220d03c22043c98865df7d Mon Sep 17 00:00:00 2001 From: mainnet-pat <74184164+mainnet-pat@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:57:52 +0300 Subject: [PATCH 16/26] Add vmResourceUsage method to TransactionBuilder (#354) Co-authored-by: Rosco Kalis --- packages/cashscript/src/TransactionBuilder.ts | 40 +++++++++++++++++++ packages/cashscript/src/interfaces.ts | 4 +- packages/cashscript/test/debugging.test.ts | 35 ++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index a52f3838..81d39874 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -17,6 +17,8 @@ import { isUnlockableUtxo, isStandardUnlockableUtxo, StandardUnlockableUtxo, + VmResourceUsage, + isContractUnlocker, } from './interfaces.js'; import { NetworkProvider } from './network/index.js'; import { @@ -172,6 +174,44 @@ export class TransactionBuilder { return debugLibauthTemplate(this.getLibauthTemplate(), this); } + getVmResourceUsage(verbose: boolean = false): Array { + // Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers + // cannot be debugged (and therefore cannot return VM resource usage) + const results = this.debug(); + const vmResourceUsage: Array = []; + const tableData: Array> = []; + + const formatMetric = (value: number, total: number, withPercentage: boolean = false): string => + `${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`; + const formatNumber = (value: number): string => value.toLocaleString('en'); + + const resultEntries = Object.entries(results); + for (const [index, input] of this.inputs.entries()) { + const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? []; + const metrics = result?.at(-1)?.metrics; + + // Should not happen + if (!metrics) throw new Error('VM resource could not be calculated'); + + vmResourceUsage.push(metrics); + tableData.push({ + 'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input', + Ops: metrics.evaluatedInstructionCount, + 'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true), + SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount), + Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations), + }); + } + + if (verbose) { + console.log('VM Resource usage by inputs:'); + console.table(tableData); + } + + return vmResourceUsage; + } + + // TODO: rename to getBitauthUri() bitauthUri(): string { console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'); return getBitauthUri(this.getLibauthTemplate()); diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index 8e89e39e..badfc116 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -1,4 +1,4 @@ -import { type Transaction } from '@bitauth/libauth'; +import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth'; import type { NetworkProvider } from './network/index.js'; import type SignatureTemplate from './SignatureTemplate.js'; import { Contract } from './Contract.js'; @@ -164,3 +164,5 @@ export interface ContractOptions { } export type AddressType = 'p2sh20' | 'p2sh32'; + +export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics']; diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index 3183bb64..0cd18b9d 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -678,3 +678,38 @@ describe('Debugging tests', () => { } }); }); + +describe('VM Resources', () => { + it('Should output VM resource usage', async () => { + const provider = new MockNetworkProvider(); + + const contractSingleFunction = new Contract({ ...artifactTestSingleFunction, contractName: 'SingleFunction' }, [], { provider }); + const contractZeroHandling = new Contract({ ...artifactTestZeroHandling, contractName: 'ZeroHandling' }, [0n], { provider }); + + provider.addUtxo(contractSingleFunction.address, randomUtxo()); + provider.addUtxo(contractZeroHandling.address, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); + + const tx = new TransactionBuilder({ provider }) + .addInputs(await contractSingleFunction.getUtxos(), contractSingleFunction.unlock.test_require_single_function()) + .addInputs(await contractZeroHandling.getUtxos(), contractZeroHandling.unlock.test_zero_handling(0n)) + .addInput((await provider.getUtxos(aliceAddress))[0], new SignatureTemplate(alicePriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 1000n }); + + console.log = jest.fn(); + console.table = jest.fn(); + + const vmUsage = tx.getVmResourceUsage(); + expect(console.log).not.toHaveBeenCalled(); + expect(console.table).not.toHaveBeenCalled(); + + tx.getVmResourceUsage(true); + expect(console.log).toHaveBeenCalledWith('VM Resource usage by inputs:'); + expect(console.table).toHaveBeenCalled(); + + jest.restoreAllMocks(); + + expect(vmUsage[0]?.hashDigestIterations).toBeGreaterThan(0); + expect(vmUsage[2]?.hashDigestIterations).toBeGreaterThan(0); + }); +}); From 328657cf88adf347834d4c4d71e5090e2c933b73 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 25 Sep 2025 11:38:09 +0200 Subject: [PATCH 17/26] Fix bug in SignatureTemplate where private key hex strings were not supported --- packages/cashscript/src/SignatureTemplate.ts | 9 +++++++-- website/docs/releases/release-notes.md | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cashscript/src/SignatureTemplate.ts b/packages/cashscript/src/SignatureTemplate.ts index e834a32b..301cf427 100644 --- a/packages/cashscript/src/SignatureTemplate.ts +++ b/packages/cashscript/src/SignatureTemplate.ts @@ -1,4 +1,4 @@ -import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth'; +import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth'; import { hash256, scriptToBytecode } from '@cashscript/utils'; import { GenerateUnlockingBytecodeOptions, @@ -20,7 +20,12 @@ export default class SignatureTemplate { const wif = signer.toWIF(); this.privateKey = decodeWif(wif); } else if (typeof signer === 'string') { - this.privateKey = decodeWif(signer); + const maybeHexString = signer.startsWith('0x') ? signer.slice(2) : signer; + if (isHex(maybeHexString)) { + this.privateKey = hexToBin(maybeHexString); + } else { + this.privateKey = decodeWif(maybeHexString); + } } else { this.privateKey = signer; } diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 7533629c..02fca242 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -12,6 +12,7 @@ title: Release Notes - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. - :hammer_and_wrench: Improve libauth template generation. +- :bug: Fix bug where `SignatureTemplate` would not accept private key hex strings as a signer. ## v0.11.5 From 65af68aeb9ec2e279227f3f0fa4a3182cea296e3 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 25 Sep 2025 11:58:49 +0200 Subject: [PATCH 18/26] Add tests for contract unlockers and SignatureTemplate --- packages/cashscript/src/SignatureTemplate.ts | 2 +- packages/cashscript/test/Contract.test.ts | 30 ++++++- .../cashscript/test/SignatureTemplate.test.ts | 80 +++++++++++++++++++ packages/cashscript/test/fixture/vars.ts | 4 + 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 packages/cashscript/test/SignatureTemplate.test.ts diff --git a/packages/cashscript/src/SignatureTemplate.ts b/packages/cashscript/src/SignatureTemplate.ts index 301cf427..c3461853 100644 --- a/packages/cashscript/src/SignatureTemplate.ts +++ b/packages/cashscript/src/SignatureTemplate.ts @@ -71,7 +71,7 @@ export default class SignatureTemplate { } } -// Works for both BITBOX/bitcoincash.js ECPair and bitcore-lib-cash PrivateKey +// Works for both bitcoincash.js/bchjs ECPair and bitcore-lib-cash PrivateKey interface Keypair { toWIF(): string; } diff --git a/packages/cashscript/test/Contract.test.ts b/packages/cashscript/test/Contract.test.ts index d41baccb..159d0d2d 100644 --- a/packages/cashscript/test/Contract.test.ts +++ b/packages/cashscript/test/Contract.test.ts @@ -7,10 +7,13 @@ import { Network, randomUtxo, SignatureTemplate, + TransactionBuilder, } from '../src/index.js'; import { + aliceAddress, alicePkh, alicePriv, alicePub, bobPriv, } from './fixture/vars.js'; +import { generateLibauthSourceOutputs } from '../src/utils.js'; import p2pkhArtifact from './fixture/p2pkh.artifact.js'; import twtArtifact from './fixture/transfer_with_timeout.artifact.js'; import hodlVaultArtifact from './fixture/hodl_vault.artifact.js'; @@ -137,11 +140,12 @@ describe('Contract', () => { }); describe('Contract unlockers', () => { + let provider: MockNetworkProvider; let instance: Contract; let bbInstance: Contract; beforeEach(() => { - const provider = new ElectrumNetworkProvider(Network.CHIPNET); + provider = new MockNetworkProvider(); instance = new Contract(p2pkhArtifact, [alicePkh], { provider }); bbInstance = new Contract(boundedBytesArtifact, [], { provider }); }); @@ -164,5 +168,29 @@ describe('Contract', () => { expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))).not.toThrow(); expect(() => bbInstance.unlock.spend(hexToBin('e8030000'), 1000n)).not.toThrow(); }); + + it('generates correct locking bytecode', () => { + expect(instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)).generateLockingBytecode()) + .toEqual(hexToBin('aa2034d9ffce86b4d136ca74e9db6f6433d3548966a6be064052e728a4c1d16aa3a587')); + }); + + it('generates correct unlocking bytecode', () => { + const utxo = { + txid: 'e5ac1aa9730d7514b541895e466c987327a4b0c57fcbbd50fc73788f5c0f65d9', + vout: 4, + satoshis: 102745n, + }; + + const unlocker = instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const transactionBuilder = new TransactionBuilder({ provider }) + .addInput(utxo, unlocker) + .addOutput({ to: aliceAddress, amount: 1000n }); + + const transaction = transactionBuilder.buildLibauthTransaction(); + const sourceOutputs = generateLibauthSourceOutputs(transactionBuilder.inputs); + + expect(unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex: 0 })) + .toEqual(hexToBin('4135fac4118af15e0d66f30548dd0c31e1108f3389af96bb9db4f2305706e18fe52cc7163f6440fae98c48332d09c30380527a90604f14b4b3fc0c3aa0884c9c0a61210373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c0881914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97078a988ac')); + }); }); }); diff --git a/packages/cashscript/test/SignatureTemplate.test.ts b/packages/cashscript/test/SignatureTemplate.test.ts new file mode 100644 index 00000000..763903cd --- /dev/null +++ b/packages/cashscript/test/SignatureTemplate.test.ts @@ -0,0 +1,80 @@ +import { generateLibauthSourceOutputs } from 'cashscript/dist/utils.js'; +import { HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { aliceAddress, alicePriv, alicePub, aliceWif } from './fixture/vars.js'; +import { binToHex, hexToBin } from '@bitauth/libauth'; + +describe('SignatureTemplate', () => { + describe('constructor', () => { + it('should properly convert different signer formats to raw private key', () => { + const mockKeyPair = { toWIF: () => aliceWif }; + const fromKeyPair = new SignatureTemplate(mockKeyPair); + const from0xHex = new SignatureTemplate(`0x${binToHex(alicePriv)}`); + const fromHex = new SignatureTemplate(binToHex(alicePriv)); + const fromWif = new SignatureTemplate(aliceWif); + const fromPriv = new SignatureTemplate(alicePriv); + expect(fromKeyPair.privateKey).toEqual(alicePriv); + expect(from0xHex.privateKey).toEqual(alicePriv); + expect(fromHex.privateKey).toEqual(alicePriv); + expect(fromWif.privateKey).toEqual(alicePriv); + expect(fromPriv.privateKey).toEqual(alicePriv); + }); + }); + + describe('generateSignature', () => { + it('should generate a correct signature using Schnorr', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79961')); + }); + + it('should generate a correct signature using ECDSA', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, undefined, SignatureAlgorithm.ECDSA); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('3045022100fa1d6a159a124e99479f78152422d55ff3c16f7fac5ae47fa291907f8f47613f02200d6c906f667b3712860b6f5a1f296ecb7dcd44da83c6a1eb45869b61c6b8dadb61')); + }); + + it('should append the correct hash type when fork ID is true', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), true); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79943')); + }); + + it('should append the correct hash type when fork ID is false', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), false); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79903')); + }); + }); + + describe('getPublicKey', () => { + it('should generate a correct public key', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + expect(signatureTemplate.getPublicKey()).toEqual(alicePub); + }); + }); + + describe('unlockP2PKH', () => { + it('should generate a correct unlocker', () => { + const utxo = { + txid: '043ec3826702c45460a6dd6b13e343a8f1bc06bc047b63ca484f791dfdfd92c2', + vout: 8, + satoshis: 109759n, + }; + + const signatureTemplate = new SignatureTemplate(alicePriv); + const unlocker = signatureTemplate.unlockP2PKH(); + + expect(unlocker.generateLockingBytecode()).toEqual(hexToBin('76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac')); + + const transactionBuilder = new TransactionBuilder({ provider: new MockNetworkProvider() }) + .addInput(utxo, unlocker) + .addOutput({ to: aliceAddress, amount: 1000n }); + + const transaction = transactionBuilder.buildLibauthTransaction(); + const sourceOutputs = generateLibauthSourceOutputs(transactionBuilder.inputs); + + expect(unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex: 0 })) + .toEqual(hexToBin('415cbd7f111be33daa9578ed7ace1b6721a5d14206302c628b1bfc27cfef92a334943504089731b7ce10173be7f22dc175f6d10c8c5a3d1b41a64db09555ebb00d61210373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088')); + }); + }); +}); diff --git a/packages/cashscript/test/fixture/vars.ts b/packages/cashscript/test/fixture/vars.ts index ffd77c38..b562f5bd 100644 --- a/packages/cashscript/test/fixture/vars.ts +++ b/packages/cashscript/test/fixture/vars.ts @@ -3,6 +3,7 @@ import { deriveHdPrivateNodeFromSeed, deriveSeedFromBip39Mnemonic, encodeCashAddress, + encodePrivateKeyWif, secp256k1, } from '@bitauth/libauth'; import { hash160 } from '@cashscript/utils'; @@ -23,18 +24,21 @@ if (typeof bobNode === 'string') throw new Error(); if (typeof carolNode === 'string') throw new Error(); export const alicePriv = aliceNode.privateKey; +export const aliceWif = encodePrivateKeyWif(alicePriv, 'testnet'); export const alicePub = secp256k1.derivePublicKeyCompressed(alicePriv) as Uint8Array; export const alicePkh = hash160(alicePub); export const aliceAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: alicePkh, throwErrors: true }).address; export const aliceTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: alicePkh, throwErrors: true }).address; export const bobPriv = bobNode.privateKey; +export const bobWif = encodePrivateKeyWif(bobPriv, 'testnet'); export const bobPub = secp256k1.derivePublicKeyCompressed(bobPriv) as Uint8Array; export const bobPkh = hash160(bobPub); export const bobAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: bobPkh, throwErrors: true }).address; export const bobTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: bobPkh, throwErrors: true }).address; export const carolPriv = carolNode.privateKey; +export const carolWif = encodePrivateKeyWif(carolPriv, 'testnet'); export const carolPub = secp256k1.derivePublicKeyCompressed(carolPriv) as Uint8Array; export const carolPkh = hash160(carolPub); export const carolAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: carolPkh, throwErrors: true }).address; From 5f03650746f185413139302478bfec7652e2b60c Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 30 Sep 2025 10:31:43 +0200 Subject: [PATCH 19/26] Add signMessageHash method to SignatureTemplate --- examples/PriceOracle.ts | 12 +++++------ packages/cashscript/src/SignatureTemplate.ts | 8 +++++-- .../cashscript/test/SignatureTemplate.test.ts | 16 ++++++++++++++ .../cashscript/test/fixture/PriceOracle.ts | 14 +++++-------- website/docs/releases/release-notes.md | 1 + website/docs/sdk/examples.md | 13 ++++++------ website/docs/sdk/signature-templates.md | 21 +++++++++++++++++-- 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/examples/PriceOracle.ts b/examples/PriceOracle.ts index eb174099..e0eb5664 100644 --- a/examples/PriceOracle.ts +++ b/examples/PriceOracle.ts @@ -1,8 +1,9 @@ -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm, SignatureTemplate } from 'cashscript'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -12,9 +13,8 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); - if (typeof signature === 'string') throw new Error(); - return signature; + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } diff --git a/packages/cashscript/src/SignatureTemplate.ts b/packages/cashscript/src/SignatureTemplate.ts index c3461853..804c37d7 100644 --- a/packages/cashscript/src/SignatureTemplate.ts +++ b/packages/cashscript/src/SignatureTemplate.ts @@ -31,13 +31,17 @@ export default class SignatureTemplate { } } - // TODO: Allow signing of non-transaction messages (i.e. don't add the hashtype) generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array { + const signature = this.signMessageHash(payload); + return Uint8Array.from([...signature, this.getHashType(bchForkId)]); + } + + signMessageHash(payload: Uint8Array): Uint8Array { const signature = this.signatureAlgorithm === SignatureAlgorithm.SCHNORR ? secp256k1.signMessageHashSchnorr(this.privateKey, payload) as Uint8Array : secp256k1.signMessageHashDER(this.privateKey, payload) as Uint8Array; - return Uint8Array.from([...signature, this.getHashType(bchForkId)]); + return signature; } getHashType(bchForkId: boolean = true): number { diff --git a/packages/cashscript/test/SignatureTemplate.test.ts b/packages/cashscript/test/SignatureTemplate.test.ts index 763903cd..31d4e2ec 100644 --- a/packages/cashscript/test/SignatureTemplate.test.ts +++ b/packages/cashscript/test/SignatureTemplate.test.ts @@ -46,6 +46,22 @@ describe('SignatureTemplate', () => { }); }); + describe('signMessageHash', () => { + it('should generate a correct signature using Schnorr', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + const signature = signatureTemplate.signMessageHash(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d799')); + }); + }); + + describe('signMessageHash', () => { + it('should generate a correct signature using ECDSA', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, undefined, SignatureAlgorithm.ECDSA); + const signature = signatureTemplate.signMessageHash(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('3045022100fa1d6a159a124e99479f78152422d55ff3c16f7fac5ae47fa291907f8f47613f02200d6c906f667b3712860b6f5a1f296ecb7dcd44da83c6a1eb45869b61c6b8dadb')); + }); + }); + describe('getPublicKey', () => { it('should generate a correct public key', () => { const signatureTemplate = new SignatureTemplate(alicePriv); diff --git a/packages/cashscript/test/fixture/PriceOracle.ts b/packages/cashscript/test/fixture/PriceOracle.ts index f8643ac4..82d354e1 100644 --- a/packages/cashscript/test/fixture/PriceOracle.ts +++ b/packages/cashscript/test/fixture/PriceOracle.ts @@ -1,9 +1,9 @@ -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; -import { SignatureAlgorithm } from '../../src/index.js'; +import { SignatureAlgorithm, SignatureTemplate } from '../../src/index.js'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -14,11 +14,7 @@ export class PriceOracle { } signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { - const signature = signatureAlgorithm === SignatureAlgorithm.SCHNORR ? - secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)) : - secp256k1.signMessageHashDER(this.privateKey, sha256(message)); - - if (typeof signature === 'string') throw new Error(); - return signature; + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 02fca242..6893ea21 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -11,6 +11,7 @@ title: Release Notes - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. +- :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages. - :hammer_and_wrench: Improve libauth template generation. - :bug: Fix bug where `SignatureTemplate` would not accept private key hex strings as a signer. diff --git a/website/docs/sdk/examples.md b/website/docs/sdk/examples.md index 25fd2beb..ffb13050 100644 --- a/website/docs/sdk/examples.md +++ b/website/docs/sdk/examples.md @@ -92,11 +92,12 @@ console.log('contract balance:', await contract.getBalance()); We need the create the functionality for generating and signing the oracle message to use in the HodlVault contract: ```ts title="PriceOracle.ts" -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm, SignatureTemplate } from 'cashscript'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -106,12 +107,12 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); - if (typeof signature === 'string') throw new Error(); - return signature; + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } + ``` ### Sending a Transaction diff --git a/website/docs/sdk/signature-templates.md b/website/docs/sdk/signature-templates.md index 88d906d5..9f988966 100644 --- a/website/docs/sdk/signature-templates.md +++ b/website/docs/sdk/signature-templates.md @@ -2,7 +2,7 @@ title: Signature Templates --- -When a contract function has a `sig` parameter, it needs a cryptographic signature from a private key for the spending transaction. +When a contract function has a `sig` parameter, it needs a cryptographic signature from a private key for the spending transaction. In place of a signature, a `SignatureTemplate` can be passed, which will generate the correct signature when the transaction is built. :::tip @@ -59,7 +59,7 @@ transactionBuilder.addInput(aliceUtxos[0], aliceTemplate.unlockP2PKH()); ### getPublicKey() -The `SignatureTemplate` also had a helper method to get the matching PublicKey in the following way: +The `SignatureTemplate` also has a helper method to get the matching PublicKey in the following way: ```ts signatureTemplate.getPublicKey(): Uint8Array @@ -72,6 +72,23 @@ import { aliceTemplate } from './somewhere.js'; const alicePublicKey = aliceTemplate.getPublicKey() ``` +### signMessageHash() + +The `SignatureTemplate` also has a helper method to sign a message hash, which can be used to sign non-transaction messages. This is useful for generating `datasig` signatures for smart contract use cases. + +```ts +signatureTemplate.signMessageHash(message: Uint8Array): Uint8Array +``` + +#### Example +```ts +import { aliceTemplate } from './somewhere.js'; +import { sha256 } from '@cashscript/utils'; +import { hexToBin } from '@bitauth/libauth'; + +const signature = aliceTemplate.signMessageHash(sha256(hexToBin('0000000000000000000000'))); +``` + ## Advanced Usage ### HashType From b3cb2736f75e45ce0f529c5448492f30682c236a Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 30 Sep 2025 10:52:47 +0200 Subject: [PATCH 20/26] Rework max fee options in TransactionBuilder & add max fee per byte --- packages/cashscript/src/TransactionBuilder.ts | 31 +++++----- .../test/TransactionBuilder.test.ts | 56 ++++++++++++++++--- website/docs/releases/release-notes.md | 2 + website/docs/sdk/transaction-builder.md | 31 +++++----- 4 files changed, 83 insertions(+), 37 deletions(-) diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 81d39874..94832b2e 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -38,6 +38,8 @@ import { WcTransactionObject } from './walletconnect-utils.js'; export interface TransactionBuilderOptions { provider: NetworkProvider; + maximumFeeSatoshis?: bigint; + maximumFeeSatsPerByte?: number; } const DEFAULT_SEQUENCE = 0xfffffffe; @@ -48,10 +50,9 @@ export class TransactionBuilder { public outputs: Output[] = []; public locktime: number = 0; - public maxFee?: bigint; constructor( - options: TransactionBuilderOptions, + private options: TransactionBuilderOptions, ) { this.provider = options.provider; } @@ -102,26 +103,26 @@ export class TransactionBuilder { return this; } - setMaxFee(maxFee: bigint): this { - this.maxFee = maxFee; - return this; - } - - private checkMaxFee(): void { - if (!this.maxFee) return; - + private checkMaxFee(transaction: LibauthTransaction): void { const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n); const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n); const fee = totalInputAmount - totalOutputAmount; - if (fee > this.maxFee) { - throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.maxFee}`); + if (this.options.maximumFeeSatoshis && fee > this.options.maximumFeeSatoshis) { + throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.options.maximumFeeSatoshis}`); + } + + if (this.options.maximumFeeSatsPerByte) { + const transactionSize = encodeTransaction(transaction).byteLength; + const feePerByte = Number(fee) / transactionSize; + + if (feePerByte > this.options.maximumFeeSatsPerByte) { + throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`); + } } } buildLibauthTransaction(): LibauthTransaction { - this.checkMaxFee(); - const inputs: LibauthTransaction['inputs'] = this.inputs.map((utxo) => ({ outpointIndex: utxo.vout, outpointTransactionHash: hexToBin(utxo.txid), @@ -149,6 +150,8 @@ export class TransactionBuilder { transaction.inputs[i].unlockingBytecode = script; }); + this.checkMaxFee(transaction); + return transaction; } diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index fc12e53d..4627b8f2 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -80,9 +80,9 @@ describe('Transaction Builder', () => { expect(txOutputs).toEqual(expect.arrayContaining(outputs)); }); - it('should fail when fee is higher than maxFee', async () => { + it('should fail when fee is higher than maximumFeeSatoshis', async () => { const fee = 2000n; - const maxFee = 1000n; + const maximumFeeSatoshis = 1000n; const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); const amount = p2pkhUtxos[0].satoshis - fee; @@ -93,17 +93,16 @@ describe('Transaction Builder', () => { } expect(() => { - new TransactionBuilder({ provider }) + new TransactionBuilder({ provider, maximumFeeSatoshis }) .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) .addOutput({ to: p2pkhInstance.address, amount }) - .setMaxFee(maxFee) .build(); - }).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maxFee}`); + }).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maximumFeeSatoshis}`); }); - it('should succeed when fee is lower than maxFee', async () => { + it('should succeed when fee is lower than maximumFeeSatoshis', async () => { const fee = 1000n; - const maxFee = 2000n; + const maximumFeeSatoshis = 2000n; const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); const amount = p2pkhUtxos[0].satoshis - fee; @@ -113,10 +112,49 @@ describe('Transaction Builder', () => { throw new Error('Not enough funds to send transaction'); } - const tx = new TransactionBuilder({ provider }) + const tx = new TransactionBuilder({ provider, maximumFeeSatoshis }) + .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) + .addOutput({ to: p2pkhInstance.address, amount }) + .build(); + + expect(tx).toBeDefined(); + }); + + it('should fail when fee per byte is higher than maximumFeeSatsPerByte', async () => { + const fee = 2000n; + const maximumFeeSatsPerByte = 1.0; + const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); + + const amount = p2pkhUtxos[0].satoshis - fee; + const dustAmount = calculateDust({ to: p2pkhInstance.address, amount }); + + if (amount < dustAmount) { + throw new Error('Not enough funds to send transaction'); + } + + expect(() => { + new TransactionBuilder({ provider, maximumFeeSatsPerByte }) + .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) + .addOutput({ to: p2pkhInstance.address, amount }) + .build(); + }).toThrow(`Transaction fee per byte of 9.05 is higher than max fee per byte of ${maximumFeeSatsPerByte}`); + }); + + it('should succeed when fee per byte is lower than maximumFeeSatsPerByte', async () => { + const fee = 1000n; + const maximumFeeSatsPerByte = 10.0; + const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); + + const amount = p2pkhUtxos[0].satoshis - fee; + const dustAmount = calculateDust({ to: p2pkhInstance.address, amount }); + + if (amount < dustAmount) { + throw new Error('Not enough funds to send transaction'); + } + + const tx = new TransactionBuilder({ provider, maximumFeeSatsPerByte }) .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) .addOutput({ to: p2pkhInstance.address, amount }) - .setMaxFee(maxFee) .build(); expect(tx).toBeDefined(); diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 6893ea21..c9391672 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -9,6 +9,8 @@ title: Release Notes - :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. +- :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with `TransactionBuilderOptions` on the constructor. +- :sparkles: Add `maximumFeeSatsPerByte` option to `TransactionBuilder` constructor. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. - :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages. diff --git a/website/docs/sdk/transaction-builder.md b/website/docs/sdk/transaction-builder.md index 009bdd84..e319b253 100644 --- a/website/docs/sdk/transaction-builder.md +++ b/website/docs/sdk/transaction-builder.md @@ -13,15 +13,16 @@ Defining the inputs and outputs requires careful consideration because the diffe new TransactionBuilder(options: TransactionBuilderOptions) ``` -To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance. +To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance and other options. ```ts interface TransactionBuilderOptions { provider: NetworkProvider; + maximumFeeSatoshis?: bigint; + maximumFeeSatsPerByte?: number; } ``` - #### Example ```ts import { ElectrumNetworkProvider, TransactionBuilder, Network } from 'cashscript'; @@ -30,6 +31,20 @@ const provider = new ElectrumNetworkProvider(Network.MAINNET); const transactionBuilder = new TransactionBuilder({ provider }); ``` +### Constructor Options + +#### provider + +The `provider` option is used to specify the network provider to use when sending the transaction. + +#### maximumFeeSatoshis + +The `maximumFeeSatoshis` option is used to specify the maximum fee for the transaction in satoshis. If this fee is exceeded, an error will be thrown when building the transaction. + +#### maximumFeeSatsPerByte + +The `maximumFeeSatsPerByte` option is used to specify the maximum fee per byte for the transaction. If this fee is exceeded, an error will be thrown when building the transaction. + ## Transaction Building ### addInput() @@ -159,18 +174,6 @@ Sets the locktime for the transaction to set a transaction-level absolute timelo transactionBuilder.setLocktime(((Date.now() / 1000) + 24 * 60 * 60) * 1000); ``` -### setMaxFee() -```ts -transactionBuilder.setMaxFee(maxFee: bigint): this -``` - -Sets a max fee for the transaction. Because the transaction builder does not automatically add a change output, you can set a max fee as a safety measure to make sure you don't accidentally pay too much in fees. If the transaction fee exceeds the max fee, an error will be thrown when building the transaction. - -#### Example -```ts -transactionBuilder.setMaxFee(1000n); -``` - ## Completing the Transaction ### send() ```ts From 10fbe817e5449e5ba4b7311c59e6da4fecdeb57c Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 30 Sep 2025 11:19:02 +0200 Subject: [PATCH 21/26] Update P2PKH-tokens tests to use new Transaction Builder --- packages/cashscript/package.json | 1 - .../cashscript/test/e2e/P2PKH-tokens.test.ts | 239 +++++++----------- 2 files changed, 85 insertions(+), 155 deletions(-) diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index c8722e06..55cdebc9 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -49,7 +49,6 @@ "@cashscript/utils": "^0.11.5", "@electrum-cash/network": "^4.1.3", "@mr-zwets/bchn-api-wrapper": "^1.0.1", - "fast-deep-equal": "^3.1.3", "pako": "^2.1.0", "semver": "^7.7.2" }, diff --git a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts index 2ab5f9ce..5405b92a 100644 --- a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts +++ b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts @@ -1,6 +1,8 @@ import { randomUtxo, randomToken, randomNFT } from '../../src/utils.js'; import { Contract, SignatureTemplate, ElectrumNetworkProvider, MockNetworkProvider, + TransactionBuilder, + NetworkProvider, } from '../../src/index.js'; import { alicePkh, @@ -14,9 +16,10 @@ import artifact from '../fixture/p2pkh.artifact.js'; // TODO: Replace this with unlockers describe('P2PKH-tokens', () => { let p2pkhInstance: Contract; + let provider: NetworkProvider; beforeAll(() => { - const provider = process.env.TESTS_USE_CHIPNET + provider = process.env.TESTS_USE_CHIPNET ? new ElectrumNetworkProvider(Network.CHIPNET) : new MockNetworkProvider(); @@ -61,15 +64,19 @@ describe('P2PKH-tokens', () => { throw new Error('No token UTXO found with fungible tokens'); } + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const to = p2pkhInstance.tokenAddress; const amount = 1000n; const { token } = tokenUtxo; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const fee = 1000n; + const changeAmount = fullBchBalance - fee - amount; + + const tx = await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addInput(tokenUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); @@ -87,24 +94,21 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; + const fee = 1000n; + const fullBchBalance = nftUtxo1.satoshis + nftUtxo2.satoshis + nonTokenUtxos.reduce( + (total, utxo) => total + utxo.satoshis, 0n, + ); + const changeAmount = fullBchBalance - fee - amount; - // We ran into a bug with the order of the properties, so we re-order the properties here to test that it works - const reorderedToken1 = { - nft: { - commitment: nftUtxo1.token!.nft!.commitment, - capability: nftUtxo1.token!.nft!.capability, - }, - category: nftUtxo1.token!.category, - amount: 0n, - }; + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo1) - .from(nftUtxo2) - .to(to, amount, reorderedToken1) - .to(to, amount, nftUtxo2.token) + const tx = await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(nftUtxo1, unlocker) + .addInput(nftUtxo2, unlocker) + .addOutput({ to, amount, token: nftUtxo1.token }) + .addOutput({ to, amount, token: nftUtxo2.token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); @@ -113,59 +117,23 @@ describe('P2PKH-tokens', () => { ); }); - it('can automatically select UTXOs for fungible tokens', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); - - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - + it('can create new token category (NFT and fungible token)', async () => { + const fee = 1000n; const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const { token } = tokenUtxo; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(to, amount, token) - .send(); - - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('adds automatic change output for fungible tokens', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); - const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - - const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const { token } = tokenUtxo; + // As a prerequisite to creating a new token category, we need a vout0 UTXO, so we create one here + const nonTokenUtxosBeforeGenesis = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo); + const preGenesisAmount = 10_000n; + const fullBchBalance = nonTokenUtxosBeforeGenesis.reduce((total, utxo) => total + utxo.satoshis, 0n); + const preGenesisChangeAmount = fullBchBalance - fee - preGenesisAmount; - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount) + await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxosBeforeGenesis, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount: preGenesisAmount }) + .addOutput({ to, amount: preGenesisChangeAmount }) .send(); - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('can create new token categories', async () => { - const to = p2pkhInstance.tokenAddress; - - // Send a transaction to be used as the genesis UTXO - await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(to, 10_000n) - .send(); + ////////////////////////////////////////////////////////////////////////////////////////////////// const contractUtxos = await p2pkhInstance.getUtxos(); const [genesisUtxo] = contractUtxos.filter((utxo) => utxo.vout === 0 && utxo.satoshis > 2000); @@ -175,6 +143,7 @@ describe('P2PKH-tokens', () => { } const amount = 1000n; + const changeAmount = genesisUtxo.satoshis - fee - amount; const token: TokenDetails = { amount: 1000n, category: genesisUtxo.txid, @@ -184,72 +153,16 @@ describe('P2PKH-tokens', () => { }, }; - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(genesisUtxo) - .to(to, amount, token) - .send(); - - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('adds automatic change output for NFTs', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const nftUtxo = contractUtxos.find(isNftUtxo); - const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - - if (!nftUtxo) { - throw new Error('No token UTXO found with an NFT'); - } - - const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const { token } = nftUtxo; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo) - .to(to, amount) - .send(); - - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('can disable automatic change output for fungible tokens', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); - const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - - const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) - .withoutTokenChange() + const tx = await new TransactionBuilder({ provider }) + .addInput(genesisUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - - // Check that the change output is not present - txOutputs.forEach((output) => { - expect(output.token?.amount).not.toEqual(1n); - }); }); - it.todo('can disable automatic change output for NFTs'); - it('should throw an error when trying to send more tokens than the contract has', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); @@ -262,15 +175,22 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount + 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/Insufficient funds for token/); + await expect(txPromise).rejects.toThrow( + /the sum of fungible tokens in the transaction outputs exceed that of the transaction inputs for a category/, + ); }); it('should throw an error when trying to send a token the contract doesn\'t have', async () => { @@ -285,15 +205,21 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; const token = { ...tokenUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; - - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/Insufficient funds for token/); + await expect(txPromise).rejects.toThrow( + /the transaction creates new fungible tokens for a category without a matching genesis input/, + ); }); it('should throw an error when trying to send an NFT the contract doesn\'t have', async () => { @@ -308,20 +234,25 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; const token = { ...nftUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; - - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo) - .to(to, amount, token) + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + nftUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(nftUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/NFT output with token category .* does not have corresponding input/); + await expect(txPromise).rejects.toThrow( + /the transaction creates an immutable token for a category without a matching minting token/, + ); }); - it.todo('can mint new NFTs if the NFT has minting capabilities'); - it.todo('can change the NFT commitment if the NFT has mutable capabilities'); - // TODO: Add more edge case tests for NFTs (minting, mutable, change outputs with multiple kinds of NFTs) + it.todo('cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)'); + it.todo('can burn fungible tokens when allowImplicitFungibleTokenBurn is true'); }); }); From 3229f5e46575098e5bacab6078bdd995bb0d1c64 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 30 Sep 2025 11:38:09 +0200 Subject: [PATCH 22/26] Add allowImplicitFungibleTokenBurn option to TransactionBuilder --- packages/cashscript/src/TransactionBuilder.ts | 37 ++++++++++- .../cashscript/test/e2e/P2PKH-tokens.test.ts | 65 ++++++++++++++++--- website/docs/releases/release-notes.md | 2 +- website/docs/sdk/transaction-builder.md | 4 ++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 94832b2e..92ad08c0 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -40,6 +40,7 @@ export interface TransactionBuilderOptions { provider: NetworkProvider; maximumFeeSatoshis?: bigint; maximumFeeSatsPerByte?: number; + allowImplicitFungibleTokenBurn?: boolean; } const DEFAULT_SEQUENCE = 0xfffffffe; @@ -50,11 +51,16 @@ export class TransactionBuilder { public outputs: Output[] = []; public locktime: number = 0; + public options: TransactionBuilderOptions; constructor( - private options: TransactionBuilderOptions, + options: TransactionBuilderOptions, ) { this.provider = options.provider; + this.options = { + allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false, + ...options, + }; } addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this { @@ -114,7 +120,7 @@ export class TransactionBuilder { if (this.options.maximumFeeSatsPerByte) { const transactionSize = encodeTransaction(transaction).byteLength; - const feePerByte = Number(fee) / transactionSize; + const feePerByte = Number((Number(fee) / transactionSize).toFixed(2)); if (feePerByte > this.options.maximumFeeSatsPerByte) { throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`); @@ -122,7 +128,34 @@ export class TransactionBuilder { } } + private checkFungibleTokenBurn(): void { + if (this.options.allowImplicitFungibleTokenBurn) return; + + const tokenInputAmounts: Record = {}; + const tokenOutputAmounts: Record = {}; + + for (const input of this.inputs) { + if (input.token?.amount) { + tokenInputAmounts[input.token.category] = (tokenInputAmounts[input.token.category] || 0n) + input.token.amount; + } + } + for (const output of this.outputs) { + if (output.token?.amount) { + tokenOutputAmounts[output.token.category] = (tokenOutputAmounts[output.token.category] || 0n) + output.token.amount; + } + } + + for (const [category, inputAmount] of Object.entries(tokenInputAmounts)) { + const outputAmount = tokenOutputAmounts[category] || 0n; + if (outputAmount < inputAmount) { + throw new Error(`Implicit burning of fungible tokens for category ${category} is not allowed (input amount: ${inputAmount}, output amount: ${outputAmount}). If this is intended, set allowImplicitFungibleTokenBurn to true.`); + } + } + } + buildLibauthTransaction(): LibauthTransaction { + this.checkFungibleTokenBurn(); + const inputs: LibauthTransaction['inputs'] = this.inputs.map((utxo) => ({ outpointIndex: utxo.vout, outpointTransactionHash: hexToBin(utxo.txid), diff --git a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts index 5405b92a..e6e7e12e 100644 --- a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts +++ b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts @@ -195,24 +195,18 @@ describe('P2PKH-tokens', () => { it('should throw an error when trying to send a token the contract doesn\'t have', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const token = { ...tokenUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; + const token = { category: '0000000000000000000000000000000000000000000000000000000000000000', amount: 100n }; const fee = 1000n; - const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n); const changeAmount = fullBchBalance - fee - amount; const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); const txPromise = new TransactionBuilder({ provider }) .addInputs(nonTokenUtxos, unlocker) - .addInput(tokenUtxo, unlocker) .addOutput({ to, amount, token }) .addOutput({ to, amount: changeAmount }) .send(); @@ -251,8 +245,59 @@ describe('P2PKH-tokens', () => { ); }); - it.todo('cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)'); - it.todo('can burn fungible tokens when allowImplicitFungibleTokenBurn is true'); + it('cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)', async () => { + const contractUtxos = await p2pkhInstance.getUtxos(); + const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); + const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); + + if (!tokenUtxo) { + throw new Error('No token UTXO found with fungible tokens'); + } + + const to = p2pkhInstance.tokenAddress; + const amount = 1000n; + const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) + .send(); + + await expect(txPromise).rejects.toThrow('Implicit burning of fungible tokens for category'); + }); + + it('can burn fungible tokens when allowImplicitFungibleTokenBurn is true', async () => { + const contractUtxos = await p2pkhInstance.getUtxos(); + const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); + const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); + + if (!tokenUtxo) { + throw new Error('No token UTXO found with fungible tokens'); + } + + const to = p2pkhInstance.tokenAddress; + const amount = 1000n; + const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) + .send(); + + await expect(txPromise).resolves.toBeDefined(); + }); }); }); diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index c9391672..fd1b4e33 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -10,7 +10,7 @@ title: Release Notes - :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. - :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with `TransactionBuilderOptions` on the constructor. -- :sparkles: Add `maximumFeeSatsPerByte` option to `TransactionBuilder` constructor. +- :sparkles: Add `maximumFeeSatsPerByte` and `allowImplicitFungibleTokenBurn` options to `TransactionBuilder` constructor. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. - :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages. diff --git a/website/docs/sdk/transaction-builder.md b/website/docs/sdk/transaction-builder.md index e319b253..42a5027d 100644 --- a/website/docs/sdk/transaction-builder.md +++ b/website/docs/sdk/transaction-builder.md @@ -45,6 +45,10 @@ The `maximumFeeSatoshis` option is used to specify the maximum fee for the trans The `maximumFeeSatsPerByte` option is used to specify the maximum fee per byte for the transaction. If this fee is exceeded, an error will be thrown when building the transaction. +#### allowImplicitFungibleTokenBurn + +The `allowImplicitFungibleTokenBurn` option is used to specify whether implicit burning of fungible tokens is allowed (default: `false`). If this is set to `true`, the transaction builder will not throw an error when burning fungible tokens. + ## Transaction Building ### addInput() From ab75e5e56ee57d87cfed46af9be6ad0e4269f534 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 30 Sep 2025 11:56:20 +0200 Subject: [PATCH 23/26] Update migration notes for v0.12 --- website/docs/releases/migration-notes.md | 47 ++++++++++++++++++++++++ website/docs/releases/release-notes.md | 12 +++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/website/docs/releases/migration-notes.md b/website/docs/releases/migration-notes.md index f7644f0b..96337cc9 100644 --- a/website/docs/releases/migration-notes.md +++ b/website/docs/releases/migration-notes.md @@ -2,6 +2,53 @@ title: Migration Notes --- +## v0.11 to v0.12 + +There are several breaking changes to the SDK in this release. + +### CashScript SDK + +#### Old Transaction Builder Removal + +The most impactful breaking change is the removal of the deprecated 'Simple Transaction Builder'. See [below for steps to migrate to the new transaction builder](/docs/releases/migration-notes#sdk-transaction-builder). + +#### Contract constructor +Before, the `provider` option was optional in the `Contract` constructor. This is no longer the case. + +```ts +// Before: defaults to mainnet ElectrumNetworkProvider +const contract = new Contract(artifact, constructorArgs); + +// After: explicitly specify the provider +const provider = new ElectrumNetworkProvider('mainnet'); +const contract = new Contract(artifact, constructorArgs, { provider }); +``` + +#### Transaction Builder Max Fee +Before, the `setMaxFee()` method was used to set the maximum fee for the transaction. This was replaced with the `maximumFeeSatoshis` option in the constructor. Additionally, the `maximumFeeSatsPerByte` option was added. + +```ts +// Before: setMaxFee() was used to set the maximum fee +const builder = new TransactionBuilder({ provider }).setMaxFee(1000n); + +// After: maximumFeeSatoshis option was added to the constructor +const builder = new TransactionBuilder({ provider, maximumFeeSatoshis: 1000n }); +``` + +#### MockNetworkProvider + +Before, the `updateUtxoSet` option was `false` by default for the `MockNetworkProvider`. This is now `true` by default to better match real-world network behaviour. + +```ts +// Before: updateUtxoSet is false by default +const provider = new MockNetworkProvider(); + +// After: updateUtxoSet is true by default, if you want to keep the old behaviour, set it to false +const provider = new MockNetworkProvider({ updateUtxoSet: false }); +``` + +Earlier, the `MockNetworkProvider` also automatically added some test UTXOs to the provider, which is no longer the case. Make sure to add any UTXOs you need manually. + ## v0.10 to v0.11 There are several breaking changes to the compiler and SDK in this release. They are listed below in their own sections. diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index fd1b4e33..049059a2 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -4,16 +4,16 @@ title: Release Notes ## v0.12.0 -#### CashScript SDK -- :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. -- :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. -- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). -- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. -- :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with `TransactionBuilderOptions` on the constructor. +#### CashScript SDK`TransactionBuilderOptions` on the constructor. - :sparkles: Add `maximumFeeSatsPerByte` and `allowImplicitFungibleTokenBurn` options to `TransactionBuilder` constructor. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. - :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages. +- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). +- :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. +- :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. +- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. +- :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with - :hammer_and_wrench: Improve libauth template generation. - :bug: Fix bug where `SignatureTemplate` would not accept private key hex strings as a signer. From 175954c4921dbc975aa74e8f0bbdea719f43e1e6 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 2 Oct 2025 10:59:51 +0200 Subject: [PATCH 24/26] Refactor LibauthTemplate - Extract some utils into their own file - Avoid deep object reassignment in favour of function calls and direct assignment - Remove unneccesary TransactionType type --- packages/cashscript/src/TransactionBuilder.ts | 4 +- packages/cashscript/src/debugging.ts | 2 +- .../{ => libauth-template}/LibauthTemplate.ts | 476 +++++++----------- .../cashscript/src/libauth-template/utils.ts | 114 +++++ 4 files changed, 291 insertions(+), 305 deletions(-) rename packages/cashscript/src/{ => libauth-template}/LibauthTemplate.ts (57%) create mode 100644 packages/cashscript/src/libauth-template/utils.ts diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 92ad08c0..57d67c7a 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -31,7 +31,7 @@ import { } from './utils.js'; import { FailedTransactionError } from './Errors.js'; import { DebugResults } from './debugging.js'; -import { debugLibauthTemplate, getLibauthTemplates, getBitauthUri } from './LibauthTemplate.js'; +import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libauth-template/LibauthTemplate.js'; import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js'; import semver from 'semver'; import { WcTransactionObject } from './walletconnect-utils.js'; @@ -254,7 +254,7 @@ export class TransactionBuilder { } getLibauthTemplate(): WalletTemplate { - return getLibauthTemplates(this); + return getLibauthTemplate(this); } async send(): Promise; diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index c138e819..e12a0442 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -2,7 +2,7 @@ import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationPro import { Artifact, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils'; import { findLastIndex, toRegExp } from './utils.js'; import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js'; -import { getBitauthUri } from './LibauthTemplate.js'; +import { getBitauthUri } from './libauth-template/LibauthTemplate.js'; import { VmTarget } from './interfaces.js'; export type DebugResult = AuthenticationProgramStateCommon[]; diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/libauth-template/LibauthTemplate.ts similarity index 57% rename from packages/cashscript/src/LibauthTemplate.ts rename to packages/cashscript/src/libauth-template/LibauthTemplate.ts index 979e7357..225f03ab 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/libauth-template/LibauthTemplate.ts @@ -1,20 +1,13 @@ import { binToBase64, binToHex, - decodeCashAddress, - hexToBin, Input, - isHex, TransactionBch, utf8ToBin, WalletTemplate, - WalletTemplateEntity, WalletTemplateScenario, WalletTemplateScenarioBytecode, - WalletTemplateScenarioInput, WalletTemplateScenarioOutput, - WalletTemplateScenarioTransactionOutput, - WalletTemplateScript, WalletTemplateScriptLocking, WalletTemplateScriptUnlocking, WalletTemplateVariable, @@ -23,193 +16,118 @@ import { AbiFunction, AbiInput, Artifact, - bytecodeToScript, - formatBitAuthScript, } from '@cashscript/utils'; -import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from './Argument.js'; -import { Contract } from './Contract.js'; -import { DebugResults, debugTemplate } from './debugging.js'; +import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from '../Argument.js'; +import { Contract } from '../Contract.js'; +import { DebugResults, debugTemplate } from '../debugging.js'; import { - HashType, isContractUnlocker, isP2PKHUnlocker, isStandardUnlockableUtxo, isUnlockableUtxo, - LibauthTokenDetails, Output, - SignatureAlgorithm, StandardUnlockableUtxo, - TokenDetails, - UnlockableUtxo, Utxo, VmTarget, -} from './interfaces.js'; -import SignatureTemplate from './SignatureTemplate.js'; -import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from './utils.js'; -import { TransactionBuilder } from './TransactionBuilder.js'; +} from '../interfaces.js'; +import SignatureTemplate from '../SignatureTemplate.js'; +import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from '../utils.js'; +import { TransactionBuilder } from '../TransactionBuilder.js'; import { deflate } from 'pako'; -import MockNetworkProvider from './network/MockNetworkProvider.js'; +import MockNetworkProvider from '../network/MockNetworkProvider.js'; +import { addHexPrefixExceptEmpty, formatBytecodeForDebugging, formatParametersForDebugging, getLockScriptName, getUnlockScriptName, lockingBytecodeIsSetToSlot, serialiseTokenDetails } from './utils.js'; // TODO: Add / improve descriptions throughout the template generation -export const getLibauthTemplates = ( - txn: TransactionBuilder, +export const getLibauthTemplate = ( + transactionBuilder: TransactionBuilder, ): WalletTemplate => { - if (txn.inputs.some((input) => !isStandardUnlockableUtxo(input))) { + if (transactionBuilder.inputs.some((input) => !isStandardUnlockableUtxo(input))) { throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); } - const libauthTransaction = txn.buildLibauthTransaction(); - const csTransaction = createTransactionTypeFromTransactionBuilder(txn); + const libauthTransaction = transactionBuilder.buildLibauthTransaction(); - const vmTarget = txn.provider instanceof MockNetworkProvider ? txn.provider.vmTarget : VmTarget.BCH_2025_05; + const vmTarget = transactionBuilder.provider instanceof MockNetworkProvider + ? transactionBuilder.provider.vmTarget + : VmTarget.BCH_2025_05; - const baseTemplate: WalletTemplate = { + const template: WalletTemplate = { $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', description: 'Imported from cashscript', name: 'CashScript Generated Debugging Template', supported: [vmTarget], version: 0, - entities: {}, - scripts: {}, - scenarios: {}, + entities: generateAllTemplateEntities(transactionBuilder), + scripts: generateAllTemplateScripts(transactionBuilder), + scenarios: generateAllTemplateScenarios(libauthTransaction, transactionBuilder), }; - // Initialize collections for entities, scripts, and scenarios - const entities: Record = {}; - const scripts: Record = {}; - const scenarios: Record = {}; - - // Initialize collections for P2PKH entities and scripts - const p2pkhEntities: Record = {}; - const p2pkhScripts: Record = {}; + // TODO: Refactor the below code to not have deep reassignment of scenario.sourceOutputs and scenario.transaction.outputs // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts const unlockingBytecodeToLockingBytecodeParams: Record = {}; const lockingBytecodeToLockingBytecodeParams: Record = {}; // We can typecast this because we check that all inputs are standard unlockable at the top of this function - for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) { - if (isP2PKHUnlocker(input.unlocker)) { - Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex)); - Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(inputIndex)); - Object.assign(scenarios, generateTemplateScenariosP2PKH(libauthTransaction, csTransaction, inputIndex)); - continue; - } - + for (const [inputIndex, input] of (transactionBuilder.inputs as StandardUnlockableUtxo[]).entries()) { if (isContractUnlocker(input.unlocker)) { - const contract = input.unlocker?.contract; - const abiFunction = input.unlocker?.abiFunction; - - if (!abiFunction) { - throw new Error('No ABI function found in unlocker'); - } - - // Encode the function arguments for this contract input - const encodedArgs = encodeFunctionArguments( - abiFunction, - input.unlocker.params ?? [], - ); + const lockScriptName = getLockScriptName(input.unlocker.contract); + if (!lockScriptName) continue; - // Generate a scenario object for this contract input - Object.assign(scenarios, - generateTemplateScenarios( - contract, - libauthTransaction, - csTransaction, - abiFunction, - encodedArgs, - inputIndex, - ), - ); - - // Generate entities for this contract input - const entity = generateTemplateEntitiesP2SH( - contract, - abiFunction, - encodedArgs, - inputIndex, - ); + const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); - // Generate scripts for this contract input - const script = generateTemplateScriptsP2SH( - contract, - abiFunction, - encodedArgs, - contract.encodedConstructorArgs, - inputIndex, - ); + const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); + unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; - // Find the lock script name for this contract input - const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock')); - if (lockScriptName) { - // Generate bytecodes for this contract input - const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); - const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); - - // Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios - unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; - // Assign a name to the locking bytecode so later it can be used to replace with bytecode/slot in scenarios - lockingBytecodeToLockingBytecodeParams[binToHex(addressToLockScript(contract.address))] = lockingScriptParams; - } - - // Add entities and scripts to the base template and repeat the process for the next input - Object.assign(entities, entity); - Object.assign(scripts, script); + const lockingBytecode = binToHex(addressToLockScript(input.unlocker.contract.address)); + lockingBytecodeToLockingBytecodeParams[lockingBytecode] = lockingScriptParams; } } - Object.assign(entities, p2pkhEntities); - Object.assign(scripts, p2pkhScripts); - - const finalTemplate = { ...baseTemplate, entities, scripts, scenarios }; - - // Loop through all scenarios and map the locking and unlocking scripts to the scenarios - // Replace the script tag with the identifiers we created earlier - - // For Inputs - for (const scenario of Object.values(scenarios)) { + for (const scenario of Object.values(template.scenarios!)) { + // For Inputs for (const [idx, input] of libauthTransaction.inputs.entries()) { const unlockingBytecode = binToHex(input.unlockingBytecode); + const lockingBytecodeParams = unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]; + + // If lockingBytecodeParams is unknown, then it stays at default: {} + if (!lockingBytecodeParams) continue; - // If false then it stays lockingBytecode: {} - if (unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]) { - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; - - // If true then assign a name to the locking bytecode script. - if (scenario.sourceOutputs && scenario.sourceOutputs[idx]) { - scenario.sourceOutputs[idx] = { - ...scenario.sourceOutputs[idx], - lockingBytecode: unlockingBytecodeToLockingBytecodeParams[unlockingBytecode], - }; - } + // If locking bytecode is set to ['slot'] then this is being evaluated by the scenario, so we don't replace bytecode + if (lockingBytecodeIsSetToSlot(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; + + // If lockingBytecodeParams is known, and this input is not ['slot'] then assign a locking bytecode as source output + if (scenario.sourceOutputs?.[idx]) { + scenario.sourceOutputs[idx] = { + ...scenario.sourceOutputs[idx], + lockingBytecode: lockingBytecodeParams, + }; } } // For Outputs for (const [idx, output] of libauthTransaction.outputs.entries()) { const lockingBytecode = binToHex(output.lockingBytecode); + const lockingBytecodeParams = lockingBytecodeToLockingBytecodeParams[lockingBytecode]; - // If false then it stays lockingBytecode: {} - if (lockingBytecodeToLockingBytecodeParams[lockingBytecode]) { + // If lockingBytecodeParams is unknown, then it stays at default: {} + if (!lockingBytecodeParams) continue; - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; + // If locking bytecode is set to ['slot'] then this is being evaluated by the scenario, so we don't replace bytecode + if (lockingBytecodeIsSetToSlot(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; - // If true then assign a name to the locking bytecode script. - if (scenario?.transaction && scenario?.transaction?.outputs && scenario?.transaction?.outputs[idx]) { - scenario.transaction.outputs[idx] = { - ...scenario.transaction.outputs[idx], - lockingBytecode: lockingBytecodeToLockingBytecodeParams[lockingBytecode], - }; - } + // If lockingBytecodeParams is known, and this input is not ['slot'] then assign a locking bytecode as source output + if (scenario?.transaction?.outputs?.[idx]) { + scenario.transaction.outputs[idx] = { + ...scenario.transaction.outputs[idx], + lockingBytecode: lockingBytecodeParams, + }; } } - } - return finalTemplate; + return template; }; export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { @@ -227,6 +145,77 @@ export const getBitauthUri = (template: WalletTemplate): string => { return `https://ide.bitauth.com/import-template/${payload}`; }; +const generateAllTemplateEntities = ( + transactionBuilder: TransactionBuilder, +): WalletTemplate['entities'] => { + const entities = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateEntitiesP2PKH(inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateEntitiesP2SH(input.unlocker.contract, input.unlocker.abiFunction, encodedArgs, inputIndex); + } + + throw new Error('Unknown unlocker type'); + }); + + return entities.reduce((acc, entity) => ({ ...acc, ...entity }), {}); +}; + +const generateAllTemplateScripts = ( + transactionBuilder: TransactionBuilder, +): WalletTemplate['scripts'] => { + const scripts = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateScriptsP2PKH(inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateScriptsP2SH( + input.unlocker.contract, + input.unlocker.abiFunction, + encodedArgs, + input.unlocker.contract.encodedConstructorArgs, + inputIndex, + ); + } + + throw new Error('Unknown unlocker type'); + }); + + return scripts.reduce((acc, script) => ({ ...acc, ...script }), {}); +}; + +const generateAllTemplateScenarios = ( + libauthTransaction: TransactionBch, + transactionBuilder: TransactionBuilder, +): WalletTemplate['scenarios'] => { + const scenarios = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateScenariosP2PKH(libauthTransaction, transactionBuilder, inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateScenarios( + input.unlocker.contract, + libauthTransaction, + transactionBuilder, + input.unlocker.abiFunction, + encodedArgs, + inputIndex, + ); + } + + throw new Error('Unknown unlocker type'); + }); + + return scenarios.reduce((acc, scenario) => ({ ...acc, ...scenario }), {}); +}; + const generateTemplateEntitiesP2PKH = ( inputIndex: number, ): WalletTemplate['entities'] => { @@ -268,19 +257,13 @@ const generateTemplateEntitiesP2SH = ( getLockScriptName(contract), getUnlockScriptName(contract, abiFunction, inputIndex), ], - variables: createWalletTemplateVariables(contract.artifact, abiFunction, encodedFunctionArgs), + variables: { + ...createWalletTemplateVariables(contract.artifact, abiFunction, encodedFunctionArgs), + ...generateFunctionIndexTemplateVariable(contract.artifact.abi), + }, }, }; - // function_index is a special variable that indicates the function to execute - if (contract.artifact.abi.length > 1) { - entities[contract.artifact.contractName + '_input' + inputIndex + '_parameters'].variables.function_index = { - description: 'Script function index to execute', - name: 'function_index', - type: 'WalletData', - }; - } - return entities; }; @@ -317,27 +300,23 @@ const createWalletTemplateVariables = ( const generateTemplateScriptsP2PKH = ( inputIndex: number, ): WalletTemplate['scripts'] => { - const scripts: WalletTemplate['scripts'] = {}; const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; - const signatureString = `signature_${inputIndex}`; - const publicKeyString = `public_key_${inputIndex}`; - - // add extra unlocking and locking script for P2PKH inputs spent alongside our contract - // this is needed for correct cross-references in the template - scripts[unlockScriptName] = { - passes: [`P2PKH_spend_input${inputIndex}_evaluate`], - name: `P2PKH Unlock (input #${inputIndex})`, - script: - `<${signatureString}>\n<${publicKeyString}>`, - unlocks: lockScriptName, - }; - scripts[lockScriptName] = { - lockingType: 'standard', - name: `P2PKH Lock (input #${inputIndex})`, - script: - `OP_DUP\nOP_HASH160 <$(<${publicKeyString}> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, + const scripts = { + [unlockScriptName]: { + passes: [`P2PKH_spend_input${inputIndex}_evaluate`], + name: `P2PKH Unlock (input #${inputIndex})`, + script: + `\n`, + unlocks: lockScriptName, + }, + [lockScriptName]: { + lockingType: 'standard', + name: `P2PKH Lock (input #${inputIndex})`, + script: + `OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, + }, }; return scripts; @@ -350,7 +329,6 @@ const generateTemplateScriptsP2SH = ( encodedConstructorArgs: EncodedConstructorArgument[], inputIndex: number, ): WalletTemplate['scripts'] => { - // definition of locking scripts and unlocking scripts with their respective bytecode const unlockingScriptName = getUnlockScriptName(contract, abiFunction, inputIndex); const lockingScriptName = getLockScriptName(contract); @@ -407,7 +385,7 @@ const generateTemplateUnlockScript = ( const generateTemplateScenarios = ( contract: Contract, libauthTransaction: TransactionBch, - csTransaction: TransactionType, + transactionBuilder: TransactionBuilder, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], inputIndex: number, @@ -432,8 +410,8 @@ const generateTemplateScenarios = ( privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, }, - transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, csTransaction, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex), + transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, transactionBuilder, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), }, }; @@ -442,7 +420,7 @@ const generateTemplateScenarios = ( const generateTemplateScenariosP2PKH = ( libauthTransaction: TransactionBch, - csTransaction: TransactionType, + transactionBuilder: TransactionBuilder, inputIndex: number, ): WalletTemplate['scenarios'] => { const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`; @@ -460,8 +438,8 @@ const generateTemplateScenariosP2PKH = ( [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, }, }, - transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, csTransaction, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex), + transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, transactionBuilder, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), }, }; @@ -471,36 +449,36 @@ const generateTemplateScenariosP2PKH = ( const generateTemplateScenarioTransaction = ( contract: Contract | undefined, libauthTransaction: TransactionBch, - csTransaction: TransactionType, + transactionBuilder: TransactionBuilder, slotIndex: number, ): WalletTemplateScenario['transaction'] => { - const zippedInputs = zip(csTransaction.inputs, libauthTransaction.inputs); + const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); const inputs = zippedInputs.map(([csInput, libauthInput], inputIndex) => { return { outpointIndex: libauthInput.outpointIndex, outpointTransactionHash: binToHex(libauthInput.outpointTransactionHash), sequenceNumber: libauthInput.sequenceNumber, unlockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), - } as WalletTemplateScenarioInput; + }; }); const locktime = libauthTransaction.locktime; - const zippedOutputs = zip(csTransaction.outputs, libauthTransaction.outputs); + const zippedOutputs = zip(transactionBuilder.outputs, libauthTransaction.outputs); const outputs = zippedOutputs.map(([csOutput, libauthOutput]) => { if (csOutput && contract) { return { lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), token: serialiseTokenDetails(libauthOutput.token), valueSatoshis: Number(libauthOutput.valueSatoshis), - } as WalletTemplateScenarioTransactionOutput; + }; } return { lockingBytecode: `${binToHex(libauthOutput.lockingBytecode)}`, token: serialiseTokenDetails(libauthOutput.token), valueSatoshis: Number(libauthOutput.valueSatoshis), - } as WalletTemplateScenarioTransactionOutput; + }; }); const version = libauthTransaction.version; @@ -509,11 +487,11 @@ const generateTemplateScenarioTransaction = ( }; const generateTemplateScenarioSourceOutputs = ( - csTransaction: TransactionType, + transactionBuilder: TransactionBuilder, libauthTransaction: TransactionBch, slotIndex: number, ): Array> => { - const zippedInputs = zip(csTransaction.inputs, libauthTransaction.inputs); + const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); return zippedInputs.map(([csInput, libauthInput], inputIndex) => { return { lockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), @@ -523,24 +501,6 @@ const generateTemplateScenarioSourceOutputs = ( }); }; -const createTransactionTypeFromTransactionBuilder = (txn: TransactionBuilder): TransactionType => { - const csTransaction = { - inputs: txn.inputs, - locktime: txn.locktime, - outputs: txn.outputs, - version: 2, - }; - - return csTransaction; -}; - -interface TransactionType { - inputs: UnlockableUtxo[]; - locktime: number; - outputs: Output[]; - version: number; -} - const generateLockingScriptParams = ( contract: Contract, { unlocker }: StandardUnlockableUtxo, @@ -610,101 +570,6 @@ const generateUnlockingScriptParams = ( }; }; -const getLockScriptName = (contract: Contract): string => { - const result = decodeCashAddress(contract.address); - if (typeof result === 'string') throw new Error(result); - - return `${contract.artifact.contractName}_${binToHex(result.payload)}_lock`; -}; - -const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, inputIndex: number): string => { - return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; -}; - - -const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { - const signatureAlgorithmNames = { - [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', - [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', - }; - - return signatureAlgorithmNames[signatureAlgorithm]; -}; - -const getHashTypeName = (hashType: HashType): string => { - const hashtypeNames = { - [HashType.SIGHASH_ALL]: 'all_outputs', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', - [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', - [HashType.SIGHASH_SINGLE]: 'corresponding_output', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', - [HashType.SIGHASH_NONE]: 'no_outputs', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', - [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', - }; - - return hashtypeNames[hashType]; -}; - -const addHexPrefixExceptEmpty = (value: string): string => { - return value.length > 0 ? `0x${value}` : ''; -}; - -const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { - if (types.length === 0) return '// none'; - - // We reverse the arguments because the order of the arguments in the bytecode is reversed - const typesAndArguments = zip(types, args).reverse(); - - return typesAndArguments.map(([input, arg]) => { - if (arg instanceof SignatureTemplate) { - const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(arg.getHashType(false)); - return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; - } - - const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; - - // we output these values as pushdata, comment will contain the type and the value of the variable - // e.g. // int = <0xa08601> - return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; - }).join('\n'); -}; - -const formatBytecodeForDebugging = (artifact: Artifact): string => { - if (!artifact.debug) { - return artifact.bytecode - .split(' ') - .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) - .join('\n'); - } - - return formatBitAuthScript( - bytecodeToScript(hexToBin(artifact.debug.bytecode)), - artifact.debug.sourceMap, - artifact.source, - ); -}; - -const serialiseTokenDetails = ( - token?: TokenDetails | LibauthTokenDetails, -): LibauthTemplateTokenDetails | undefined => { - if (!token) return undefined; - - return { - amount: token.amount.toString(), - category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, - nft: token.nft ? { - capability: token.nft.capability, - commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, - } : undefined, - }; -}; - const generateTemplateScenarioParametersValues = ( types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[], @@ -775,11 +640,18 @@ const generateTemplateScenarioParametersFunctionIndex = ( return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; }; -interface LibauthTemplateTokenDetails { - amount: string; - category: string; - nft?: { - capability: 'none' | 'mutable' | 'minting'; - commitment: string; - }; -} +const generateFunctionIndexTemplateVariable = ( + abi: readonly AbiFunction[], +): Record => { + if (abi.length > 1) { + return { + function_index: { + description: 'Script function index to execute', + name: 'function_index', + type: 'WalletData', + }, + }; + } + + return {}; +}; diff --git a/packages/cashscript/src/libauth-template/utils.ts b/packages/cashscript/src/libauth-template/utils.ts new file mode 100644 index 00000000..9e2f4169 --- /dev/null +++ b/packages/cashscript/src/libauth-template/utils.ts @@ -0,0 +1,114 @@ +import { AbiFunction, AbiInput, Artifact, bytecodeToScript, formatBitAuthScript } from '@cashscript/utils'; +import { HashType, LibauthTokenDetails, SignatureAlgorithm, TokenDetails } from '../interfaces.js'; +import { hexToBin, binToHex, isHex, decodeCashAddress, type WalletTemplateScenarioBytecode } from '@bitauth/libauth'; +import { EncodedFunctionArgument } from '../Argument.js'; +import { zip } from '../utils.js'; +import SignatureTemplate from '../SignatureTemplate.js'; +import { Contract } from '../Contract.js'; + +export const getLockScriptName = (contract: Contract): string => { + const result = decodeCashAddress(contract.address); + if (typeof result === 'string') throw new Error(result); + + return `${contract.artifact.contractName}_${binToHex(result.payload)}_lock`; +}; + +export const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, inputIndex: number): string => { + return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; +}; + +export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { + const signatureAlgorithmNames = { + [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', + [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', + }; + + return signatureAlgorithmNames[signatureAlgorithm]; +}; + +export const getHashTypeName = (hashType: HashType): string => { + const hashtypeNames = { + [HashType.SIGHASH_ALL]: 'all_outputs', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', + [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', + [HashType.SIGHASH_SINGLE]: 'corresponding_output', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', + [HashType.SIGHASH_NONE]: 'no_outputs', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', + [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', + }; + + return hashtypeNames[hashType]; +}; + +export const addHexPrefixExceptEmpty = (value: string): string => { + return value.length > 0 ? `0x${value}` : ''; +}; + +export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { + if (types.length === 0) return '// none'; + + // We reverse the arguments because the order of the arguments in the bytecode is reversed + const typesAndArguments = zip(types, args).reverse(); + + return typesAndArguments.map(([input, arg]) => { + if (arg instanceof SignatureTemplate) { + const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); + const hashtypeName = getHashTypeName(arg.getHashType(false)); + return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; + } + + const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; + + // we output these values as pushdata, comment will contain the type and the value of the variable + // e.g. // int = <0xa08601> + return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; + }).join('\n'); +}; + +export const formatBytecodeForDebugging = (artifact: Artifact): string => { + if (!artifact.debug) { + return artifact.bytecode + .split(' ') + .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) + .join('\n'); + } + + return formatBitAuthScript( + bytecodeToScript(hexToBin(artifact.debug.bytecode)), + artifact.debug.sourceMap, + artifact.source, + ); +}; + +export const serialiseTokenDetails = ( + token?: TokenDetails | LibauthTokenDetails, +): LibauthTemplateTokenDetails | undefined => { + if (!token) return undefined; + + return { + amount: token.amount.toString(), + category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, + nft: token.nft ? { + capability: token.nft.capability, + commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, + } : undefined, + }; +}; + +interface LibauthTemplateTokenDetails { + amount: string; + category: string; + nft?: { + capability: 'none' | 'mutable' | 'minting'; + commitment: string; + }; +} + +export const lockingBytecodeIsSetToSlot = (lockingBytecode?: WalletTemplateScenarioBytecode | ['slot']): boolean => { + return Array.isArray(lockingBytecode) && lockingBytecode.length === 1 && lockingBytecode[0] === 'slot'; +}; From 20080f5f48999cf9ad284a1c5e3eef5bdb5460f9 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 2 Oct 2025 11:26:27 +0200 Subject: [PATCH 25/26] Bump version to 0.12.0 --- examples/announcement.cash | 2 +- examples/hodl_vault.cash | 2 +- examples/mecenas.cash | 2 +- examples/mecenas_locktime.cash | 2 +- examples/p2pkh.cash | 2 +- examples/package.json | 6 +++--- examples/testing-suite/package.json | 6 +++--- examples/transfer_with_timeout.cash | 2 +- packages/cashc/package.json | 4 ++-- packages/cashc/src/index.ts | 2 +- packages/cashscript/package.json | 4 ++-- packages/utils/package.json | 2 +- website/docs/basics/getting-started.md | 2 +- website/docs/language/contracts.md | 12 ++++++------ website/docs/language/examples.md | 6 +++--- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/examples/announcement.cash b/examples/announcement.cash index a678e80a..d8dbce8a 100644 --- a/examples/announcement.cash +++ b/examples/announcement.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; /* This is a contract showcasing covenants outside of regular transactional use. * It enforces the contract to make an "announcement" on Memo.cash, and send the diff --git a/examples/hodl_vault.cash b/examples/hodl_vault.cash index 1af8c96b..95adbfa8 100644 --- a/examples/hodl_vault.cash +++ b/examples/hodl_vault.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // This contract forces HODLing until a certain price target has been reached // A minimum block is provided to ensure that oracle price entries from before this block are disregarded diff --git a/examples/mecenas.cash b/examples/mecenas.cash index 341a523d..aef868ab 100644 --- a/examples/mecenas.cash +++ b/examples/mecenas.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; /* This is an unofficial CashScript port of Licho's Mecenas contract. It is * not compatible with Licho's EC plugin, but rather meant as a demonstration diff --git a/examples/mecenas_locktime.cash b/examples/mecenas_locktime.cash index cb0e56ff..f4d1ccdc 100644 --- a/examples/mecenas_locktime.cash +++ b/examples/mecenas_locktime.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // This is an experimental contract for a more "streaming" Mecenas experience // Completely untested, just a concept diff --git a/examples/p2pkh.cash b/examples/p2pkh.cash index 6cc64059..0cd49ca0 100644 --- a/examples/p2pkh.cash +++ b/examples/p2pkh.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { // Require pk to match stored pkh and signature to match diff --git a/examples/package.json b/examples/package.json index 9ac02049..9e99d730 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,7 +1,7 @@ { "name": "cashscript-examples", "private": true, - "version": "0.11.5", + "version": "0.12.0", "description": "Usage examples of the CashScript SDK", "main": "p2pkh.js", "type": "module", @@ -13,8 +13,8 @@ "dependencies": { "@bitauth/libauth": "^3.1.0-next.2", "@types/node": "^22.17.0", - "cashc": "^0.11.5", - "cashscript": "^0.11.5", + "cashc": "^0.12.0", + "cashscript": "^0.12.0", "eslint": "^8.56.0", "typescript": "^5.9.2" } diff --git a/examples/testing-suite/package.json b/examples/testing-suite/package.json index 4502082b..c705b655 100644 --- a/examples/testing-suite/package.json +++ b/examples/testing-suite/package.json @@ -1,6 +1,6 @@ { "name": "testing-suite", - "version": "0.11.5", + "version": "0.12.0", "description": "Example project to develop and test CashScript contracts", "main": "index.js", "type": "module", @@ -26,8 +26,8 @@ }, "dependencies": { "@bitauth/libauth": "^3.1.0-next.2", - "cashc": "^0.11.5", - "cashscript": "^0.11.5", + "cashc": "^0.12.0", + "cashscript": "^0.12.0", "url-join": "^5.0.0" }, "devDependencies": { diff --git a/examples/transfer_with_timeout.cash b/examples/transfer_with_timeout.cash index f49b0e9d..98723e44 100644 --- a/examples/transfer_with_timeout.cash +++ b/examples/transfer_with_timeout.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout( pubkey sender, diff --git a/packages/cashc/package.json b/packages/cashc/package.json index 0e90a460..43e145a6 100644 --- a/packages/cashc/package.json +++ b/packages/cashc/package.json @@ -1,6 +1,6 @@ { "name": "cashc", - "version": "0.11.5", + "version": "0.12.0", "description": "Compile Bitcoin Cash contracts to Bitcoin Cash Script or artifacts", "keywords": [ "bitcoin", @@ -52,7 +52,7 @@ }, "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", - "@cashscript/utils": "^0.11.5", + "@cashscript/utils": "^0.12.0", "antlr4": "^4.13.2", "commander": "^14.0.0", "semver": "^7.7.2" diff --git a/packages/cashc/src/index.ts b/packages/cashc/src/index.ts index 15cdeb27..a4efe3fc 100644 --- a/packages/cashc/src/index.ts +++ b/packages/cashc/src/index.ts @@ -2,4 +2,4 @@ export * from './Errors.js'; export * as utils from '@cashscript/utils'; export { compileFile, compileString } from './compiler.js'; -export const version = '0.11.5'; +export const version = '0.12.0'; diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index 55cdebc9..f830fbc2 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -1,6 +1,6 @@ { "name": "cashscript", - "version": "0.11.5", + "version": "0.12.0", "description": "Easily write and interact with Bitcoin Cash contracts", "keywords": [ "bitcoin cash", @@ -46,7 +46,7 @@ }, "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", - "@cashscript/utils": "^0.11.5", + "@cashscript/utils": "^0.12.0", "@electrum-cash/network": "^4.1.3", "@mr-zwets/bchn-api-wrapper": "^1.0.1", "pako": "^2.1.0", diff --git a/packages/utils/package.json b/packages/utils/package.json index c29a3171..d4cdb6bf 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@cashscript/utils", - "version": "0.11.5", + "version": "0.12.0", "description": "CashScript utilities and types", "keywords": [ "bitcoin cash", diff --git a/website/docs/basics/getting-started.md b/website/docs/basics/getting-started.md index 31778f86..2d1dae35 100644 --- a/website/docs/basics/getting-started.md +++ b/website/docs/basics/getting-started.md @@ -52,7 +52,7 @@ We can start from a basic `TransferWithTimeout` smart contract, a simple contrac Open your code editor to start writing your first CashScript smart contract. Then create a new file `TransferWithTimeout.cash` and copy over the smart contracts code from below. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) { // Allow the recipient to claim their received money diff --git a/website/docs/language/contracts.md b/website/docs/language/contracts.md index 9cb0c75d..f58036a0 100644 --- a/website/docs/language/contracts.md +++ b/website/docs/language/contracts.md @@ -13,7 +13,7 @@ Contract authors should be careful when allowing a range of versions to check th #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; pragma cashscript >= 0.7.0 < 0.9.3; ``` @@ -22,7 +22,7 @@ A CashScript constructor works slightly differently than what you might be used #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract HTLC(pubkey sender, pubkey recipient, int expiration, bytes32 hash) { ... @@ -46,7 +46,7 @@ The main construct in a CashScript contract is the function. A contract can cont #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) { function transfer(sig recipientSig) { @@ -87,7 +87,7 @@ The error message in a `require` statement is only available in debug evaluation #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { function spend(pubkey pk, sig s) { @@ -129,7 +129,7 @@ There is no implicit type conversion from non-boolean to boolean types. So `if ( #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract OneOfTwo(bytes20 pkh1, bytes32 hash1, bytes20 pkh2, bytes32 hash2) { function spend(pubkey pk, sig s, bytes message) { @@ -156,7 +156,7 @@ Logging is only available in debug evaluation of a transaction, but has no impac #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { function spend(pubkey pk, sig s) { diff --git a/website/docs/language/examples.md b/website/docs/language/examples.md index 434f2981..80c12567 100644 --- a/website/docs/language/examples.md +++ b/website/docs/language/examples.md @@ -12,7 +12,7 @@ This smart contract works by connecting with a price oracle. This price oracle i This involves some degree of trust in the price oracle, but since the oracle produces price data for everyone to use, their incentive to attack *your* smart contract is minimised. To improve this situation, you can also choose to connect with multiple oracle providers so you do not have to trust a single party. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // A minimum block is provided to ensure that oracle price entries from before // this block are disregarded. i.e. when the BCH price was $1000 in the past, @@ -53,7 +53,7 @@ The contract works by checking that a UTXO is at least 30 days old, after which Due to the nature of covenants, we have to be very specific about the outputs (amounts and destinations) of the transaction. This also means that we have to account for the special case where the remaining contract balance is lower than the `pledge` amount, meaning no remainder should be sent back. Finally, we have to account for a small fee that has to be taken from the contract's balance to pay the miners. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract Mecenas(bytes20 recipient, bytes20 funder, int pledge, int period) { function receive() { @@ -95,7 +95,7 @@ AMM DEX contract based on [the Cauldron DEX contract](https://www.cauldron.quest The CashScript contract code has the big advantage of abstracting away any stack management, having variable names, explicit types and a logical order of operations (compared to the 'reverse Polish notation' of raw script). ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract DexContract(bytes20 poolOwnerPkh) { function swap() { From 4eea331c9c988d0458453858b854782c09506c54 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 2 Oct 2025 11:52:47 +0200 Subject: [PATCH 26/26] Final changes before 0.12 release - Rename bitauthUri to getBitauthUri and update docs for it - Move around some utils - Add getVmResourceUsage to release notes and docs - Update docs for MockNetworkProvider default changes - Update libauth version in examples/ --- examples/package.json | 2 +- examples/testing-suite/artifacts/example.json | 4 +- examples/testing-suite/package.json | 2 +- packages/cashscript/src/TransactionBuilder.ts | 5 +-- .../src/libauth-template/LibauthTemplate.ts | 4 +- .../cashscript/src/libauth-template/utils.ts | 14 ++++++- packages/cashscript/src/utils.ts | 17 --------- .../test/debugging-old-artifacts.test.ts | 4 +- .../cashscript/test/e2e/MultiContract.test.ts | 4 +- .../libauth-template/LibauthTemplate.test.ts | 2 +- .../LibauthTemplateMultiContract.test.ts | 2 +- .../test/multi-contract-debugging.test.ts | 8 ++-- website/docs/guides/debugging.md | 6 +-- website/docs/releases/migration-notes.md | 4 +- website/docs/releases/release-notes.md | 6 ++- website/docs/sdk/other-network-providers.md | 2 +- website/docs/sdk/transaction-builder.md | 6 +-- website/docs/sdk/transactions.md | 38 ++++++++++++++++++- yarn.lock | 5 --- 19 files changed, 82 insertions(+), 53 deletions(-) diff --git a/examples/package.json b/examples/package.json index 9e99d730..ff3f9988 100644 --- a/examples/package.json +++ b/examples/package.json @@ -11,7 +11,7 @@ "lint": "eslint . --ext .ts --ignore-path ../.eslintignore" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", + "@bitauth/libauth": "^3.1.0-next.8", "@types/node": "^22.17.0", "cashc": "^0.12.0", "cashscript": "^0.12.0", diff --git a/examples/testing-suite/artifacts/example.json b/examples/testing-suite/artifacts/example.json index 79c72269..b28c0de2 100644 --- a/examples/testing-suite/artifacts/example.json +++ b/examples/testing-suite/artifacts/example.json @@ -41,7 +41,7 @@ }, "compiler": { "name": "cashc", - "version": "0.11.3" + "version": "0.12.0" }, - "updatedAt": "2025-08-05T08:32:16.100Z" + "updatedAt": "2025-10-02T09:56:11.510Z" } \ No newline at end of file diff --git a/examples/testing-suite/package.json b/examples/testing-suite/package.json index c705b655..53b4f682 100644 --- a/examples/testing-suite/package.json +++ b/examples/testing-suite/package.json @@ -25,7 +25,7 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", + "@bitauth/libauth": "^3.1.0-next.8", "cashc": "^0.12.0", "cashscript": "^0.12.0", "url-join": "^5.0.0" diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 57d67c7a..dffeae7a 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -247,8 +247,7 @@ export class TransactionBuilder { return vmResourceUsage; } - // TODO: rename to getBitauthUri() - bitauthUri(): string { + getBitauthUri(): string { console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'); return getBitauthUri(this.getLibauthTemplate()); } @@ -273,7 +272,7 @@ export class TransactionBuilder { return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid); } catch (e: any) { const reason = e.error ?? e.message; - throw new FailedTransactionError(reason, this.bitauthUri()); + throw new FailedTransactionError(reason, this.getBitauthUri()); } } diff --git a/packages/cashscript/src/libauth-template/LibauthTemplate.ts b/packages/cashscript/src/libauth-template/LibauthTemplate.ts index 225f03ab..27d15a50 100644 --- a/packages/cashscript/src/libauth-template/LibauthTemplate.ts +++ b/packages/cashscript/src/libauth-template/LibauthTemplate.ts @@ -31,11 +31,11 @@ import { VmTarget, } from '../interfaces.js'; import SignatureTemplate from '../SignatureTemplate.js'; -import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from '../utils.js'; +import { addressToLockScript, extendedStringify, zip } from '../utils.js'; import { TransactionBuilder } from '../TransactionBuilder.js'; import { deflate } from 'pako'; import MockNetworkProvider from '../network/MockNetworkProvider.js'; -import { addHexPrefixExceptEmpty, formatBytecodeForDebugging, formatParametersForDebugging, getLockScriptName, getUnlockScriptName, lockingBytecodeIsSetToSlot, serialiseTokenDetails } from './utils.js'; +import { addHexPrefixExceptEmpty, formatBytecodeForDebugging, formatParametersForDebugging, getLockScriptName, getSignatureAndPubkeyFromP2PKHInput, getUnlockScriptName, lockingBytecodeIsSetToSlot, serialiseTokenDetails } from './utils.js'; // TODO: Add / improve descriptions throughout the template generation diff --git a/packages/cashscript/src/libauth-template/utils.ts b/packages/cashscript/src/libauth-template/utils.ts index 9e2f4169..f4e3b02c 100644 --- a/packages/cashscript/src/libauth-template/utils.ts +++ b/packages/cashscript/src/libauth-template/utils.ts @@ -1,6 +1,6 @@ import { AbiFunction, AbiInput, Artifact, bytecodeToScript, formatBitAuthScript } from '@cashscript/utils'; import { HashType, LibauthTokenDetails, SignatureAlgorithm, TokenDetails } from '../interfaces.js'; -import { hexToBin, binToHex, isHex, decodeCashAddress, type WalletTemplateScenarioBytecode } from '@bitauth/libauth'; +import { hexToBin, binToHex, isHex, decodeCashAddress, type WalletTemplateScenarioBytecode, Input, assertSuccess, decodeAuthenticationInstructions, AuthenticationInstructionPush } from '@bitauth/libauth'; import { EncodedFunctionArgument } from '../Argument.js'; import { zip } from '../utils.js'; import SignatureTemplate from '../SignatureTemplate.js'; @@ -112,3 +112,15 @@ interface LibauthTemplateTokenDetails { export const lockingBytecodeIsSetToSlot = (lockingBytecode?: WalletTemplateScenarioBytecode | ['slot']): boolean => { return Array.isArray(lockingBytecode) && lockingBytecode.length === 1 && lockingBytecode[0] === 'slot'; }; + +export const getSignatureAndPubkeyFromP2PKHInput = ( + libauthInput: Input, +): { signature: Uint8Array; publicKey: Uint8Array } => { + const inputData = (assertSuccess( + decodeAuthenticationInstructions(libauthInput.unlockingBytecode)) + ) as AuthenticationInstructionPush[]; + const signature = inputData[0].data; + const publicKey = inputData[1].data; + + return { signature, publicKey }; +}; diff --git a/packages/cashscript/src/utils.ts b/packages/cashscript/src/utils.ts index 23c61df3..8b23ff41 100644 --- a/packages/cashscript/src/utils.ts +++ b/packages/cashscript/src/utils.ts @@ -14,11 +14,6 @@ import { bigIntToCompactUint, NonFungibleTokenCapability, bigIntToVmNumber, - assertSuccess, - AuthenticationInstructionPush, - AuthenticationInstructions, - decodeAuthenticationInstructions, - Input, } from '@bitauth/libauth'; import { encodeInt, @@ -373,15 +368,3 @@ export const isFungibleTokenUtxo = (utxo: Utxo): boolean => ( export const isNonTokenUtxo = (utxo: Utxo): boolean => utxo.token === undefined; export const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); - -export const getSignatureAndPubkeyFromP2PKHInput = ( - libauthInput: Input, -): { signature: Uint8Array; publicKey: Uint8Array } => { - const inputData = (assertSuccess( - decodeAuthenticationInstructions(libauthInput.unlockingBytecode)) as AuthenticationInstructions - ) as AuthenticationInstructionPush[]; - const signature = inputData[0].data; - const publicKey = inputData[1].data; - - return { signature, publicKey }; -}; diff --git a/packages/cashscript/test/debugging-old-artifacts.test.ts b/packages/cashscript/test/debugging-old-artifacts.test.ts index 35ced7a8..fc33c7a7 100644 --- a/packages/cashscript/test/debugging-old-artifacts.test.ts +++ b/packages/cashscript/test/debugging-old-artifacts.test.ts @@ -35,7 +35,7 @@ describe('Debugging tests - old artifacts', () => { .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) .addOutput({ to: contractTestLogs.address, amount: 10000n }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); expect(() => transaction.debug()).not.toThrow(); }); @@ -50,7 +50,7 @@ describe('Debugging tests - old artifacts', () => { .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(bobPriv))) .addOutput({ to: contractTestLogs.address, amount: 10000n }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); expect(() => transaction.debug()).toThrow(); }); diff --git a/packages/cashscript/test/e2e/MultiContract.test.ts b/packages/cashscript/test/e2e/MultiContract.test.ts index 3167ec1c..8fc3ca66 100644 --- a/packages/cashscript/test/e2e/MultiContract.test.ts +++ b/packages/cashscript/test/e2e/MultiContract.test.ts @@ -87,7 +87,7 @@ describe('Multi Contract', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.log(transaction.bitauthUri()); + console.log(transaction.getBitauthUri()); const txPromise = transaction.send(); @@ -173,7 +173,7 @@ describe('Multi Contract', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.log(transaction.bitauthUri()); + console.log(transaction.getBitauthUri()); const txPromise = transaction.send(); diff --git a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts index 0a95ca7e..40fd0bd3 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts @@ -5,7 +5,7 @@ describe('Libauth Template generation tests (single-contract)', () => { it(`should generate a valid libauth template for ${fixture.name}`, () => { const generatedTemplate = fixture.transaction.getLibauthTemplate(); // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(fixture.transaction.bitauthUri()); + // console.warn(fixture.transaction.getBitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); }); diff --git a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts index 438c1b30..33f4cfb6 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts @@ -6,7 +6,7 @@ describe('Libauth Template generation tests (multi-contract)', () => { const builder = await fixture.transaction; const generatedTemplate = builder.getLibauthTemplate(); // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(builder.bitauthUri()); + // console.warn(builder.getBitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); }); diff --git a/packages/cashscript/test/multi-contract-debugging.test.ts b/packages/cashscript/test/multi-contract-debugging.test.ts index b28c8c59..c9a7032d 100644 --- a/packages/cashscript/test/multi-contract-debugging.test.ts +++ b/packages/cashscript/test/multi-contract-debugging.test.ts @@ -143,7 +143,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('P2PKH.cash:4 Require statement failed at input 0 in contract P2PKH.cash at line 4.'); }); @@ -169,7 +169,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('BigInt.cash:4 Require statement failed at input 1 in contract BigInt.cash at line 4.'); }); @@ -197,7 +197,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('P2PKH.cash:5 Require statement failed at input 0 in contract P2PKH.cash at line 5'); }); @@ -225,7 +225,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('BigInt.cash'); }); diff --git a/website/docs/guides/debugging.md b/website/docs/guides/debugging.md index fe4d7de8..5189623f 100644 --- a/website/docs/guides/debugging.md +++ b/website/docs/guides/debugging.md @@ -42,10 +42,10 @@ To help with debugging you can add `console.log` statements to your CashScript c Whenever a transaction fails, there will be a link in the console to open your smart contract transaction in the BitAuth IDE. This will allow you to inspect the transaction in detail, and see exactly why the transaction failed. In the BitAuth IDE you will see the raw BCH Script mapping to each line in your CashScript contract. Find the failing line and investigate the failing OpCode. You can break up the failing line, one opcode at a time, to see how the stack evolves and ends with your `require` failure. -It's also possible to export the transaction for step-by-step debugging in the BitAuth IDE without failure. To do so, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. +It's also possible to export the transaction for step-by-step debugging in the BitAuth IDE without failure. To do so, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. ```ts -const uri = await transactionBuilder.bitauthUri(); +const uri = await transactionBuilder.getBitauthUri(); ``` :::caution @@ -75,7 +75,7 @@ OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, se OP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */ OP_2DROP OP_1 /* } */ OP_ENDIF /* } */ - + ``` [BitauthIDE]: https://ide.bitauth.com/import-template/eJzFWAtv2zYQ_isCN2BJ4drUW8raAK3jNkbSJEvcFUMdGBR1stXakidRWYIg--07SrIk23LgDhlGBKEiHu-7x3fUMY_k55TPYMHIEZkJsUyPer3Qh64XCpaJWZfHi558gEiEnIkwjl4LWCznTMDrO9ot9na_pXFEOsSHlCfhUkqhuuFiGScCfCVI4oXCWTorVlEwYgtAiT6-u8nfKR8hgoRJ6RPwsuk0jKbKqATCDWm2LJSRo6_kff90olHNnFCT3HbIHSRpjkg7RJopQkjJ0SMZJSxKA0i-hGI2ChcQZ2ISRstM0MmSJWiBwI1ScN3sfhyJhHGh8ARyhxUWoQ9ZxPM_GlsrP1qQlIMcSvmJHkrzc_2pNL7NqnnMv6NU25JYNzyLclnpNUtC5s0LV1OIfEhuwum2O-N6cUxq65U4qH0akxJmTGqnap0dIh6W8tUZPJCnTrmyG2oTR8zCVOFlWDcBau1f2HwO4oQJJkES4OEyxHy24VSLe0LVynaglf63YVWh2QtppWgHzirkmE8f7rfhymqoMpOLKSJW4B54lpdCCbShqRUPR4N77RXRTjAUXrI0hZ2U3dgGd2yeyVK93YxEWyHgaq_XZF1beY2jNxUF5TkTxUkyScNpxESWQBfdnOBeVJ1O5HMm7uP0WEG9KDOOxhE-bYQxjKqEofL1AOY7w0gob5U36vFYnmZFKNL2-i5q9qm9aFchlDMeZKMiM0stnenas6fGVoRaZDBavDqi4igVScZFnKxHroxs0yt671KfUh2de1PVQy6wzLzv8FDIUN0JTNM0HN8OdF91dA6e5wDYNtWtAMAzqBMYzKQ6DTzDsWzVtkzX81zDCMDEZX5cJW5buaY6vqFalmq5mouPGlcD29JBM3QwPNPjuh84oOIyZcw1dU-1mQVUc6lmaWbg0uMys96DAB77MI6U_UbvlbJM2HTBGh8i5W_aVdUu_XVPHc3xqvcj0C86fgy64koLlQ7K5BTp6qxyVbGjk3On5NKh8vgvvMZcXcOfWZhArfaXVKnqWJ5tCyb4bB-vL68muoK_rob9MzlT-evi86fBb5_fncvn4YcGdFX9ovT8AFFrK_BYkS7tMUpoQyJcX56fF_NITv3TQf_sZvhxw2s5ksLtA-yQ-HcEO2gid2o7Dg-f4V8JfTG8UramwfnNYDvgT_t4tK_Xe4r-vwzPp5pmq--OnCW7PMk8hlnw80au4PpOCjZptsq12qTZ74Pr4Yc_GtA1zcqikiyrvlx7UmwX9M2Xd1dNnpXoz9Gsgu6UVjxLsDXoNbTzy_7ZaPhpUDvcDHgTWtx382Afv63Oiv2P1BJaO7m-vCqCvXP8FwyXdXRxUpwbzwyEfjHYFfSeoi9fXKRoCyHCK0S8b2NYdXgoXjYwg_JV26dFaW3_1pvddxG2tQzveKCU2mUZ_TUDWZOyvS4_00XvW3SHsiftSk2ywUVTVo1AfQtCxS_RaZC1Kwh5ic6INO4ZZNWOke1LAVFlY8mzJEHs97KJPIVwOsNd2vprGWtypNqGhViaa3QIfr7zhC6T8A4zc1b-2bgfEstwLddjhqepaJvuO6pl2hq1uUZdz0HjTUvXLIeCYXsAGmO2a_HA9j3K8YfbJL9V5N9UxotMPpI8yfKy8EhkUx5j3zAsnEGjVm9G9Z5TbMLQFgBuBa5lqrYWcNc3HGrqAQ-Au6aqqtRmjgm6r_nUU33VBWmyjUmjlu5z3fAC2S_jIQQRh4ts4cnkGxgH17LzaBQdPHbg7yuWfCXpPBbk9gkvKnJR5CHUTIqjsHTlx9ZWYlvMVQ1HM1Xug61xmfvA9wPbMTBArmcyZpgOhs-2ASzHYTLjkthww0ScIqMxWdRFpKfmvy00WY1xlnC4fA5-ZXmbSsNxnm4xL_8AUE0PwQ== diff --git a/website/docs/releases/migration-notes.md b/website/docs/releases/migration-notes.md index 96337cc9..119d7925 100644 --- a/website/docs/releases/migration-notes.md +++ b/website/docs/releases/migration-notes.md @@ -24,7 +24,7 @@ const provider = new ElectrumNetworkProvider('mainnet'); const contract = new Contract(artifact, constructorArgs, { provider }); ``` -#### Transaction Builder Max Fee +#### Transaction Builder Before, the `setMaxFee()` method was used to set the maximum fee for the transaction. This was replaced with the `maximumFeeSatoshis` option in the constructor. Additionally, the `maximumFeeSatsPerByte` option was added. ```ts @@ -35,6 +35,8 @@ const builder = new TransactionBuilder({ provider }).setMaxFee(1000n); const builder = new TransactionBuilder({ provider, maximumFeeSatoshis: 1000n }); ``` +Addtionally, `transactionBuilder.bitauthUri()` was renamed to `transactionBuilder.getBitauthUri()` for consistency. + #### MockNetworkProvider Before, the `updateUtxoSet` option was `false` by default for the `MockNetworkProvider`. This is now `true` by default to better match real-world network behaviour. diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 049059a2..12f05d96 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -4,7 +4,8 @@ title: Release Notes ## v0.12.0 -#### CashScript SDK`TransactionBuilderOptions` on the constructor. +#### CashScript SDK +- :sparkles: Add `getVmResourceUsage` method to `TransactionBuilder`. - :sparkles: Add `maximumFeeSatsPerByte` and `allowImplicitFungibleTokenBurn` options to `TransactionBuilder` constructor. - :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. - :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. @@ -14,6 +15,7 @@ title: Release Notes - :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. - :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. - :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with +- :boom: **BREAKING**: Rename `bitauthUri()` method on `TransactionBuilder` to `getBitauthUri()` for consistency. - :hammer_and_wrench: Improve libauth template generation. - :bug: Fix bug where `SignatureTemplate` would not accept private key hex strings as a signer. @@ -63,6 +65,8 @@ This update adds CashScript support for the new BCH 2025 network upgrade. To rea This release also contains several breaking changes, please refer to the [migration notes](/docs/releases/migration-notes) for more information. +Thanks [kiok](https://x.com/cypherpunk_bch) for the significant contributions! + #### cashc compiler - :bug: Fix bug where source code in `--format ts` artifacts used incorrect quotation marks. - :hammer_and_wrench: Remove warning for opcount and update warning for byte size to match new limits. diff --git a/website/docs/sdk/other-network-providers.md b/website/docs/sdk/other-network-providers.md index 5e3233aa..85429d98 100644 --- a/website/docs/sdk/other-network-providers.md +++ b/website/docs/sdk/other-network-providers.md @@ -14,7 +14,7 @@ The `MockNetworkProvider` is a special network provider that allows you to evalu The `MockNetworkProvider` has extra methods to enable this local emulation such as `.addUtxo()` and `.setBlockHeight()`. You can read more about the `MockNetworkProvider` and automated tests on the [testing setup](/docs/sdk/testing-setup) page. -The `updateUtxoSet` option is used to determine whether the UTXO set should be updated after a transaction is sent. If `updateUtxoSet` is `true`, the UTXO set will be updated to reflect the new state of the mock network. If `updateUtxoSet` is `false` (default), the UTXO set will not be updated. +The `updateUtxoSet` option is used to determine whether the UTXO set should be updated after a transaction is sent. If `updateUtxoSet` is `true` (default), the UTXO set will be updated to reflect the new state of the mock network. If `updateUtxoSet` is `false`, the UTXO set will not be updated. #### Example ```ts diff --git a/website/docs/sdk/transaction-builder.md b/website/docs/sdk/transaction-builder.md index 42a5027d..d96523a6 100644 --- a/website/docs/sdk/transaction-builder.md +++ b/website/docs/sdk/transaction-builder.md @@ -243,12 +243,12 @@ transactionBuilder.debug(): DebugResult If you want to debug a transaction locally instead of sending it to the network, you can call the `debug()` function on the transaction. This will return intermediate values and the final result of the transaction. It will also show any logged values and `require` error messages. -### bitauthUri() +### getBitauthUri() ```ts -transactionBuilder.bitauthUri(): string +transactionBuilder.getBitauthUri(): string ``` -If you prefer a lower-level debugging experience, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. +If you prefer a lower-level debugging experience, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. You can read more about debugging transactions on the [debugging page](/docs/guides/debugging). :::caution diff --git a/website/docs/sdk/transactions.md b/website/docs/sdk/transactions.md index 96cdee17..5b2ca52d 100644 --- a/website/docs/sdk/transactions.md +++ b/website/docs/sdk/transactions.md @@ -248,11 +248,11 @@ const txHex = await instance.functions .build() ``` -### debug() & bitauthUri() +### debug() & getBitauthUri() If you want to debug a transaction locally instead of sending it to the network, you can call the `debug()` function on the transaction. This will return intermediate values and the final result of the transaction. It will also show any logged values and `require` error messages. -If you prefer a lower-level debugging experience, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. +If you prefer a lower-level debugging experience, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. You can read more about debugging transactions on the [debugging page](/docs/guides/debugging). @@ -260,6 +260,40 @@ You can read more about debugging transactions on the [debugging page](/docs/gui It is unsafe to debug transactions on mainnet using the BitAuth IDE as private keys will be exposed to BitAuth IDE and transmitted over the network. ::: +### getVmResourceUsage() +```ts +transaction.getVmResourceUsage(verbose: boolean = false): Array +``` + +The `getVmResourceUsage()` function allows you to get the VM resource usage for the transaction. This can be useful for debugging and optimization. + +```ts +interface VmResourceUsage { + arithmeticCost: number; + definedFunctions: number; + hashDigestIterations: number; + maximumOperationCost: number; + maximumHashDigestIterations: number; + maximumSignatureCheckCount: number; + densityControlLength: number; + operationCost: number; + signatureCheckCount: number; +} +``` + +The verbose mode logs the VM resource usage for each input to the console. + +``` +VM Resource usage by inputs: +┌─────────┬─────────────────────────────────────────────────┬─────┬──────────────────────────┬───────────┬──────────┐ +│ (index) │ Contract - Function │ Ops │ Op Cost Budget Usage │ SigChecks │ Hashes │ +├─────────┼─────────────────────────────────────────────────┼─────┼──────────────────────────┼───────────┼──────────┤ +│ 0 │ 'SingleFunction - test_require_single_function' │ 7 │ '1,155 / 36,000 (3%)' │ '0 / 1' │ '2 / 22' │ +│ 1 │ 'ZeroHandling - test_zero_handling' │ 13 │ '1,760 / 40,800 (4%)' │ '0 / 1' │ '2 / 25' │ +│ 2 │ 'P2PKH Input' │ 7 │ '28,217 / 112,800 (25%)' │ '1 / 3' │ '7 / 70' │ +└─────────┴─────────────────────────────────────────────────┴─────┴──────────────────────────┴───────────┴──────────┘ +``` + ## Transaction errors When sending a transaction, the CashScript SDK will throw an error if the transaction fails. If you are using an artifact compiled with `cashc@0.10.0` or later, the error will be of the type `FailedRequireError` or `FailedTransactionEvaluationError`. In case of a `FailedRequireError`, the error will refer to the corresponding `require` statement in the contract code so you know where your contract failed. If you want more information about the underlying error, you can check the `libauthErrorMessage` property of the error. diff --git a/yarn.lock b/yarn.lock index ad42ae0b..04d9884d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -625,11 +625,6 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bitauth/libauth@^3.1.0-next.2": - version "3.1.0-next.2" - resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.2.tgz#121782b38774d9fba8226406db9b9af0c8d8e464" - integrity sha512-XRtk9p8UHvtjSPS38rsfHXzaPHG5j9FpN4qHqqGLoAuZYy675PBiOy9zP6ah8lTnnIVaCFl2ekct8w0Hy1oefw== - "@bitauth/libauth@^3.1.0-next.8": version "3.1.0-next.8" resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz#d130e5db6c3c8b24731c8d04c4091be07f48b0ee"