cashscript 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,17 +37,29 @@ Using the CashScript SDK, you can import contract artifact files, create new ins
37
37
  const provider = new ElectrumNetworkProvider('mainnet');
38
38
 
39
39
  // Create a new P2PKH contract with constructor arguments: { pkh: pkh }
40
- const contract = new Contract(P2PKH, [pkh], provider);
40
+ const contract = new Contract(P2PKH, [pkh], { provider });
41
41
 
42
42
  // Get contract balance & output address + balance
43
43
  console.log('contract address:', contract.address);
44
44
  console.log('contract balance:', await contract.getBalance());
45
45
 
46
- // Call the spend function with the owner's signature
47
- // And use it to send 0. 000 100 00 BCH back to the contract's address
48
- const txDetails = await contract.functions
49
- .spend(pk, new SignatureTemplate(keypair))
50
- .to(contract.address, 10000)
46
+ const transactionBuilder = new TransactionBuilder({ provider });
47
+ const contractUtxos = await contract.getUtxos();
48
+
49
+ const sendAmount = 10_000n;
50
+ const destinationAddress = '... some address ...';
51
+
52
+ // Calculate the change amount, accounting for a miner fee of 1000 satoshis
53
+ const changeAmount = contractUtxos[0].satoshis - sendAmount - 1000n;
54
+
55
+ // Construct a transaction with the transaction builder
56
+ const txDetails = await transactionBuilder
57
+ // Add a contract input that spends from the contract using the 'spend' function
58
+ .addInput(contractUtxos[0], contract.unlock.spend(pk, new SignatureTemplate(keypair)))
59
+ // Add an output that sends 0. 000 100 00 BCH back to the destination address
60
+ .addOutput({ to: destinationAddress, amount: sendAmount })
61
+ // Add a change output that sends the change back to the contract's address
62
+ .addOutput({ to: contract.address, amount: changeAmount })
51
63
  .send();
52
64
 
53
65
  console.log(txDetails);
