cashscript 0.10.5 → 0.11.0-next.1

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
@@ -1,6 +1,6 @@
1
1
  # CashScript
2
2
 
3
- ![Build Status](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml/badge.svg)
3
+ [![Build Status](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml/badge.svg)](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml)
4
4
  [![Coverage Status](https://img.shields.io/codecov/c/github/CashScript/cashscript.svg)](https://codecov.io/gh/CashScript/cashscript/)
5
5
  [![NPM Version](https://img.shields.io/npm/v/cashscript.svg)](https://www.npmjs.com/package/cashscript)
6
6
  [![NPM Monthly Downloads](https://img.shields.io/npm/dm/cashscript.svg)](https://www.npmjs.com/package/cashscript)
package/dist/Contract.js CHANGED
@@ -5,16 +5,20 @@ import { encodeFunctionArgument, encodeConstructorArguments, encodeFunctionArgum
5
5
  import { addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress, } from './utils.js';
6
6
  import SignatureTemplate from './SignatureTemplate.js';
7
7
  import { ElectrumNetworkProvider } from './network/index.js';
8
+ import semver from 'semver';
8
9
  export class Contract {
9
10
  constructor(artifact, constructorArgs, options) {
10
11
  this.artifact = artifact;
11
12
  this.options = options;
12
13
  this.provider = this.options?.provider ?? new ElectrumNetworkProvider();
13
14
  this.addressType = this.options?.addressType ?? 'p2sh32';
14
- const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName'];
15
+ const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName', 'compiler'];
15
16
  if (!expectedProperties.every((property) => property in artifact)) {
16
17
  throw new Error('Invalid or incomplete artifact provided');
17
18
  }
19
+ if (!semver.satisfies(artifact.compiler.version, '>=0.7.0', { includePrerelease: true })) {
20
+ throw new Error(`Artifact compiled with unsupported compiler version: ${artifact.compiler.version}, required >=0.7.0`);
21
+ }
18
22
  if (artifact.constructorInputs.length !== constructorArgs.length) {
19
23
  throw new Error(`Incorrect number of arguments passed to ${artifact.contractName} constructor. Expected ${artifact.constructorInputs.length} arguments (${artifact.constructorInputs.map((input) => input.type)}) but got ${constructorArgs.length}`);
20
24
  }
@@ -83,26 +87,19 @@ export class Contract {
83
87
  const encodedArgs = args
84
88
  .map((arg, i) => encodeFunctionArgument(arg, abiFunction.inputs[i].type));
85
89
  const generateUnlockingBytecode = ({ transaction, sourceOutputs, inputIndex }) => {
86
- // TODO: Remove old-style covenant code for v1.0 release
87
- let covenantHashType = -1;
88
90
  const completeArgs = encodedArgs.map((arg) => {
89
91
  if (!(arg instanceof SignatureTemplate))
90
92
  return arg;
91
- // First signature is used for sighash preimage (maybe not the best way)
92
- if (covenantHashType < 0)
93
- covenantHashType = arg.getHashType();
93
+ // Generate transaction signature from SignatureTemplate
94
94
  const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.getHashType());
95
95
  const sighash = hash256(preimage);
96
96
  return arg.generateSignature(sighash);
97
97
  });
98
- const preimage = abiFunction.covenant
99
- ? createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, covenantHashType)
100
- : undefined;
101
- const unlockingBytecode = createInputScript(this.redeemScript, completeArgs, selector, preimage);
98
+ const unlockingBytecode = createInputScript(this.redeemScript, completeArgs, selector);
102
99
  return unlockingBytecode;
103
100
  };
104
101
  const generateLockingBytecode = () => addressToLockScript(this.address);
105
- return { generateUnlockingBytecode, generateLockingBytecode };
102
+ return { generateUnlockingBytecode, generateLockingBytecode, contract: this, params: args, abiFunction };
106
103
  };
107
104
  }
108
105
  }
package/dist/Errors.d.ts CHANGED
@@ -2,9 +2,15 @@ import { Artifact, RequireStatement, Type } from '@cashscript/utils';
2
2
  export declare class TypeError extends Error {
3
3
  constructor(actual: string, expected: Type);
4
4
  }
5
+ export declare class UndefinedInputError extends Error {
6
+ constructor();
7
+ }
5
8
  export declare class OutputSatoshisTooSmallError extends Error {
6
9
  constructor(satoshis: bigint, minimumAmount: bigint);
7
10
  }
11
+ export declare class OutputTokenAmountTooSmallError extends Error {
12
+ constructor(amount: bigint);
13
+ }
8
14
  export declare class TokensToNonTokenAddressError extends Error {
9
15
  constructor(address: string);
10
16
  }
package/dist/Errors.js CHANGED
@@ -4,11 +4,21 @@ export class TypeError extends Error {
4
4
  super(`Found type '${actual}' where type '${expected.toString()}' was expected`);
5
5
  }
6
6
  }
7
+ export class UndefinedInputError extends Error {
8
+ constructor() {
9
+ super('Input is undefined');
10
+ }
11
+ }
7
12
  export class OutputSatoshisTooSmallError extends Error {
8
13
  constructor(satoshis, minimumAmount) {
9
14
  super(`Tried to add an output with ${satoshis} satoshis, which is less than the required minimum for this output-type (${minimumAmount})`);
10
15
  }
11
16
  }
17
+ export class OutputTokenAmountTooSmallError extends Error {
18
+ constructor(amount) {
19
+ super(`Tried to add an output with ${amount} tokens, which is invalid`);
20
+ }
21
+ }
12
22
  export class TokensToNonTokenAddressError extends Error {
13
23
  constructor(address) {
14
24
  super(`Tried to send tokens to an address without token support, ${address}.`);
@@ -1,11 +1,25 @@
1
- import { WalletTemplate } from '@bitauth/libauth';
1
+ import { AbiInput, Artifact } from '@cashscript/utils';
2
+ import { WalletTemplate, WalletTemplateScenarioBytecode } from '@bitauth/libauth';
3
+ import { Utxo, TokenDetails, LibauthTokenDetails, Output, SignatureAlgorithm, HashType } from './interfaces.js';
2
4
  import { Transaction } from './Transaction.js';
5
+ import { EncodedFunctionArgument } from './Argument.js';
6
+ import { Contract } from './Contract.js';
3
7
  interface BuildTemplateOptions {
4
8
  transaction: Transaction;
5
9
  transactionHex?: string;
6
10
  }
7
11
  export declare const buildTemplate: ({ transaction, transactionHex, }: BuildTemplateOptions) => Promise<WalletTemplate>;
8
12
  export declare const getBitauthUri: (template: WalletTemplate) => string;
13
+ export declare const generateTemplateScenarioTransactionOutputLockingBytecode: (csOutput: Output, contract: Contract) => string | {};
14
+ export declare const generateTemplateScenarioBytecode: (input: Utxo, p2pkhScriptName: string, placeholderKeyName: string, insertSlot?: boolean) => WalletTemplateScenarioBytecode | ["slot"];
15
+ export declare const generateTemplateScenarioParametersValues: (types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[]) => Record<string, string>;
16
+ export declare const addHexPrefixExceptEmpty: (value: string) => string;
17
+ export declare const generateTemplateScenarioKeys: (types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[]) => Record<string, string>;
18
+ export declare const formatParametersForDebugging: (types: readonly AbiInput[], args: EncodedFunctionArgument[]) => string;
19
+ export declare const getSignatureAlgorithmName: (signatureAlgorithm: SignatureAlgorithm) => string;
20
+ export declare const getHashTypeName: (hashType: HashType) => string;
21
+ export declare const formatBytecodeForDebugging: (artifact: Artifact) => string;
22
+ export declare const serialiseTokenDetails: (token?: TokenDetails | LibauthTokenDetails) => LibauthTemplateTokenDetails | undefined;
9
23
  export interface LibauthTemplateTokenDetails {
10
24
  amount: string;
11
25
  category: string;
@@ -11,7 +11,7 @@ export const buildTemplate = async ({ transaction, transactionHex = undefined, /
11
11
  const template = {
12
12
  $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json',
13
13
  description: 'Imported from cashscript',
14
- name: contract.artifact.contractName,
14
+ name: 'CashScript Generated Debugging Template',
15
15
  supported: ['BCH_2023_05'],
16
16
  version: 0,
17
17
  entities: generateTemplateEntities(contract.artifact, transaction.abiFunction, transaction.encodedFunctionArgs),
@@ -28,9 +28,9 @@ export const buildTemplate = async ({ transaction, transactionHex = undefined, /
28
28
  const signatureAlgorithmName = getSignatureAlgorithmName(input.template.getSignatureAlgorithm());
29
29
  const hashtypeName = getHashTypeName(input.template.getHashType(false));
30
30
  const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`;
31
- template.entities.parameters.scripts.push(lockScriptName, unlockScriptName);
32
- template.entities.parameters.variables = {
33
- ...template.entities.parameters.variables,
31
+ template.entities[snakeCase(contract.name + 'Parameters')].scripts.push(lockScriptName, unlockScriptName);
32
+ template.entities[snakeCase(contract.name + 'Parameters')].variables = {
33
+ ...template.entities[snakeCase(contract.name + 'Parameters')].variables,
34
34
  [placeholderKeyName]: {
35
35
  description: placeholderKeyName,
36
36
  name: placeholderKeyName,
@@ -75,12 +75,12 @@ const generateTemplateEntities = (artifact, abiFunction, encodedFunctionArgs) =>
75
75
  },
76
76
  ])));
77
77
  const entities = {
78
- parameters: {
78
+ [snakeCase(artifact.contractName + 'Parameters')]: {
79
79
  description: 'Contract creation and function parameters',
80
- name: 'parameters',
80
+ name: snakeCase(artifact.contractName + 'Parameters'),
81
81
  scripts: [
82
- 'lock',
83
- 'unlock_lock',
82
+ snakeCase(artifact.contractName + '_lock'),
83
+ snakeCase(artifact.contractName + '_unlock'),
84
84
  ],
85
85
  variables: {
86
86
  ...functionParameters,
@@ -90,7 +90,7 @@ const generateTemplateEntities = (artifact, abiFunction, encodedFunctionArgs) =>
90
90
  };
91
91
  // function_index is a special variable that indicates the function to execute
92
92
  if (artifact.abi.length > 1) {
93
- entities.parameters.variables.function_index = {
93
+ entities[snakeCase(artifact.contractName + 'Parameters')].variables.function_index = {
94
94
  description: 'Script function index to execute',
95
95
  name: 'function_index',
96
96
  type: 'WalletData',
@@ -101,14 +101,14 @@ const generateTemplateEntities = (artifact, abiFunction, encodedFunctionArgs) =>
101
101
  const generateTemplateScripts = (artifact, addressType, abiFunction, encodedFunctionArgs, encodedConstructorArgs) => {
102
102
  // definition of locking scripts and unlocking scripts with their respective bytecode
103
103
  return {
104
- unlock_lock: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs),
105
- lock: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs),
104
+ [snakeCase(artifact.contractName + '_unlock')]: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs),
105
+ [snakeCase(artifact.contractName + '_lock')]: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs),
106
106
  };
107
107
  };
108
108
  const generateTemplateLockScript = (artifact, addressType, constructorArguments) => {
109
109
  return {
110
110
  lockingType: addressType,
111
- name: 'lock',
111
+ name: snakeCase(artifact.contractName + '_lock'),
112
112
  script: [
113
113
  `// "${artifact.contractName}" contract constructor parameters`,
114
114
  formatParametersForDebugging(artifact.constructorInputs, constructorArguments),
@@ -125,15 +125,15 @@ const generateTemplateUnlockScript = (artifact, abiFunction, encodedFunctionArgs
125
125
  : [];
126
126
  return {
127
127
  // this unlocking script must pass our only scenario
128
- passes: ['evaluate_function'],
129
- name: 'unlock',
128
+ passes: [snakeCase(artifact.contractName + 'Evaluate')],
129
+ name: snakeCase(artifact.contractName + '_unlock'),
130
130
  script: [
131
131
  `// "${abiFunction.name}" function parameters`,
132
132
  formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs),
133
133
  '',
134
134
  ...functionIndexString,
135
135
  ].join('\n'),
136
- unlocks: 'lock',
136
+ unlocks: snakeCase(artifact.contractName + '_lock'),
137
137
  };
138
138
  };
139
139
  const generateTemplateScenarios = (contract, transaction, transactionHex, artifact, abiFunction, encodedFunctionArgs, encodedConstructorArgs) => {
@@ -142,8 +142,8 @@ const generateTemplateScenarios = (contract, transaction, transactionHex, artifa
142
142
  throw Error(libauthTransaction);
143
143
  const scenarios = {
144
144
  // single scenario to spend out transaction under test given the CashScript parameters provided
145
- evaluate_function: {
146
- name: 'Evaluate',
145
+ [snakeCase(artifact.contractName + 'Evaluate')]: {
146
+ name: snakeCase(artifact.contractName + 'Evaluate'),
147
147
  description: 'An example evaluation where this script execution passes.',
148
148
  data: {
149
149
  // encode values for the variables defined above in `entities` property
@@ -163,7 +163,7 @@ const generateTemplateScenarios = (contract, transaction, transactionHex, artifa
163
163
  };
164
164
  if (artifact.abi.length > 1) {
165
165
  const functionIndex = artifact.abi.findIndex((func) => func.name === transaction.abiFunction.name);
166
- scenarios.evaluate_function.data.bytecode.function_index = functionIndex.toString();
166
+ scenarios[snakeCase(artifact.contractName + 'Evaluate')].data.bytecode.function_index = functionIndex.toString();
167
167
  }
168
168
  return scenarios;
169
169
  };
@@ -190,13 +190,27 @@ const generateTemplateScenarioTransaction = (contract, libauthTransaction, csTra
190
190
  const version = libauthTransaction.version;
191
191
  return { inputs, locktime, outputs, version };
192
192
  };
193
- const generateTemplateScenarioTransactionOutputLockingBytecode = (csOutput, contract) => {
193
+ export const generateTemplateScenarioTransactionOutputLockingBytecode = (csOutput, contract) => {
194
194
  if (csOutput.to instanceof Uint8Array)
195
195
  return binToHex(csOutput.to);
196
196
  if ([contract.address, contract.tokenAddress].includes(csOutput.to))
197
197
  return {};
198
198
  return binToHex(addressToLockScript(csOutput.to));
199
199
  };
200
+ /**
201
+ * Generates source outputs for a BitAuth template scenario
202
+ *
203
+ * @param csTransaction - The CashScript transaction to generate source outputs for
204
+ * @returns An array of BitAuth template scenario outputs with locking scripts and values
205
+ *
206
+ * For each input in the transaction:
207
+ * - Generates appropriate locking bytecode (P2PKH or contract)
208
+ * - Includes the input value in satoshis
209
+ * - Includes any token details if present
210
+ *
211
+ * The slotIndex tracks which input is the contract input vs P2PKH inputs
212
+ * to properly generate the locking scripts.
213
+ */
200
214
  const generateTemplateScenarioSourceOutputs = (csTransaction) => {
201
215
  const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input));
202
216
  return csTransaction.inputs.map((input, index) => {
@@ -208,7 +222,7 @@ const generateTemplateScenarioSourceOutputs = (csTransaction) => {
208
222
  });
209
223
  };
210
224
  // Used for generating the locking / unlocking bytecode for source outputs and inputs
211
- const generateTemplateScenarioBytecode = (input, p2pkhScriptName, placeholderKeyName, insertSlot) => {
225
+ export const generateTemplateScenarioBytecode = (input, p2pkhScriptName, placeholderKeyName, insertSlot) => {
212
226
  if (isUtxoP2PKH(input)) {
213
227
  return {
214
228
  script: p2pkhScriptName,
@@ -223,26 +237,29 @@ const generateTemplateScenarioBytecode = (input, p2pkhScriptName, placeholderKey
223
237
  }
224
238
  return insertSlot ? ['slot'] : {};
225
239
  };
226
- const generateTemplateScenarioParametersValues = (types, encodedArgs) => {
240
+ export const generateTemplateScenarioParametersValues = (types, encodedArgs) => {
227
241
  const typesAndArguments = zip(types, encodedArgs);
228
242
  const entries = typesAndArguments
229
243
  // SignatureTemplates are handled by the 'keys' object in the scenario
230
244
  .filter(([, arg]) => !(arg instanceof SignatureTemplate))
231
245
  .map(([input, arg]) => {
232
246
  const encodedArgumentHex = binToHex(arg);
233
- const prefixedEncodedArgument = encodedArgumentHex.length > 0 ? `0x${encodedArgumentHex}` : '';
247
+ const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex);
234
248
  return [snakeCase(input.name), prefixedEncodedArgument];
235
249
  });
236
250
  return Object.fromEntries(entries);
237
251
  };
238
- const generateTemplateScenarioKeys = (types, encodedArgs) => {
252
+ export const addHexPrefixExceptEmpty = (value) => {
253
+ return value.length > 0 ? `0x${value}` : '';
254
+ };
255
+ export const generateTemplateScenarioKeys = (types, encodedArgs) => {
239
256
  const typesAndArguments = zip(types, encodedArgs);
240
257
  const entries = typesAndArguments
241
258
  .filter(([, arg]) => arg instanceof SignatureTemplate)
242
259
  .map(([input, arg]) => [snakeCase(input.name), binToHex(arg.privateKey)]);
243
260
  return Object.fromEntries(entries);
244
261
  };
245
- const formatParametersForDebugging = (types, args) => {
262
+ export const formatParametersForDebugging = (types, args) => {
246
263
  if (types.length === 0)
247
264
  return '// none';
248
265
  // We reverse the arguments because the order of the arguments in the bytecode is reversed
@@ -259,14 +276,14 @@ const formatParametersForDebugging = (types, args) => {
259
276
  return `<${snakeCase(input.name)}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`;
260
277
  }).join('\n');
261
278
  };
262
- const getSignatureAlgorithmName = (signatureAlgorithm) => {
279
+ export const getSignatureAlgorithmName = (signatureAlgorithm) => {
263
280
  const signatureAlgorithmNames = {
264
281
  [SignatureAlgorithm.SCHNORR]: 'schnorr_signature',
265
282
  [SignatureAlgorithm.ECDSA]: 'ecdsa_signature',
266
283
  };
267
284
  return signatureAlgorithmNames[signatureAlgorithm];
268
285
  };
269
- const getHashTypeName = (hashType) => {
286
+ export const getHashTypeName = (hashType) => {
270
287
  const hashtypeNames = {
271
288
  [HashType.SIGHASH_ALL]: 'all_outputs',
272
289
  [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
@@ -283,7 +300,7 @@ const getHashTypeName = (hashType) => {
283
300
  };
284
301
  return hashtypeNames[hashType];
285
302
  };
286
- const formatBytecodeForDebugging = (artifact) => {
303
+ export const formatBytecodeForDebugging = (artifact) => {
287
304
  if (!artifact.debug) {
288
305
  return artifact.bytecode
289
306
  .split(' ')
@@ -292,7 +309,7 @@ const formatBytecodeForDebugging = (artifact) => {
292
309
  }
293
310
  return formatBitAuthScript(bytecodeToScript(hexToBin(artifact.debug.bytecode)), artifact.debug.sourceMap, artifact.source);
294
311
  };
295
- const serialiseTokenDetails = (token) => {
312
+ export const serialiseTokenDetails = (token) => {
296
313
  if (!token)
297
314
  return undefined;
298
315
  return {
@@ -46,6 +46,7 @@ export default class SignatureTemplate {
46
46
  const unlockingBytecode = scriptToBytecode([signature, publicKey]);
47
47
  return unlockingBytecode;
48
48
  },
49
+ template: this,
49
50
  };
50
51
  }
51
52
  }
@@ -3,7 +3,7 @@ import { AbiFunction } from '@cashscript/utils';
3
3
  import { Utxo, Output, Recipient, TokenDetails, TransactionDetails, Unlocker } from './interfaces.js';
4
4
  import SignatureTemplate from './SignatureTemplate.js';
5
5
  import { Contract } from './Contract.js';
6
- import { DebugResult } from './debugging.js';
6
+ import { DebugResults } from './debugging.js';
7
7
  import { EncodedFunctionArgument } from './Argument.js';
8
8
  export declare class Transaction {
9
9
  contract: Contract;
@@ -37,7 +37,7 @@ export declare class Transaction {
37
37
  build(): Promise<string>;
38
38
  send(): Promise<TransactionDetails>;
39
39
  send(raw: true): Promise<string>;
40
- debug(): Promise<DebugResult>;
40
+ debug(): Promise<DebugResults>;
41
41
  bitauthUri(): Promise<string>;
42
42
  getLibauthTemplate(): Promise<WalletTemplate>;
43
43
  private getTxDetails;
@@ -1,9 +1,9 @@
1
1
  import { hexToBin, decodeTransaction, } from '@bitauth/libauth';
2
2
  import delay from 'delay';
3
- import { encodeBip68, placeholder, scriptToBytecode, } from '@cashscript/utils';
3
+ import { encodeBip68, placeholder, } from '@cashscript/utils';
4
4
  import deepEqual from 'fast-deep-equal';
5
5
  import { isUtxoP2PKH, SignatureAlgorithm, } from './interfaces.js';
6
- import { createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, getPreimageSize, validateOutput, utxoComparator, calculateDust, getOutputSize, utxoTokenComparator, } from './utils.js';
6
+ import { createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, validateOutput, utxoComparator, calculateDust, getOutputSize, utxoTokenComparator, } from './utils.js';
7
7
  import SignatureTemplate from './SignatureTemplate.js';
8
8
  import { P2PKH_INPUT_SIZE } from './constants.js';
9
9
  import { TransactionBuilder } from './TransactionBuilder.js';
@@ -101,10 +101,7 @@ export class Transaction {
101
101
  async send(raw) {
102
102
  const tx = await this.build();
103
103
  // Debug the transaction locally before sending so any errors are caught early
104
- // Libauth debugging does not work with old-style covenants
105
- if (!this.abiFunction.covenant) {
106
- await this.debug();
107
- }
104
+ await this.debug();
108
105
  try {
109
106
  const txid = await this.contract.provider.sendRawTransaction(tx);
110
107
  return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid);
@@ -120,7 +117,7 @@ export class Transaction {
120
117
  console.warn('No debug information found in artifact. Recompile with cashc version 0.10.0 or newer to get better debugging information.');
121
118
  }
122
119
  const template = await this.getLibauthTemplate();
123
- return debugTemplate(template, this.contract.artifact);
120
+ return debugTemplate(template, [this.contract.artifact]);
124
121
  }
125
122
  async bitauthUri() {
126
123
  const template = await this.getLibauthTemplate();
@@ -259,13 +256,8 @@ export class Transaction {
259
256
  // (see https://transactionfee.info/charts/bitcoin-script-ecdsa-length/)
260
257
  return placeholder(73);
261
258
  });
262
- // Create a placeholder preimage of the correct size
263
- const placeholderPreimage = this.abiFunction.covenant
264
- ? placeholder(getPreimageSize(scriptToBytecode(this.contract.redeemScript)))
265
- : undefined;
266
- // Create a placeholder input script for size calculation using the placeholder
267
- // arguments and correctly sized placeholder preimage
268
- const placeholderScript = createInputScript(this.contract.redeemScript, placeholderArgs, this.selector, placeholderPreimage);
259
+ // Create a placeholder input script for size calculation using the placeholder arguments
260
+ const placeholderScript = createInputScript(this.contract.redeemScript, placeholderArgs, this.selector);
269
261
  // Add one extra byte per input to over-estimate tx-in count
270
262
  const contractInputSize = getInputSize(placeholderScript) + 1;
271
263
  // Note that we use the addPrecision function to add "decimal points" to BigInt numbers
@@ -1,5 +1,7 @@
1
+ import { Transaction as LibauthTransaction, WalletTemplate } from '@bitauth/libauth';
1
2
  import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions } from './interfaces.js';
2
3
  import { NetworkProvider } from './network/index.js';
4
+ import { DebugResults } from './debugging.js';
3
5
  export interface TransactionBuilderOptions {
4
6
  provider: NetworkProvider;
5
7
  }
@@ -7,8 +9,8 @@ export declare class TransactionBuilder {
7
9
  provider: NetworkProvider;
8
10
  inputs: UnlockableUtxo[];
9
11
  outputs: Output[];
10
- private locktime;
11
- private maxFee?;
12
+ locktime: number;
13
+ maxFee?: bigint;
12
14
  constructor(options: TransactionBuilderOptions);
13
15
  addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this;
14
16
  addInputs(utxos: Utxo[], unlocker: Unlocker, options?: InputOptions): this;
@@ -19,7 +21,11 @@ export declare class TransactionBuilder {
19
21
  setLocktime(locktime: number): this;
20
22
  setMaxFee(maxFee: bigint): this;
21
23
  private checkMaxFee;
24
+ buildLibauthTransaction(): LibauthTransaction;
22
25
  build(): string;
26
+ debug(): Promise<DebugResults>;
27
+ bitauthUri(): string;
28
+ getLibauthTemplate(): WalletTemplate;
23
29
  send(): Promise<TransactionDetails>;
24
30
  send(raw: true): Promise<string>;
25
31
  private getTxDetails;
@@ -1,20 +1,23 @@
1
1
  import { binToHex, decodeTransaction, encodeTransaction, hexToBin, } from '@bitauth/libauth';
2
2
  import delay from 'delay';
3
3
  import { isUnlockableUtxo, } from './interfaces.js';
4
- import { cashScriptOutputToLibauthOutput, createOpReturnOutput, validateOutput, } from './utils.js';
4
+ import { cashScriptOutputToLibauthOutput, createOpReturnOutput, validateInput, validateOutput, } from './utils.js';
5
5
  import { FailedTransactionError } from './Errors.js';
6
+ import { getBitauthUri } from './LibauthTemplate.js';
7
+ import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js';
6
8
  const DEFAULT_SEQUENCE = 0xfffffffe;
7
9
  export class TransactionBuilder {
8
10
  constructor(options) {
9
11
  this.inputs = [];
10
12
  this.outputs = [];
13
+ this.locktime = 0;
11
14
  this.provider = options.provider;
12
15
  }
13
16
  addInput(utxo, unlocker, options) {
14
- this.inputs.push({ ...utxo, unlocker, options });
15
- return this;
17
+ return this.addInputs([utxo], unlocker, options);
16
18
  }
17
19
  addInputs(utxos, unlocker, options) {
20
+ utxos.forEach(validateInput);
18
21
  if ((!unlocker && utxos.some((utxo) => !isUnlockableUtxo(utxo)))
19
22
  || (unlocker && utxos.some((utxo) => isUnlockableUtxo(utxo)))) {
20
23
  throw new Error('Either all UTXOs must have an individual unlocker specified, or no UTXOs must have an individual unlocker specified and a shared unlocker must be provided');
@@ -57,7 +60,7 @@ export class TransactionBuilder {
57
60
  throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.maxFee}`);
58
61
  }
59
62
  }
60
- build() {
63
+ buildLibauthTransaction() {
61
64
  this.checkMaxFee();
62
65
  const inputs = this.inputs.map((utxo) => ({
63
66
  outpointIndex: utxo.vout,
@@ -85,10 +88,27 @@ export class TransactionBuilder {
85
88
  inputScripts.forEach((script, i) => {
86
89
  transaction.inputs[i].unlockingBytecode = script;
87
90
  });
91
+ return transaction;
92
+ }
93
+ build() {
94
+ const transaction = this.buildLibauthTransaction();
88
95
  return binToHex(encodeTransaction(transaction));
89
96
  }
97
+ // method to debug the transaction with libauth VM, throws upon evaluation error
98
+ // TODO: Remove the async in the future (this currently breaks our debugging tests)
99
+ async debug() {
100
+ return debugLibauthTemplate(this.getLibauthTemplate(), this);
101
+ }
102
+ bitauthUri() {
103
+ return getBitauthUri(this.getLibauthTemplate());
104
+ }
105
+ getLibauthTemplate() {
106
+ return getLibauthTemplates(this);
107
+ }
90
108
  async send(raw) {
91
109
  const tx = this.build();
110
+ // Debug the transaction locally before sending so any errors are caught early
111
+ await this.debug();
92
112
  try {
93
113
  const txid = await this.provider.sendRawTransaction(tx);
94
114
  return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid);
@@ -0,0 +1,44 @@
1
+ import { TransactionBch, WalletTemplate } from '@bitauth/libauth';
2
+ import { AbiFunction, Artifact } from '@cashscript/utils';
3
+ import { EncodedConstructorArgument, EncodedFunctionArgument } from '../Argument.js';
4
+ import { Contract } from '../Contract.js';
5
+ import { DebugResults } from '../debugging.js';
6
+ import { AddressType } from '../interfaces.js';
7
+ import SignatureTemplate from '../SignatureTemplate.js';
8
+ import { Transaction } from '../Transaction.js';
9
+ import { TransactionBuilder } from '../TransactionBuilder.js';
10
+ /**
11
+ * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts.
12
+ *
13
+ * Follows the WalletTemplateEntity specification from:
14
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
15
+ *
16
+ */
17
+ export declare const generateTemplateEntitiesP2PKH: (index: number) => WalletTemplate["entities"];
18
+ /**
19
+ * Generates template entities for P2SH (Pay to Script Hash) placeholder scripts.
20
+ *
21
+ * Follows the WalletTemplateEntity specification from:
22
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
23
+ *
24
+ */
25
+ export declare const generateTemplateEntitiesP2SH: (artifact: Artifact, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[]) => WalletTemplate["entities"];
26
+ /**
27
+ * Generates template scripts for P2PKH (Pay to Public Key Hash) placeholder scripts.
28
+ *
29
+ * Follows the WalletTemplateScript specification from:
30
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
31
+ *
32
+ */
33
+ export declare const generateTemplateScriptsP2PKH: (template: SignatureTemplate, index: number) => WalletTemplate["scripts"];
34
+ /**
35
+ * Generates template scripts for P2SH (Pay to Script Hash) placeholder scripts.
36
+ *
37
+ * Follows the WalletTemplateScript specification from:
38
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
39
+ *
40
+ */
41
+ export declare const generateTemplateScriptsP2SH: (artifact: Artifact, addressType: AddressType, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], encodedConstructorArgs: EncodedConstructorArgument[], scenarioIds: string[]) => WalletTemplate["scripts"];
42
+ export declare const generateTemplateScenarios: (scenarioIdentifier: string, contract: Contract, libauthTransaction: TransactionBch, csTransaction: Transaction, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], slotIndex: number) => WalletTemplate["scenarios"];
43
+ export declare const getLibauthTemplates: (txn: TransactionBuilder) => WalletTemplate;
44
+ export declare const debugLibauthTemplate: (template: WalletTemplate, transaction: TransactionBuilder) => DebugResults;
@@ -0,0 +1,396 @@
1
+ import { binToHex, } from '@bitauth/libauth';
2
+ import { encodeFunctionArguments } from '../Argument.js';
3
+ import { debugTemplate } from '../debugging.js';
4
+ import { addHexPrefixExceptEmpty, formatBytecodeForDebugging, formatParametersForDebugging, generateTemplateScenarioBytecode, generateTemplateScenarioKeys, generateTemplateScenarioParametersValues, generateTemplateScenarioTransactionOutputLockingBytecode, getHashTypeName, getSignatureAlgorithmName, serialiseTokenDetails, } from '../LibauthTemplate.js';
5
+ import SignatureTemplate from '../SignatureTemplate.js';
6
+ import { addressToLockScript, snakeCase, titleCase } from '../utils.js';
7
+ /**
8
+ * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts.
9
+ *
10
+ * Follows the WalletTemplateEntity specification from:
11
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
12
+ *
13
+ */
14
+ export const generateTemplateEntitiesP2PKH = (index) => {
15
+ const lockScriptName = `p2pkh_placeholder_lock_${index}`;
16
+ const unlockScriptName = `p2pkh_placeholder_unlock_${index}`;
17
+ return {
18
+ [`signer_${index}`]: {
19
+ scripts: [lockScriptName, unlockScriptName],
20
+ description: `placeholder_key_${index}`,
21
+ name: `Signer ${index}`,
22
+ variables: {
23
+ [`placeholder_key_${index}`]: {
24
+ description: '',
25
+ name: `Placeholder Key ${index}`,
26
+ type: 'Key',
27
+ },
28
+ },
29
+ },
30
+ };
31
+ };
32
+ /**
33
+ * Generates template entities for P2SH (Pay to Script Hash) placeholder scripts.
34
+ *
35
+ * Follows the WalletTemplateEntity specification from:
36
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
37
+ *
38
+ */
39
+ export const generateTemplateEntitiesP2SH = (artifact, abiFunction, encodedFunctionArgs) => {
40
+ const functionParameters = Object.fromEntries(abiFunction.inputs.map((input, index) => ([
41
+ snakeCase(input.name),
42
+ {
43
+ description: `"${input.name}" parameter of function "${abiFunction.name}"`,
44
+ name: titleCase(input.name),
45
+ type: encodedFunctionArgs[index] instanceof SignatureTemplate ? 'Key' : 'WalletData',
46
+ },
47
+ ])));
48
+ const constructorParameters = Object.fromEntries(artifact.constructorInputs.map((input) => ([
49
+ snakeCase(input.name),
50
+ {
51
+ description: `"${input.name}" parameter of this contract`,
52
+ name: titleCase(input.name),
53
+ type: 'WalletData',
54
+ },
55
+ ])));
56
+ const entities = {
57
+ [snakeCase(artifact.contractName + 'Parameters')]: {
58
+ description: 'Contract creation and function parameters',
59
+ name: titleCase(artifact.contractName),
60
+ scripts: [
61
+ snakeCase(artifact.contractName + '_lock'),
62
+ snakeCase(artifact.contractName + '_' + abiFunction.name + '_unlock'),
63
+ ],
64
+ variables: {
65
+ ...functionParameters,
66
+ ...constructorParameters,
67
+ },
68
+ },
69
+ };
70
+ // function_index is a special variable that indicates the function to execute
71
+ if (artifact.abi.length > 1) {
72
+ entities[snakeCase(artifact.contractName + 'Parameters')].variables.function_index = {
73
+ description: 'Script function index to execute',
74
+ name: titleCase('function_index'),
75
+ type: 'WalletData',
76
+ };
77
+ }
78
+ return entities;
79
+ };
80
+ /**
81
+ * Generates template scripts for P2PKH (Pay to Public Key Hash) placeholder scripts.
82
+ *
83
+ * Follows the WalletTemplateScript specification from:
84
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
85
+ *
86
+ */
87
+ export const generateTemplateScriptsP2PKH = (template, index) => {
88
+ const scripts = {};
89
+ const lockScriptName = `p2pkh_placeholder_lock_${index}`;
90
+ const unlockScriptName = `p2pkh_placeholder_unlock_${index}`;
91
+ const placeholderKeyName = `placeholder_key_${index}`;
92
+ const signatureAlgorithmName = getSignatureAlgorithmName(template.getSignatureAlgorithm());
93
+ const hashtypeName = getHashTypeName(template.getHashType(false));
94
+ const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`;
95
+ // add extra unlocking and locking script for P2PKH inputs spent alongside our contract
96
+ // this is needed for correct cross-references in the template
97
+ scripts[unlockScriptName] = {
98
+ name: unlockScriptName,
99
+ script: `<${signatureString}>\n<${placeholderKeyName}.public_key>`,
100
+ unlocks: lockScriptName,
101
+ };
102
+ scripts[lockScriptName] = {
103
+ lockingType: 'standard',
104
+ name: lockScriptName,
105
+ script: `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`,
106
+ };
107
+ return scripts;
108
+ };
109
+ /**
110
+ * Generates template scripts for P2SH (Pay to Script Hash) placeholder scripts.
111
+ *
112
+ * Follows the WalletTemplateScript specification from:
113
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
114
+ *
115
+ */
116
+ export const generateTemplateScriptsP2SH = (artifact, addressType, abiFunction, encodedFunctionArgs, encodedConstructorArgs, scenarioIds) => {
117
+ // definition of locking scripts and unlocking scripts with their respective bytecode
118
+ return {
119
+ [snakeCase(artifact.contractName + '_' + abiFunction.name + '_unlock')]: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs, scenarioIds),
120
+ [snakeCase(artifact.contractName + '_lock')]: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs),
121
+ };
122
+ };
123
+ /**
124
+ * Generates a template lock script for a P2SH (Pay to Script Hash) placeholder script.
125
+ *
126
+ * Follows the WalletTemplateScriptLocking specification from:
127
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
128
+ *
129
+ */
130
+ const generateTemplateLockScript = (artifact, addressType, constructorArguments) => {
131
+ return {
132
+ lockingType: addressType,
133
+ name: artifact.contractName,
134
+ script: [
135
+ `// "${artifact.contractName}" contract constructor parameters`,
136
+ formatParametersForDebugging(artifact.constructorInputs, constructorArguments),
137
+ '',
138
+ '// bytecode',
139
+ formatBytecodeForDebugging(artifact),
140
+ ].join('\n'),
141
+ };
142
+ };
143
+ /**
144
+ * Generates a template unlock script for a P2SH (Pay to Script Hash) placeholder script.
145
+ *
146
+ * Follows the WalletTemplateScriptUnlocking specification from:
147
+ * https://ide.bitauth.com/authentication-template-v0.schema.json
148
+ *
149
+ */
150
+ const generateTemplateUnlockScript = (artifact, abiFunction, encodedFunctionArgs, scenarioIds) => {
151
+ const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name);
152
+ const functionIndexString = artifact.abi.length > 1
153
+ ? ['// function index in contract', `<function_index> // int = <${functionIndex}>`, '']
154
+ : [];
155
+ return {
156
+ // this unlocking script must pass our only scenario
157
+ passes: scenarioIds,
158
+ name: abiFunction.name,
159
+ script: [
160
+ `// "${abiFunction.name}" function parameters`,
161
+ formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs),
162
+ '',
163
+ ...functionIndexString,
164
+ ].join('\n'),
165
+ unlocks: snakeCase(artifact.contractName + '_lock'),
166
+ };
167
+ };
168
+ export const generateTemplateScenarios = (scenarioIdentifier, contract, libauthTransaction, csTransaction, abiFunction, encodedFunctionArgs, slotIndex) => {
169
+ const artifact = contract.artifact;
170
+ const encodedConstructorArgs = contract.encodedConstructorArgs;
171
+ const scenarios = {
172
+ // single scenario to spend out transaction under test given the CashScript parameters provided
173
+ [scenarioIdentifier]: {
174
+ name: snakeCase(artifact.contractName + '_' + abiFunction.name + 'Evaluate'),
175
+ description: 'An example evaluation where this script execution passes.',
176
+ data: {
177
+ // encode values for the variables defined above in `entities` property
178
+ bytecode: {
179
+ ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs),
180
+ ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs),
181
+ },
182
+ currentBlockHeight: 2,
183
+ currentBlockTime: Math.round(+new Date() / 1000),
184
+ keys: {
185
+ privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs),
186
+ },
187
+ },
188
+ transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, csTransaction, slotIndex),
189
+ sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, slotIndex),
190
+ },
191
+ };
192
+ if (artifact.abi.length > 1) {
193
+ const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name);
194
+ scenarios[scenarioIdentifier].data.bytecode.function_index = functionIndex.toString();
195
+ }
196
+ return scenarios;
197
+ };
198
+ const generateTemplateScenarioTransaction = (contract, libauthTransaction, csTransaction, slotIndex) => {
199
+ const inputs = libauthTransaction.inputs.map((input, index) => {
200
+ const csInput = csTransaction.inputs[index];
201
+ return {
202
+ outpointIndex: input.outpointIndex,
203
+ outpointTransactionHash: binToHex(input.outpointTransactionHash),
204
+ sequenceNumber: input.sequenceNumber,
205
+ unlockingBytecode: generateTemplateScenarioBytecode(csInput, `p2pkh_placeholder_unlock_${index}`, `placeholder_key_${index}`, slotIndex === index),
206
+ };
207
+ });
208
+ const locktime = libauthTransaction.locktime;
209
+ const outputs = libauthTransaction.outputs.map((output, index) => {
210
+ const csOutput = csTransaction.outputs[index];
211
+ return {
212
+ lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract),
213
+ token: serialiseTokenDetails(output.token),
214
+ valueSatoshis: Number(output.valueSatoshis),
215
+ };
216
+ });
217
+ const version = libauthTransaction.version;
218
+ return { inputs, locktime, outputs, version };
219
+ };
220
+ const generateTemplateScenarioSourceOutputs = (csTransaction, slotIndex) => {
221
+ return csTransaction.inputs.map((input, index) => {
222
+ return {
223
+ lockingBytecode: generateTemplateScenarioBytecode(input, `p2pkh_placeholder_lock_${index}`, `placeholder_key_${index}`, index === slotIndex),
224
+ valueSatoshis: Number(input.satoshis),
225
+ token: serialiseTokenDetails(input.token),
226
+ };
227
+ });
228
+ };
229
+ /**
230
+ * Creates a transaction object from a TransactionBuilder instance
231
+ *
232
+ * @param txn - The TransactionBuilder instance to convert
233
+ * @returns A transaction object containing inputs, outputs, locktime and version
234
+ */
235
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
236
+ const createCSTransaction = (txn) => {
237
+ const csTransaction = {
238
+ inputs: txn.inputs,
239
+ locktime: txn.locktime,
240
+ outputs: txn.outputs,
241
+ version: 2,
242
+ };
243
+ return csTransaction;
244
+ };
245
+ export const getLibauthTemplates = (txn) => {
246
+ const libauthTransaction = txn.buildLibauthTransaction();
247
+ const csTransaction = createCSTransaction(txn);
248
+ const baseTemplate = {
249
+ $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json',
250
+ description: 'Imported from cashscript',
251
+ name: 'CashScript Generated Debugging Template',
252
+ supported: ['BCH_2023_05'],
253
+ version: 0,
254
+ entities: {},
255
+ scripts: {},
256
+ scenarios: {},
257
+ };
258
+ // Initialize collections for entities, scripts, and scenarios
259
+ const entities = {};
260
+ const scripts = {};
261
+ const scenarios = {};
262
+ // Initialize collections for P2PKH entities and scripts
263
+ const p2pkhEntities = {};
264
+ const p2pkhScripts = {};
265
+ // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts
266
+ const unlockingBytecodeToLockingBytecodeParams = {};
267
+ const lockingBytecodeToLockingBytecodeParams = {};
268
+ for (const [idx, input] of txn.inputs.entries()) {
269
+ // If template exists on the input, it indicates this is a P2PKH (Pay to Public Key Hash) input
270
+ if (input.unlocker?.template) {
271
+ // @ts-ignore TODO: Remove UtxoP2PKH type and only use UnlockableUtxo in Libaith Template generation
272
+ input.template = input.unlocker?.template; // Added to support P2PKH inputs in buildTemplate
273
+ Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(idx));
274
+ Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(input.unlocker.template, idx));
275
+ continue;
276
+ }
277
+ // If contract exists on the input, it indicates this is a contract input
278
+ if (input.unlocker?.contract) {
279
+ const contract = input.unlocker?.contract;
280
+ const abiFunction = input.unlocker?.abiFunction;
281
+ if (!abiFunction) {
282
+ throw new Error('No ABI function found in unlocker');
283
+ }
284
+ // Find matching function and index from contract.unlock Object, this uses Function Reference Comparison.
285
+ // Generate unique scenario identifier by combining contract name, function name and counter
286
+ const baseIdentifier = `${contract.artifact.contractName}_${abiFunction.name}Evaluate`;
287
+ let scenarioIdentifier = baseIdentifier;
288
+ let counter = 0;
289
+ const scenarioIds = [snakeCase(scenarioIdentifier)];
290
+ // Find first available unique identifier by incrementing counter
291
+ while (scenarios[snakeCase(scenarioIdentifier)]) {
292
+ counter++;
293
+ scenarioIdentifier = `${baseIdentifier}${counter}`;
294
+ scenarioIds.push(snakeCase(scenarioIdentifier));
295
+ }
296
+ scenarioIdentifier = snakeCase(scenarioIdentifier);
297
+ // Encode the function arguments for this contract input
298
+ const encodedArgs = encodeFunctionArguments(abiFunction, input.unlocker.params ?? []);
299
+ // Generate a scenario object for this contract input
300
+ Object.assign(scenarios, generateTemplateScenarios(scenarioIdentifier, contract, libauthTransaction, csTransaction, abiFunction, encodedArgs, idx));
301
+ // Generate entities for this contract input
302
+ const entity = generateTemplateEntitiesP2SH(contract.artifact, abiFunction, encodedArgs);
303
+ // Generate scripts for this contract input
304
+ const script = generateTemplateScriptsP2SH(contract.artifact, contract.addressType, abiFunction, encodedArgs, contract.encodedConstructorArgs, scenarioIds);
305
+ // Find the lock script name for this contract input
306
+ const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock'));
307
+ if (lockScriptName) {
308
+ // Generate bytecodes for this contract input
309
+ const csInput = csTransaction.inputs[idx];
310
+ const unlockingBytecode = binToHex(libauthTransaction.inputs[idx].unlockingBytecode);
311
+ const lockingScriptParams = generateLockingScriptParams(csInput.unlocker.contract, csInput, lockScriptName);
312
+ // Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios
313
+ unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams;
314
+ // Assign a name to the locking bytecode so later it can be used to replace with bytecode/slot in scenarios
315
+ lockingBytecodeToLockingBytecodeParams[binToHex(addressToLockScript(contract.address))] = lockingScriptParams;
316
+ }
317
+ // Add entities and scripts to the base template and repeat the process for the next input
318
+ Object.assign(entities, entity);
319
+ Object.assign(scripts, script);
320
+ }
321
+ }
322
+ // Merge P2PKH scripts
323
+ for (const entity of Object.values(p2pkhEntities)) {
324
+ if (entity.scripts) {
325
+ entity.scripts = [...Object.keys(scripts), ...entity.scripts];
326
+ }
327
+ }
328
+ Object.assign(entities, p2pkhEntities);
329
+ Object.assign(scripts, p2pkhScripts);
330
+ const finalTemplate = { ...baseTemplate, entities, scripts, scenarios };
331
+ // Loop through all scenarios and map the locking and unlocking scripts to the scenarios
332
+ // Replace the script tag with the identifiers we created earlier
333
+ // For Inputs
334
+ for (const scenario of Object.values(scenarios)) {
335
+ for (const [idx, input] of libauthTransaction.inputs.entries()) {
336
+ const unlockingBytecode = binToHex(input.unlockingBytecode);
337
+ // If false then it stays lockingBytecode: {}
338
+ if (unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]) {
339
+ // ['slot'] this identifies the source output in which the locking script under test will be placed
340
+ if (Array.isArray(scenario?.sourceOutputs?.[idx]?.lockingBytecode))
341
+ continue;
342
+ // If true then assign a name to the locking bytecode script.
343
+ if (scenario.sourceOutputs && scenario.sourceOutputs[idx]) {
344
+ scenario.sourceOutputs[idx] = {
345
+ ...scenario.sourceOutputs[idx],
346
+ lockingBytecode: unlockingBytecodeToLockingBytecodeParams[unlockingBytecode],
347
+ };
348
+ }
349
+ }
350
+ }
351
+ // For Outputs
352
+ for (const [idx, output] of libauthTransaction.outputs.entries()) {
353
+ const lockingBytecode = binToHex(output.lockingBytecode);
354
+ // If false then it stays lockingBytecode: {}
355
+ if (lockingBytecodeToLockingBytecodeParams[lockingBytecode]) {
356
+ // ['slot'] this identifies the source output in which the locking script under test will be placed
357
+ if (Array.isArray(scenario?.transaction?.outputs?.[idx]?.lockingBytecode))
358
+ continue;
359
+ // If true then assign a name to the locking bytecode script.
360
+ if (scenario?.transaction && scenario?.transaction?.outputs && scenario?.transaction?.outputs[idx]) {
361
+ scenario.transaction.outputs[idx] = {
362
+ ...scenario.transaction.outputs[idx],
363
+ lockingBytecode: lockingBytecodeToLockingBytecodeParams[lockingBytecode],
364
+ };
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return finalTemplate;
370
+ };
371
+ export const debugLibauthTemplate = (template, transaction) => {
372
+ const allArtifacts = transaction.inputs
373
+ .map(input => input.unlocker?.contract)
374
+ .filter((contract) => !!contract)
375
+ .map(contract => contract.artifact);
376
+ return debugTemplate(template, allArtifacts);
377
+ };
378
+ const generateLockingScriptParams = (contract, csInput, lockScriptName) => {
379
+ if (!csInput.unlocker?.contract)
380
+ return {
381
+ script: lockScriptName,
382
+ };
383
+ const constructorParamsEntries = contract.artifact.constructorInputs
384
+ .map(({ name }, index) => [
385
+ name,
386
+ addHexPrefixExceptEmpty(binToHex(csInput.unlocker.contract.encodedConstructorArgs[index])),
387
+ ]);
388
+ const constructorParams = Object.fromEntries(constructorParamsEntries);
389
+ return {
390
+ script: lockScriptName,
391
+ overrides: {
392
+ bytecode: { ...constructorParams },
393
+ },
394
+ };
395
+ };
396
+ //# sourceMappingURL=LibauthTemplate.js.map
@@ -1,5 +1,5 @@
1
1
  import { AuthenticationProgramStateCommon, WalletTemplate } from '@bitauth/libauth';
2
2
  import { Artifact } from '@cashscript/utils';
3
- export declare const evaluateTemplate: (template: WalletTemplate) => boolean;
4
3
  export type DebugResult = AuthenticationProgramStateCommon[];
5
- export declare const debugTemplate: (template: WalletTemplate, artifact: Artifact) => DebugResult;
4
+ export type DebugResults = Record<string, DebugResult>;
5
+ export declare const debugTemplate: (template: WalletTemplate, artifacts: Artifact[]) => DebugResults;
package/dist/debugging.js CHANGED
@@ -1,20 +1,29 @@
1
- import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBCH2023, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
1
+ import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBch2025, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
2
2
  import { Op, PrimitiveType, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils';
3
- import { findLastIndex, toRegExp } from './utils.js';
3
+ import { findLastIndex, snakeCase, toRegExp } from './utils.js';
4
4
  import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js';
5
5
  import { getBitauthUri } from './LibauthTemplate.js';
6
- // evaluates the fully defined template, throws upon error
7
- export const evaluateTemplate = (template) => {
8
- const { vm, program } = createProgram(template);
9
- const verifyResult = vm.verify(program);
10
- if (typeof verifyResult === 'string') {
11
- throw new FailedTransactionError(verifyResult, getBitauthUri(template));
6
+ // debugs the template, optionally logging the execution data
7
+ export const debugTemplate = (template, artifacts) => {
8
+ const results = {};
9
+ const unlockingScriptIds = Object.keys(template.scripts).filter((key) => 'unlocks' in template.scripts[key]);
10
+ for (const unlockingScriptId of unlockingScriptIds) {
11
+ const scenarioIds = template.scripts[unlockingScriptId].passes ?? [];
12
+ // There are no scenarios defined for P2PKH placeholder scripts, so we skip them
13
+ if (scenarioIds.length === 0)
14
+ continue;
15
+ const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(snakeCase(artifact.contractName)));
16
+ if (!matchingArtifact) {
17
+ throw new Error(`No artifact found for unlocking script ${unlockingScriptId}`);
18
+ }
19
+ for (const scenarioId of scenarioIds) {
20
+ results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId);
21
+ }
12
22
  }
13
- return verifyResult;
23
+ return results;
14
24
  };
15
- // debugs the template, optionally logging the execution data
16
- export const debugTemplate = (template, artifact) => {
17
- const { vm, program } = createProgram(template);
25
+ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId) => {
26
+ const { vm, program } = createProgram(template, unlockingScriptId, scenarioId);
18
27
  const fullDebugSteps = vm.debug(program);
19
28
  // P2SH executions have 3 phases, we only want the last one (locking script execution)
20
29
  // https://libauth.org/types/AuthenticationVirtualMachine.html#__type.debug
@@ -39,7 +48,7 @@ export const debugTemplate = (template, artifact) => {
39
48
  // preceding OP_CHECKSIG opcode. The error message is registered in the next instruction, so we need to increment
40
49
  // the instruction pointer to get the correct error message from the require messages in the artifact.
41
50
  // Note that we do NOT use this adjusted IP when passing the failing IP into the FailedRequireError
42
- const isNullFail = lastExecutedDebugStep.error === AuthenticationErrorCommon.nonNullSignatureFailure;
51
+ const isNullFail = lastExecutedDebugStep.error.includes(AuthenticationErrorCommon.nonNullSignatureFailure);
43
52
  const requireStatementIp = failingIp + (isNullFail ? 1 : 0);
44
53
  const requireStatement = (artifact.debug?.requires ?? [])
45
54
  .find((statement) => statement.ip === requireStatementIp);
@@ -72,15 +81,20 @@ export const debugTemplate = (template, artifact) => {
72
81
  return fullDebugSteps;
73
82
  };
74
83
  // internal util. instantiates the virtual machine and compiles the template into a program
75
- const createProgram = (template) => {
84
+ const createProgram = (template, unlockingScriptId, scenarioId) => {
76
85
  const configuration = walletTemplateToCompilerConfiguration(template);
77
- const vm = createVirtualMachineBCH2023();
86
+ const vm = createVirtualMachineBch2025();
78
87
  const compiler = createCompiler(configuration);
88
+ if (!template.scripts[unlockingScriptId]) {
89
+ throw new Error(`No unlock script found in template for ID ${unlockingScriptId}`);
90
+ }
91
+ if (!template.scenarios?.[scenarioId]) {
92
+ throw new Error(`No scenario found in template for ID ${scenarioId}`);
93
+ }
79
94
  const scenarioGeneration = compiler.generateScenario({
80
95
  debug: true,
81
- lockingScriptId: undefined,
82
- unlockingScriptId: 'unlock_lock',
83
- scenarioId: 'evaluate_function',
96
+ unlockingScriptId,
97
+ scenarioId,
84
98
  });
85
99
  if (typeof scenarioGeneration === 'string') {
86
100
  throw new FailedTransactionError(scenarioGeneration, getBitauthUri(template));
@@ -121,8 +135,9 @@ const failedFinalVerify = (evaluationResult) => {
121
135
  // If any of the following errors occurred, then the final verify failed - any other messages
122
136
  // indicate other kinds of failures
123
137
  return toRegExp([
124
- AuthenticationErrorCommon.requiresCleanStack,
125
- AuthenticationErrorCommon.nonEmptyControlStack,
138
+ // TODO: Ask Jason to put these back into an enum and replace with the enum value
139
+ 'The CashAssembly internal evaluation completed with an unexpected number of items on the stack (must be exactly 1).', // AuthenticationErrorCommon.requiresCleanStack,
140
+ 'The CashAssembly internal evaluation completed with a non-empty control stack.', // AuthenticationErrorCommon.nonEmptyControlStack,
126
141
  AuthenticationErrorCommon.unsuccessfulEvaluation,
127
142
  ]).test(evaluationResult);
128
143
  };
@@ -1,6 +1,9 @@
1
1
  import { type Transaction } from '@bitauth/libauth';
2
2
  import type { NetworkProvider } from './network/index.js';
3
3
  import type SignatureTemplate from './SignatureTemplate.js';
4
+ import { Contract } from './Contract.js';
5
+ import { AbiFunction } from '@cashscript/utils';
6
+ import { FunctionArgument } from './Argument.js';
4
7
  export interface Utxo {
5
8
  txid: string;
6
9
  vout: number;
@@ -23,6 +26,10 @@ export interface GenerateUnlockingBytecodeOptions {
23
26
  export interface Unlocker {
24
27
  generateLockingBytecode: () => Uint8Array;
25
28
  generateUnlockingBytecode: (options: GenerateUnlockingBytecodeOptions) => Uint8Array;
29
+ contract?: Contract;
30
+ params?: FunctionArgument[];
31
+ abiFunction?: AbiFunction;
32
+ template?: SignatureTemplate;
26
33
  }
27
34
  export interface UtxoP2PKH extends Utxo {
28
35
  template: SignatureTemplate;
@@ -38,7 +38,7 @@ export default class ElectrumNetworkProvider {
38
38
  }
39
39
  else if (network === Network.CHIPNET) {
40
40
  this.electrum = new ElectrumCluster('CashScript Application', '1.4.1', 1, 1, ClusterOrder.PRIORITY);
41
- this.electrum.addServer('chipnet.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme, false);
41
+ this.electrum.addServer('chipnet.bch.ninja', 50004, ElectrumTransport.WSS.Scheme, false);
42
42
  }
43
43
  else {
44
44
  throw new Error(`Tried to instantiate an ElectrumNetworkProvider for unsupported network ${network}`);
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Transaction } from '@bitauth/libauth';
2
2
  import { Script } from '@cashscript/utils';
3
3
  import { Utxo, Output, LibauthOutput, TokenDetails, AddressType } from './interfaces.js';
4
+ export { snakeCase } from 'change-case';
5
+ export declare function validateInput(utxo: Utxo): void;
4
6
  export declare function validateOutput(output: Output): void;
5
7
  export declare function calculateDust(output: Output): number;
6
8
  export declare function getOutputSize(output: Output): number;
@@ -8,9 +10,8 @@ export declare function encodeOutput(output: Output): Uint8Array;
8
10
  export declare function cashScriptOutputToLibauthOutput(output: Output): LibauthOutput;
9
11
  export declare function libauthOutputToCashScriptOutput(output: LibauthOutput): Output;
10
12
  export declare function getInputSize(inputScript: Uint8Array): number;
11
- export declare function getPreimageSize(script: Uint8Array): number;
12
13
  export declare function getTxSizeWithoutInputs(outputs: Output[]): number;
13
- export declare function createInputScript(redeemScript: Script, encodedArgs: Uint8Array[], selector?: number, preimage?: Uint8Array): Uint8Array;
14
+ export declare function createInputScript(redeemScript: Script, encodedArgs: Uint8Array[], selector?: number): Uint8Array;
14
15
  export declare function createOpReturnOutput(opReturnData: string[]): Output;
15
16
  export declare function createSighashPreimage(transaction: Transaction, sourceOutputs: LibauthOutput[], inputIndex: number, coveredBytecode: Uint8Array, hashtype: number): Uint8Array;
16
17
  export declare function toRegExp(reasons: string[]): RegExp;
@@ -32,6 +33,8 @@ export declare const randomUtxo: (defaults?: Partial<Utxo>) => Utxo;
32
33
  export declare const randomToken: (defaults?: Partial<TokenDetails>) => TokenDetails;
33
34
  export declare const randomNFT: (defaults?: Partial<TokenDetails>) => TokenDetails;
34
35
  export declare function findLastIndex<T>(array: Array<T>, predicate: (value: T, index: number, obj: T[]) => boolean): number;
35
- export declare const snakeCase: (str: string) => string;
36
+ export declare const titleCase: (str: string) => string;
36
37
  export declare const extendedStringify: (obj: any, spaces?: number) => string;
37
38
  export declare const zip: <T, U>(a: readonly T[], b: readonly U[]) => [T, U][];
39
+ export declare const isFungibleTokenUtxo: (utxo: Utxo) => boolean;
40
+ export declare const isNonTokenUtxo: (utxo: Utxo) => boolean;
package/dist/utils.js CHANGED
@@ -1,9 +1,15 @@
1
- import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLockingBytecode, lockingBytecodeToCashAddress, binToHex, generateSigningSerializationBCH, utf8ToBin, hexToBin, flattenBinArray, LockingBytecodeType, encodeTransactionOutput, isHex, bigIntToCompactUint, NonFungibleTokenCapability, bigIntToVmNumber, } from '@bitauth/libauth';
1
+ import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLockingBytecode, lockingBytecodeToCashAddress, binToHex, generateSigningSerializationBch, utf8ToBin, hexToBin, flattenBinArray, LockingBytecodeType, encodeTransactionOutput, isHex, bigIntToCompactUint, NonFungibleTokenCapability, bigIntToVmNumber, } from '@bitauth/libauth';
2
2
  import { encodeInt, hash160, hash256, sha256, Op, scriptToBytecode, } from '@cashscript/utils';
3
3
  import { Network, } from './interfaces.js';
4
4
  import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
5
- import { OutputSatoshisTooSmallError, TokensToNonTokenAddressError, } from './Errors.js';
5
+ import { OutputSatoshisTooSmallError, OutputTokenAmountTooSmallError, TokensToNonTokenAddressError, UndefinedInputError, } from './Errors.js';
6
+ export { snakeCase } from 'change-case';
6
7
  // ////////// PARAMETER VALIDATION ////////////////////////////////////////////
8
+ export function validateInput(utxo) {
9
+ if (!utxo) {
10
+ throw new UndefinedInputError();
11
+ }
12
+ }
7
13
  export function validateOutput(output) {
8
14
  if (typeof output.to !== 'string')
9
15
  return;
@@ -15,6 +21,9 @@ export function validateOutput(output) {
15
21
  if (!isTokenAddress(output.to)) {
16
22
  throw new TokensToNonTokenAddressError(output.to);
17
23
  }
24
+ if (output.token.amount < 0n) {
25
+ throw new OutputTokenAmountTooSmallError(output.token.amount);
26
+ }
18
27
  }
19
28
  }
20
29
  export function calculateDust(output) {
@@ -79,11 +88,6 @@ export function getInputSize(inputScript) {
79
88
  const varIntSize = scriptSize > 252 ? 3 : 1;
80
89
  return 32 + 4 + varIntSize + scriptSize + 4;
81
90
  }
82
- export function getPreimageSize(script) {
83
- const scriptSize = script.byteLength;
84
- const varIntSize = scriptSize > 252 ? 3 : 1;
85
- return 4 + 32 + 32 + 36 + varIntSize + scriptSize + 8 + 4 + 32 + 4 + 4;
86
- }
87
91
  export function getTxSizeWithoutInputs(outputs) {
88
92
  // Transaction format:
89
93
  // Version (4 Bytes)
@@ -106,11 +110,9 @@ export function getTxSizeWithoutInputs(outputs) {
106
110
  return size;
107
111
  }
108
112
  // ////////// BUILD OBJECTS ///////////////////////////////////////////////////
109
- export function createInputScript(redeemScript, encodedArgs, selector, preimage) {
110
- // Create unlock script / redeemScriptSig (add potential preimage and selector)
113
+ export function createInputScript(redeemScript, encodedArgs, selector) {
114
+ // Create unlock script / redeemScriptSig (add potential selector)
111
115
  const unlockScript = [...encodedArgs].reverse();
112
- if (preimage !== undefined)
113
- unlockScript.push(preimage);
114
116
  if (selector !== undefined)
115
117
  unlockScript.push(encodeInt(BigInt(selector)));
116
118
  // Create input script and compile it to bytecode
@@ -132,7 +134,7 @@ function toBin(output) {
132
134
  export function createSighashPreimage(transaction, sourceOutputs, inputIndex, coveredBytecode, hashtype) {
133
135
  const context = { inputIndex, sourceOutputs, transaction };
134
136
  const signingSerializationType = new Uint8Array([hashtype]);
135
- const sighashPreimage = generateSigningSerializationBCH(context, { coveredBytecode, signingSerializationType });
137
+ const sighashPreimage = generateSigningSerializationBch(context, { coveredBytecode, signingSerializationType });
136
138
  return sighashPreimage;
137
139
  }
138
140
  export function toRegExp(reasons) {
@@ -262,11 +264,24 @@ export function findLastIndex(array, predicate) {
262
264
  }
263
265
  return -1;
264
266
  }
265
- export const snakeCase = (str) => (str
266
- && str
267
+ // TODO: Somehow, this turns P2PKHLock into p2pkh_lock, but P2PKH_Lock into p2_pkh_lock
268
+ // export const snakeCase = (str: string): string => (
269
+ // str
270
+ // && str
271
+ // .match(
272
+ // /([A-Z]+\d*[A-Z]*(?=[A-Z][a-z])|[A-Z]?[a-z]+\d*[a-z]+|[A-Z]?[a-z]+\d*|[A-Z]+\d*|\d+)/g,
273
+ // )!
274
+ // .map((s) => s.toLowerCase())
275
+ // .join('_')
276
+ // );
277
+ export const titleCase = (str) => {
278
+ if (!str)
279
+ return '';
280
+ return str
267
281
  .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
268
- .map((s) => s.toLowerCase())
269
- .join('_'));
282
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
283
+ .join(' ');
284
+ };
270
285
  // JSON.stringify version that can serialize otherwise unsupported types (bigint and Uint8Array)
271
286
  export const extendedStringify = (obj, spaces) => JSON.stringify(obj, (_, v) => {
272
287
  if (typeof v === 'bigint') {
@@ -278,4 +293,6 @@ export const extendedStringify = (obj, spaces) => JSON.stringify(obj, (_, v) =>
278
293
  return v;
279
294
  }, spaces);
280
295
  export const zip = (a, b) => (Array.from(Array(Math.max(b.length, a.length)), (_, i) => [a[i], b[i]]));
296
+ export const isFungibleTokenUtxo = (utxo) => (utxo.token !== undefined && utxo.token.amount > 0n && utxo.token.nft === undefined);
297
+ export const isNonTokenUtxo = (utxo) => utxo.token === undefined;
281
298
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cashscript",
3
- "version": "0.10.5",
3
+ "version": "0.11.0-next.1",
4
4
  "description": "Easily write and interact with Bitcoin Cash contracts",
5
5
  "keywords": [
6
6
  "bitcoin cash",
@@ -43,21 +43,25 @@
43
43
  "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest"
44
44
  },
45
45
  "dependencies": {
46
- "@bitauth/libauth": "^3.0.0",
47
- "@cashscript/utils": "^0.10.5",
46
+ "@bitauth/libauth": "^3.1.0-next.2",
47
+ "@cashscript/utils": "^0.11.0-next.1",
48
48
  "@mr-zwets/bchn-api-wrapper": "^1.0.1",
49
- "delay": "^5.0.0",
49
+ "change-case": "^5.4.4",
50
+ "delay": "^6.0.0",
50
51
  "electrum-cash": "^2.0.10",
51
52
  "fast-deep-equal": "^3.1.3",
52
- "pako": "^2.1.0"
53
+ "pako": "^2.1.0",
54
+ "semver": "^7.6.3"
53
55
  },
54
56
  "devDependencies": {
55
- "@jest/globals": "^29.4.1",
57
+ "@jest/globals": "^29.7.0",
56
58
  "@psf/bch-js": "^6.8.0",
59
+ "@types/change-case": "^2.3.5",
57
60
  "@types/pako": "^2.0.3",
61
+ "@types/semver": "^7.5.8",
58
62
  "eslint": "^8.54.0",
59
- "jest": "^29.4.1",
60
- "typescript": "^5.5.4"
63
+ "jest": "^29.7.0",
64
+ "typescript": "^5.7.3"
61
65
  },
62
- "gitHead": "08b93f1457b8b2c54169cfd9ba72004124a65b8e"
66
+ "gitHead": "41f969f460606048595bb108e753044b9673e748"
63
67
  }