package/dist/Argument.js CHANGED
@@ -34,15 +34,19 @@ export function encodeFunctionArgument(argument, typeStr) {
34
34
  if (!(argument instanceof Uint8Array)) {
35
35
  throw Error(`Value for type ${type} should be a Uint8Array or hex string`);
36
36
  }
37
- // Redefine SIG as a bytes65 so it is included in the size checks below
38
- // Note that ONLY Schnorr signatures are accepted
39
- if (type === PrimitiveType.SIG && argument.byteLength !== 0) {
40
- type = new BytesType(65);
37
+ // Redefine SIG as a bytes65 (Schnorr) or bytes71, bytes72, bytes73 (ECDSA) or bytes0 (for NULLFAIL)
38
+ if (type === PrimitiveType.SIG) {
39
+ if (![0, 65, 71, 72, 73].includes(argument.byteLength)) {
40
+ throw new TypeError(`bytes${argument.byteLength}`, type);
41
+ }
42
+ type = new BytesType(argument.byteLength);
41
43
  }
42
- // Redefine DATASIG as a bytes64 so it is included in the size checks below
43
- // Note that ONLY Schnorr signatures are accepted
44
- if (type === PrimitiveType.DATASIG && argument.byteLength !== 0) {
45
- type = new BytesType(64);
44
+ // Redefine DATASIG as a bytes64 (Schnorr) or bytes70, bytes71, bytes72 (ECDSA) or bytes0 (for NULLFAIL)
45
+ if (type === PrimitiveType.DATASIG) {
46
+ if (![0, 64, 70, 71, 72].includes(argument.byteLength)) {
47
+ throw new TypeError(`bytes${argument.byteLength}`, type);
48
+ }
49
+ type = new BytesType(argument.byteLength);
46
50
  }
47
51
  // Bounded bytes types require a correctly sized argument
48
52
  if (type instanceof BytesType && type.bound && argument.byteLength !== type.bound) {
@@ -1,36 +1,30 @@
1
1
  import { Artifact, Script } from '@cashscript/utils';
2
- import { Transaction } from './Transaction.js';
3
- import { ConstructorArgument, FunctionArgument } from './Argument.js';
2
+ import { ConstructorArgument } from './Argument.js';
4
3
  import { Unlocker, ContractOptions, Utxo, AddressType } from './interfaces.js';
5
4
  import NetworkProvider from './network/NetworkProvider.js';
6
5
  import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js';
7
6
  export declare class Contract<TArtifact extends Artifact = Artifact, TResolved extends {
8
7
  constructorInputs: ConstructorArgument[];
9
- functions: Record<string, any>;
10
8
  unlock: Record<string, any>;
11
9
  } = {
12
10
  constructorInputs: ParamsToTuple<TArtifact['constructorInputs']>;
13
- functions: AbiToFunctionMap<TArtifact['abi'], Transaction>;
14
11
  unlock: AbiToFunctionMap<TArtifact['abi'], Unlocker>;
15
12
  }> {
16
13
  artifact: TArtifact;
17
- private options?;
14
+ private options;
18
15
  name: string;
19
16
  address: string;
20
17
  tokenAddress: string;
21
18
  bytecode: string;
22
19
  bytesize: number;
23
20
  opcount: number;
24
- functions: TResolved['functions'];
25
21
  unlock: TResolved['unlock'];
26
22
  redeemScript: Script;
27
23
  provider: NetworkProvider;
28
24
  addressType: AddressType;
29
25
  encodedConstructorArgs: Uint8Array[];
30
- constructor(artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], options?: ContractOptions | undefined);
26
+ constructor(artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], options: ContractOptions);
31
27
  getBalance(): Promise<bigint>;
32
28
  getUtxos(): Promise<Utxo[]>;
33
- private createFunction;
34
29
  private createUnlocker;
35
30
  }
36
- export type ContractFunction = (...args: FunctionArgument[]) => Transaction;
package/dist/Contract.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import { binToHex } from '@bitauth/libauth';
2
2
  import { asmToScript, calculateBytesize, countOpcodes, generateRedeemScript, hash256, scriptToBytecode, } from '@cashscript/utils';
3
- import { Transaction } from './Transaction.js';
4
- import { encodeFunctionArgument, encodeConstructorArguments, encodeFunctionArguments, } from './Argument.js';
3
+ import { encodeFunctionArgument, encodeConstructorArguments, } from './Argument.js';
5
4
  import { addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress, } from './utils.js';
6
5
  import SignatureTemplate from './SignatureTemplate.js';
7
- import { ElectrumNetworkProvider } from './network/index.js';
8
6
  import semver from 'semver';
9
7
  export class Contract {
10
8
  constructor(artifact, constructorArgs, options) {
11
9
  this.artifact = artifact;
12
10
  this.options = options;
13
- this.provider = this.options?.provider ?? new ElectrumNetworkProvider();
14
- this.addressType = this.options?.addressType ?? 'p2sh32';
11
+ this.provider = this.options.provider;
12
+ this.addressType = this.options.addressType ?? 'p2sh32';
15
13
  const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName', 'compiler'];
16
14
  if (!expectedProperties.every((property) => property in artifact)) {
17
15
  throw new Error('Invalid or incomplete artifact provided');
@@ -25,21 +23,7 @@ export class Contract {
25
23
  // Encode arguments (this also performs type checking)
26
24
  this.encodedConstructorArgs = encodeConstructorArguments(artifact, constructorArgs);
27
25
  this.redeemScript = generateRedeemScript(asmToScript(this.artifact.bytecode), this.encodedConstructorArgs);
28
- // Populate the functions object with the contract's functions
29
- // (with a special case for single function, which has no "function selector")
30
- this.functions = {};
31
- if (artifact.abi.length === 1) {
32
- const f = artifact.abi[0];
33
- // @ts-ignore TODO: see if we can use generics to make TypeScript happy
34
- this.functions[f.name] = this.createFunction(f);
35
- }
36
- else {
37
- artifact.abi.forEach((f, i) => {
38
- // @ts-ignore TODO: see if we can use generics to make TypeScript happy
39
- this.functions[f.name] = this.createFunction(f, i);
40
- });
41
- }
42
- // Populate the functions object with the contract's functions
26
+ // Populate the 'unlock' object with the contract's functions
43
27
  // (with a special case for single function, which has no "function selector")
44
28
  this.unlock = {};
45
29
  if (artifact.abi.length === 1) {
@@ -67,17 +51,6 @@ export class Contract {
67
51
  async getUtxos() {
68
52
  return this.provider.getUtxos(this.address);
69
53
  }
70
- createFunction(abiFunction, selector) {
71
- return (...args) => {
72
- if (abiFunction.inputs.length !== args.length) {
73
- 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}`);
74
- }
75
- // Encode passed args (this also performs type checking)
76
- const encodedArgs = encodeFunctionArguments(abiFunction, args);
77
- const unlocker = this.createUnlocker(abiFunction, selector)(...args);
78
- return new Transaction(this, unlocker, abiFunction, encodedArgs, selector);
79
- };
80
- }
81
54
  createUnlocker(abiFunction, selector) {
82
55
  return (...args) => {
83
56
  if (abiFunction.inputs.length !== args.length) {
package/dist/Errors.d.ts CHANGED
@@ -19,8 +19,8 @@ export declare class NoDebugInformationInArtifactError extends Error {
19
19
  }
20
20
  export declare class FailedTransactionError extends Error {
21
21
  reason: string;
22
- bitauthUri?: string | undefined;
23
- constructor(reason: string, bitauthUri?: string | undefined);
22
+ bitauthUri: string;
23
+ constructor(reason: string, bitauthUri: string);
24
24
  }
25
25
  export declare class FailedTransactionEvaluationError extends FailedTransactionError {
26
26
  artifact: Artifact;
@@ -5,6 +5,7 @@ export default class SignatureTemplate {
5
5
  privateKey: Uint8Array;
6
6
  constructor(signer: Keypair | Uint8Array | string, hashtype?: HashType, signatureAlgorithm?: SignatureAlgorithm);
7
7
  generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array;
8
+ signMessageHash(payload: Uint8Array): Uint8Array;
8
9
  getHashType(bchForkId?: boolean): number;
9
10
  getSignatureAlgorithm(): SignatureAlgorithm;
10
11
  getPublicKey(): Uint8Array;
@@ -1,4 +1,4 @@
1
- import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
1
+ import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
2
2
  import { hash256, scriptToBytecode } from '@cashscript/utils';
3
3
  import { HashType, SignatureAlgorithm, } from './interfaces.js';
4
4
  import { createSighashPreimage, publicKeyToP2PKHLockingBytecode } from './utils.js';
@@ -11,18 +11,27 @@ export default class SignatureTemplate {
11
11
  this.privateKey = decodeWif(wif);
12
12
  }
13
13
  else if (typeof signer === 'string') {
14
- this.privateKey = decodeWif(signer);
14
+ const maybeHexString = signer.startsWith('0x') ? signer.slice(2) : signer;
15
+ if (isHex(maybeHexString)) {
16
+ this.privateKey = hexToBin(maybeHexString);
17
+ }
18
+ else {
19
+ this.privateKey = decodeWif(maybeHexString);
20
+ }
15
21
  }
16
22
  else {
17
23
  this.privateKey = signer;
18
24
  }
19
25
  }
20
- // TODO: Allow signing of non-transaction messages (i.e. don't add the hashtype)
21
26
  generateSignature(payload, bchForkId) {
27
+ const signature = this.signMessageHash(payload);
28
+ return Uint8Array.from([...signature, this.getHashType(bchForkId)]);
29
+ }
30
+ signMessageHash(payload) {
22
31
  const signature = this.signatureAlgorithm === SignatureAlgorithm.SCHNORR
23
32
  ? secp256k1.signMessageHashSchnorr(this.privateKey, payload)
24
33
  : secp256k1.signMessageHashDER(this.privateKey, payload);
25
- return Uint8Array.from([...signature, this.getHashType(bchForkId)]);
34
+ return signature;
26
35
  }
27
36
  getHashType(bchForkId = true) {
28
37
  return bchForkId ? (this.hashtype | SigningSerializationFlag.forkId) : this.hashtype;
@@ -1,18 +1,21 @@
1
1
  import { Transaction as LibauthTransaction, WalletTemplate } from '@bitauth/libauth';
2
- import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions } from './interfaces.js';
2
+ import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions, VmResourceUsage } from './interfaces.js';
3
3
  import { NetworkProvider } from './network/index.js';
4
4
  import { DebugResults } from './debugging.js';
5
5
  import { WcTransactionOptions } from './walletconnect-utils.js';
6
6
  import { WcTransactionObject } from './walletconnect-utils.js';
7
7
  export interface TransactionBuilderOptions {
8
8
  provider: NetworkProvider;
9
+ maximumFeeSatoshis?: bigint;
10
+ maximumFeeSatsPerByte?: number;
11
+ allowImplicitFungibleTokenBurn?: boolean;
9
12
  }
10
13
  export declare class TransactionBuilder {
11
14
  provider: NetworkProvider;
12
15
  inputs: UnlockableUtxo[];
13
16
  outputs: Output[];
14
17
  locktime: number;
15
- maxFee?: bigint;
18
+ options: TransactionBuilderOptions;
16
19
  constructor(options: TransactionBuilderOptions);
17
20
  addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this;
18
21
  addInputs(utxos: Utxo[], unlocker: Unlocker, options?: InputOptions): this;
@@ -21,12 +24,13 @@ export declare class TransactionBuilder {
21
24
  addOutputs(outputs: Output[]): this;
22
25
  addOpReturnOutput(chunks: string[]): this;
23
26
  setLocktime(locktime: number): this;
24
- setMaxFee(maxFee: bigint): this;
25
27
  private checkMaxFee;
28
+ private checkFungibleTokenBurn;
26
29
  buildLibauthTransaction(): LibauthTransaction;
27
30
  build(): string;
28
31
  debug(): DebugResults;
29
- bitauthUri(): string;
32
+ getVmResourceUsage(verbose?: boolean): Array<VmResourceUsage>;
33
+ getBitauthUri(): string;
30
34
  getLibauthTemplate(): WalletTemplate;
31
35
  send(): Promise<TransactionDetails>;
32
36
  send(raw: true): Promise<string>;
@@ -1,10 +1,8 @@
1
1
  import { binToHex, decodeTransaction, decodeTransactionUnsafe, encodeTransaction, hexToBin, } from '@bitauth/libauth';
2
- import delay from 'delay';
3
- import { isUnlockableUtxo, isStandardUnlockableUtxo, isP2PKHUnlocker, } from './interfaces.js';
4
- import { cashScriptOutputToLibauthOutput, createOpReturnOutput, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
2
+ import { isUnlockableUtxo, isStandardUnlockableUtxo, isContractUnlocker, } from './interfaces.js';
3
+ import { cashScriptOutputToLibauthOutput, createOpReturnOutput, delay, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
5
4
  import { FailedTransactionError } from './Errors.js';
6
- import { getBitauthUri } from './LibauthTemplate.js';
7
- import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js';
5
+ import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libauth-template/LibauthTemplate.js';
8
6
  import { getWcContractInfo } from './walletconnect-utils.js';
9
7
  import semver from 'semver';
10
8
  const DEFAULT_SEQUENCE = 0xfffffffe;
@@ -14,6 +12,10 @@ export class TransactionBuilder {
14
12
  this.outputs = [];
15
13
  this.locktime = 0;
16
14
  this.provider = options.provider;
15
+ this.options = {
16
+ allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false,
17
+ ...options,
18
+ };
17
19
  }
18
20
  addInput(utxo, unlocker, options) {
19
21
  return this.addInputs([utxo], unlocker, options);
@@ -48,22 +50,45 @@ export class TransactionBuilder {
48
50
  this.locktime = locktime;
49
51
  return this;
50
52
  }
51
- setMaxFee(maxFee) {
52
- this.maxFee = maxFee;
53
- return this;
54
- }
55
- checkMaxFee() {
56
- if (!this.maxFee)
57
- return;
53
+ checkMaxFee(transaction) {
58
54
  const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
59
55
  const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n);
60
56
  const fee = totalInputAmount - totalOutputAmount;
61
- if (fee > this.maxFee) {
62
- throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.maxFee}`);
57
+ if (this.options.maximumFeeSatoshis && fee > this.options.maximumFeeSatoshis) {
58
+ throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.options.maximumFeeSatoshis}`);
59
+ }
60
+ if (this.options.maximumFeeSatsPerByte) {
61
+ const transactionSize = encodeTransaction(transaction).byteLength;
62
+ const feePerByte = Number((Number(fee) / transactionSize).toFixed(2));
63
+ if (feePerByte > this.options.maximumFeeSatsPerByte) {
64
+ throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`);
65
+ }
66
+ }
67
+ }
68
+ checkFungibleTokenBurn() {
69
+ if (this.options.allowImplicitFungibleTokenBurn)
70
+ return;
71
+ const tokenInputAmounts = {};
72
+ const tokenOutputAmounts = {};
73
+ for (const input of this.inputs) {
74
+ if (input.token?.amount) {
75
+ tokenInputAmounts[input.token.category] = (tokenInputAmounts[input.token.category] || 0n) + input.token.amount;
76
+ }
77
+ }
78
+ for (const output of this.outputs) {
79
+ if (output.token?.amount) {
80
+ tokenOutputAmounts[output.token.category] = (tokenOutputAmounts[output.token.category] || 0n) + output.token.amount;
81
+ }
82
+ }
83
+ for (const [category, inputAmount] of Object.entries(tokenInputAmounts)) {
84
+ const outputAmount = tokenOutputAmounts[category] || 0n;
85
+ if (outputAmount < inputAmount) {
86
+ 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.`);
87
+ }
63
88
  }
64
89
  }
65
90
  buildLibauthTransaction() {
66
- this.checkMaxFee();
91
+ this.checkFungibleTokenBurn();
67
92
  const inputs = this.inputs.map((utxo) => ({
68
93
  outpointIndex: utxo.vout,
69
94
  outpointTransactionHash: hexToBin(utxo.txid),
@@ -83,6 +108,7 @@ export class TransactionBuilder {
83
108
  inputScripts.forEach((script, i) => {
84
109
  transaction.inputs[i].unlockingBytecode = script;
85
110
  });
111
+ this.checkMaxFee(transaction);
86
112
  return transaction;
87
113
  }
88
114
  build() {
@@ -90,10 +116,6 @@ export class TransactionBuilder {
90
116
  return binToHex(encodeTransaction(transaction));
91
117
  }
92
118
  debug() {
93
- // do not debug a pure P2PKH-spend transaction
94
- if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) {
95
- return {};
96
- }
97
119
  if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
98
120
  throw new Error('Cannot debug a transaction with custom unlocker');
99
121
  }
@@ -106,12 +128,42 @@ export class TransactionBuilder {
106
128
  }
107
129
  return debugLibauthTemplate(this.getLibauthTemplate(), this);
108
130
  }
109
- bitauthUri() {
131
+ getVmResourceUsage(verbose = false) {
132
+ // Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers
133
+ // cannot be debugged (and therefore cannot return VM resource usage)
134
+ const results = this.debug();
135
+ const vmResourceUsage = [];
136
+ const tableData = [];
137
+ const formatMetric = (value, total, withPercentage = false) => `${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`;
138
+ const formatNumber = (value) => value.toLocaleString('en');
139
+ const resultEntries = Object.entries(results);
140
+ for (const [index, input] of this.inputs.entries()) {
141
+ const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? [];
142
+ const metrics = result?.at(-1)?.metrics;
143
+ // Should not happen
144
+ if (!metrics)
145
+ throw new Error('VM resource could not be calculated');
146
+ vmResourceUsage.push(metrics);
147
+ tableData.push({
148
+ 'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input',
149
+ Ops: metrics.evaluatedInstructionCount,
150
+ 'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true),
151
+ SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount),
152
+ Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations),
153
+ });
154
+ }
155
+ if (verbose) {
156
+ console.log('VM Resource usage by inputs:');
157
+ console.table(tableData);
158
+ }
159
+ return vmResourceUsage;
160
+ }
161
+ getBitauthUri() {
110
162
  console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
111
163
  return getBitauthUri(this.getLibauthTemplate());
112
164
  }
113
165
  getLibauthTemplate() {
114
- return getLibauthTemplates(this);
166
+ return getLibauthTemplate(this);
115
167
  }
116
168
  async send(raw) {
117
169
  const tx = this.build();
@@ -126,7 +178,7 @@ export class TransactionBuilder {
126
178
  }
127
179
  catch (e) {
128
180
  const reason = e.error ?? e.message;
129
- throw new FailedTransactionError(reason);
181
+ throw new FailedTransactionError(reason, this.getBitauthUri());
130
182
  }
131
183
  }
132
184
  async getTxDetails(txid, raw) {
package/dist/debugging.js CHANGED
@@ -1,8 +1,24 @@
1
- import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBch2025, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
1
+ import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBch2023, createVirtualMachineBch2025, createVirtualMachineBch2026, createVirtualMachineBchSpec, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
2
2
  import { Op, PrimitiveType, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils';
3
3
  import { findLastIndex, toRegExp } from './utils.js';
4
4
  import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js';
5
- import { getBitauthUri } from './LibauthTemplate.js';
5
+ import { getBitauthUri } from './libauth-template/LibauthTemplate.js';
6
+ /* eslint-enable @typescript-eslint/indent */
7
+ const createVirtualMachine = (vmTarget) => {
8
+ switch (vmTarget) {
9
+ case 'BCH_2023_05':
10
+ return createVirtualMachineBch2023();
11
+ case 'BCH_2025_05':
12
+ return createVirtualMachineBch2025();
13
+ case 'BCH_2026_05':
14
+ return createVirtualMachineBch2026();
15
+ case 'BCH_SPEC':
16
+ // TODO: This typecast is shitty, but it's hard to fix
17
+ return createVirtualMachineBchSpec();
18
+ default:
19
+ throw new Error(`Debugging is not supported for the ${vmTarget} virtual machine.`);
20
+ }
21
+ };
6
22
  // debugs the template, optionally logging the execution data
7
23
  export const debugTemplate = (template, artifacts) => {
8
24
  // If a contract has the same name, but a different bytecode, then it is considered a name collision
@@ -14,13 +30,7 @@ export const debugTemplate = (template, artifacts) => {
14
30
  const unlockingScriptIds = Object.keys(template.scripts).filter((key) => 'unlocks' in template.scripts[key]);
15
31
  for (const unlockingScriptId of unlockingScriptIds) {
16
32
  const scenarioIds = template.scripts[unlockingScriptId].passes ?? [];
17
- // There are no scenarios defined for P2PKH placeholder scripts, so we skip them
18
- if (scenarioIds.length === 0)
19
- continue;
20
33
  const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName));
21
- if (!matchingArtifact) {
22
- throw new Error(`No artifact found for unlocking script ${unlockingScriptId}`);
23
- }
24
34
  for (const scenarioId of scenarioIds) {
25
35
  results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId);
26
36
  }
@@ -38,10 +48,14 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
38
48
  // https://libauth.org/types/AuthenticationProgramStateControlStack.html
39
49
  const executedDebugSteps = lockingScriptDebugResult
40
50
  .filter((debugStep) => debugStep.controlStack.every(item => item === true));
41
- const executedLogs = (artifact.debug?.logs ?? [])
42
- .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip));
43
- for (const log of executedLogs) {
44
- logConsoleLogStatement(log, executedDebugSteps, artifact);
51
+ // P2PKH inputs do not have an artifact, so we skip the console.log handling
52
+ if (artifact) {
53
+ const executedLogs = (artifact.debug?.logs ?? [])
54
+ .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip));
55
+ for (const log of executedLogs) {
56
+ const inputIndex = extractInputIndexFromScenario(scenarioId);
57
+ logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex, vm);
58
+ }
45
59
  }
46
60
  const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1];
47
61
  // If an error is present in the last step, that means a require statement in the middle of the function failed
@@ -59,9 +73,15 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
59
73
  // Note that we do NOT use this adjusted IP when passing the failing IP into the FailedRequireError.
60
74
  const isNullFail = lastExecutedDebugStep.error.includes(AuthenticationErrorCommon.nonNullSignatureFailure);
61
75
  const requireStatementIp = failingIp + (isNullFail && isSignatureCheckWithoutVerify(failingInstruction) ? 1 : 0);
76
+ const { program: { inputIndex }, error } = lastExecutedDebugStep;
77
+ // If there is no artifact, this is a P2PKH debug error, error can occur when final CHECKSIG fails with NULLFAIL or when
78
+ // public key does not match pkh in EQUALVERIFY
79
+ // Note: due to P2PKHUnlocker implementation, the CHECKSIG cannot fail in practice, only the EQUALVERIFY can fail
80
+ if (!artifact) {
81
+ throw new FailedTransactionError(error, getBitauthUri(template));
82
+ }
62
83
  const requireStatement = (artifact.debug?.requires ?? [])
63
84
  .find((statement) => statement.ip === requireStatementIp);
64
- const { program: { inputIndex }, error } = lastExecutedDebugStep;
65
85
  if (requireStatement) {
66
86
  // Note that we use failingIp here rather than requireStatementIp, see comment above
67
87
  throw new FailedRequireError(artifact, failingIp, requireStatement, inputIndex, getBitauthUri(template), error);
@@ -81,9 +101,14 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
81
101
  // logDebugSteps(executedDebugSteps, lastExecutedDebugStep.instructions);
82
102
  // console.warn('message', finalExecutedVerifyIp);
83
103
  // console.warn(artifact.debug?.requires);
104
+ const { program: { inputIndex } } = lastExecutedDebugStep;
105
+ // If there is no artifact, this is a P2PKH debug error, final verify can only occur when final CHECKSIG failed
106
+ // Note: due to P2PKHUnlocker implementation, this cannot happen in practice
107
+ if (!artifact) {
108
+ throw new FailedTransactionError(evaluationResult, getBitauthUri(template));
109
+ }
84
110
  const requireStatement = (artifact.debug?.requires ?? [])
85
111
  .find((message) => message.ip === finalExecutedVerifyIp);
86
- const { program: { inputIndex } } = lastExecutedDebugStep;
87
112
  if (requireStatement) {
88
113
  throw new FailedRequireError(artifact, sourcemapInstructionPointer, requireStatement, inputIndex, getBitauthUri(template));
89
114
  }
@@ -91,10 +116,17 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
91
116
  }
92
117
  return fullDebugSteps;
93
118
  };
119
+ // Note: this relies on the naming convention that the scenario ID is of the form <name>_input<index>_evaluate
120
+ const extractInputIndexFromScenario = (scenarioId) => {
121
+ const match = scenarioId.match(/_input(\d+)_/);
122
+ if (!match)
123
+ throw new Error(`Invalid scenario ID: ${scenarioId}`);
124
+ return parseInt(match[1]);
125
+ };
94
126
  // internal util. instantiates the virtual machine and compiles the template into a program
95
127
  const createProgram = (template, unlockingScriptId, scenarioId) => {
96
128
  const configuration = walletTemplateToCompilerConfiguration(template);
97
- const vm = createVirtualMachineBch2025();
129
+ const vm = createVirtualMachine(template.supported[0]);
98
130
  const compiler = createCompiler(configuration);
99
131
  if (!template.scripts[unlockingScriptId]) {
100
132
  throw new Error(`No unlock script found in template for ID ${unlockingScriptId}`);
@@ -115,18 +147,18 @@ const createProgram = (template, unlockingScriptId, scenarioId) => {
115
147
  }
116
148
  return { vm, program: scenarioGeneration.scenario.program };
117
149
  };
118
- const logConsoleLogStatement = (log, debugSteps, artifact) => {
150
+ const logConsoleLogStatement = (log, debugSteps, artifact, inputIndex, vm) => {
119
151
  let line = `${artifact.contractName}.cash:${log.line}`;
120
152
  const decodedData = log.data.map((element) => {
121
153
  if (typeof element === 'string')
122
154
  return element;
123
155
  const debugStep = debugSteps.find((step) => step.ip === element.ip);
124
- const transformedDebugStep = applyStackItemTransformations(element, debugStep);
156
+ const transformedDebugStep = applyStackItemTransformations(element, debugStep, vm);
125
157
  return decodeStackItem(element, transformedDebugStep.stack);
126
158
  });
127
- console.log(`${line} ${decodedData.join(' ')}`);
159
+ console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`);
128
160
  };
129
- const applyStackItemTransformations = (element, debugStep) => {
161
+ const applyStackItemTransformations = (element, debugStep, vm) => {
130
162
  if (!element.transformations)
131
163
  return debugStep;
132
164
  const transformationsBytecode = asmToBytecode(element.transformations);
@@ -142,8 +174,9 @@ const applyStackItemTransformations = (element, debugStep) => {
142
174
  instructions: transformationsAuthenticationInstructions,
143
175
  signedMessages: [],
144
176
  program: { ...debugStep.program },
177
+ functionTable: debugStep.functionTable ?? {},
178
+ functionCount: debugStep.functionCount ?? 0,
145
179
  };
146
- const vm = createVirtualMachineBch2025();
147
180
  const transformationsEndState = vm.stateEvaluate(transformationsStartState);
148
181
  return transformationsEndState;
149
182
  };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export { default as SignatureTemplate } from './SignatureTemplate.js';
2
- export { Contract, type ContractFunction } from './Contract.js';
3
- export { Transaction } from './Transaction.js';
2
+ export { Contract } from './Contract.js';
4
3
  export { TransactionBuilder } from './TransactionBuilder.js';
5
4
  export { type ConstructorArgument, type FunctionArgument, type EncodedConstructorArgument, type EncodedFunctionArgument, encodeFunctionArgument, } from './Argument.js';
6
5
  export type { Artifact, AbiFunction, AbiInput } from '@cashscript/utils';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export { default as SignatureTemplate } from './SignatureTemplate.js';
2
2
  export { Contract } from './Contract.js';
3
- export { Transaction } from './Transaction.js';
4
3
  export { TransactionBuilder } from './TransactionBuilder.js';
5
4
  export { encodeFunctionArgument, } from './Argument.js';
6
5
  export * as utils from '@cashscript/utils';
@@ -1,4 +1,4 @@
1
- import { type Transaction } from '@bitauth/libauth';
1
+ import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth';
2
2
  import type { NetworkProvider } from './network/index.js';
3
3
  import type SignatureTemplate from './SignatureTemplate.js';
4
4
  import { Contract } from './Contract.js';
@@ -47,10 +47,6 @@ export declare function isContractUnlocker(unlocker: Unlocker): unlocker is Cont
47
47
  export declare function isP2PKHUnlocker(unlocker: Unlocker): unlocker is P2PKHUnlocker;
48
48
  export declare function isStandardUnlocker(unlocker: Unlocker): unlocker is StandardUnlocker;
49
49
  export declare function isPlaceholderUnlocker(unlocker: Unlocker): unlocker is PlaceholderP2PKHUnlocker;
50
- export interface UtxoP2PKH extends Utxo {
51
- template: SignatureTemplate;
52
- }
53
- export declare function isUtxoP2PKH(utxo: Utxo): utxo is UtxoP2PKH;
54
50
  export interface Recipient {
55
51
  to: string;
56
52
  amount: bigint;
@@ -107,12 +103,20 @@ export declare const Network: {
107
103
  REGTEST: "regtest";
108
104
  };
109
105
  export type Network = (typeof Network)[keyof typeof Network];
106
+ export declare const VmTarget: {
107
+ BCH_2023_05: "BCH_2023_05";
108
+ BCH_2025_05: "BCH_2025_05";
109
+ BCH_2026_05: "BCH_2026_05";
110
+ BCH_SPEC: "BCH_SPEC";
111
+ };
112
+ export type VmTarget = (typeof VmTarget)[keyof typeof VmTarget];
110
113
  export interface TransactionDetails extends Transaction {
111
114
  txid: string;
112
115
  hex: string;
113
116
  }
114
117
  export interface ContractOptions {
115
- provider?: NetworkProvider;
118
+ provider: NetworkProvider;
116
119
  addressType?: AddressType;
117
120
  }
118
121
  export type AddressType = 'p2sh20' | 'p2sh32';
122
+ export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics'];
@@ -16,9 +16,6 @@ export function isStandardUnlocker(unlocker) {
16
16
  export function isPlaceholderUnlocker(unlocker) {
17
17
  return 'placeholder' in unlocker;
18
18
  }
19
- export function isUtxoP2PKH(utxo) {
20
- return 'template' in utxo;
21
- }
22
19
  export var SignatureAlgorithm;
23
20
  (function (SignatureAlgorithm) {
24
21
  SignatureAlgorithm[SignatureAlgorithm["ECDSA"] = 0] = "ECDSA";
@@ -43,4 +40,10 @@ export const Network = {
43
40
  MOCKNET: literal('mocknet'),
44
41
  REGTEST: literal('regtest'),
45
42
  };
43
+ export const VmTarget = {
44
+ BCH_2023_05: literal('BCH_2023_05'),
45
+ BCH_2025_05: literal('BCH_2025_05'),
46
+ BCH_2026_05: literal('BCH_2026_05'),
47
+ BCH_SPEC: literal('BCH_SPEC'),
48
+ };
46
49
  //# sourceMappingURL=interfaces.js.map