cashscript 0.13.0-next.7 → 0.13.0-next.9

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/dist/Contract.js CHANGED
@@ -92,10 +92,10 @@ class ContractInternal extends ContractBase {
92
92
  if (abiFunction.inputs.length !== args.length) {
93
93
  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}`);
94
94
  }
95
- const bytecode = hexToBin(this.bytecode);
96
95
  const encodedArgs = args
97
96
  .map((arg, i) => encodeFunctionArgument(arg, abiFunction.inputs[i].type));
98
97
  const generateUnlockingBytecode = ({ transaction, sourceOutputs, inputIndex }) => {
98
+ const bytecode = hexToBin(this.bytecode);
99
99
  const completeArgs = encodedArgs.map((arg) => {
100
100
  if (!(arg instanceof SignatureTemplate))
101
101
  return arg;
@@ -104,10 +104,10 @@ class ContractInternal extends ContractBase {
104
104
  const sighash = hash256(preimage);
105
105
  return arg.generateSignature(sighash);
106
106
  });
107
- const unlockingBytecode = createUnlockingBytecode(this.contractType, hexToBin(this.bytecode), completeArgs, selector);
107
+ const unlockingBytecode = createUnlockingBytecode(this.contractType, bytecode, completeArgs, selector);
108
108
  return unlockingBytecode;
109
109
  };
110
- const generateLockingBytecode = () => addressToLockScript(this.address);
110
+ const generateLockingBytecode = () => hexToBin(this.lockingBytecode);
111
111
  return { generateUnlockingBytecode, generateLockingBytecode, contract: this, params: args, abiFunction };
112
112
  };
113
113
  }
package/dist/Errors.d.ts CHANGED
@@ -17,6 +17,12 @@ export declare class OutputTokenCategoryInvalidError extends Error {
17
17
  export declare class OutputTokenCommitmentInvalidError extends Error {
18
18
  constructor(commitment: string);
19
19
  }
20
+ export declare class OutputBchChangeLockedError extends Error {
21
+ constructor();
22
+ }
23
+ export declare class OutputTokenChangeLockedError extends Error {
24
+ constructor(category: string);
25
+ }
20
26
  export declare class TokensToNonTokenAddressError extends Error {
21
27
  constructor(address: string);
22
28
  }
package/dist/Errors.js CHANGED
@@ -29,6 +29,16 @@ export class OutputTokenCommitmentInvalidError extends Error {
29
29
  super(`Provided token commitment ${commitment} is not a hex string`);
30
30
  }
31
31
  }
32
+ export class OutputBchChangeLockedError extends Error {
33
+ constructor() {
34
+ super('Tried to add a BCH input or output after a BCH change output was already added');
35
+ }
36
+ }
37
+ export class OutputTokenChangeLockedError extends Error {
38
+ constructor(category) {
39
+ super(`Tried to add a token input or output with category ${category} after a change output with the same category was already added`);
40
+ }
41
+ }
32
42
  export class TokensToNonTokenAddressError extends Error {
33
43
  constructor(address) {
34
44
  super(`Tried to send tokens to an address without token support, ${address}.`);
@@ -72,7 +82,10 @@ export class FailedRequireError extends FailedTransactionError {
72
82
  const { statement, lineNumber } = getLocationDataForInstructionPointer(artifact, failingInstructionPointer);
73
83
  const baseMessage = `${artifact.contractName}.cash:${lineNumber} Require statement failed at input ${inputIndex} in contract ${artifact.contractName}.cash at line ${lineNumber}`;
74
84
  const baseMessageWithRequireMessage = `${baseMessage} with the following message: ${requireStatement.message}`;
75
- const fullMessage = `${requireStatement.message ? baseMessageWithRequireMessage : baseMessage}.\nFailing statement: ${statement}`;
85
+ const headline = `${requireStatement.message ? baseMessageWithRequireMessage : baseMessage}.`;
86
+ // Compiler-injected guards (e.g. the tx.locktime guard) have no user-written source, so the
87
+ // extracted statement is empty — the require message fully describes the failure on its own.
88
+ const fullMessage = statement.trim() ? `${headline}\nFailing statement: ${statement}` : headline;
76
89
  super(fullMessage, bitauthUri);
77
90
  this.artifact = artifact;
78
91
  this.failingInstructionPointer = failingInstructionPointer;
@@ -1,5 +1,5 @@
1
1
  import { WalletTemplate } from '@bitauth/libauth';
2
- import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions, VmResourceUsage, BchChangeOutputOptions } from './interfaces.js';
2
+ import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions, VmResourceUsage, BchChangeOutputOptions, TokenChangeOutputOptions } 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';
@@ -30,6 +30,7 @@ export declare class TransactionBuilder {
30
30
  outputs: Output[];
31
31
  locktime: number;
32
32
  options: TransactionBuilderOptions;
33
+ private changeLocks;
33
34
  /**
34
35
  * Create a new TransactionBuilder.
35
36
  *
@@ -95,11 +96,28 @@ export declare class TransactionBuilder {
95
96
  * fee is computed from the transaction size at the configured fee rate; dust-sized change is
96
97
  * simply absorbed into the fee.
97
98
  *
99
+ * Should be called *after* all explicit inputs and outputs are added.
100
+ *
98
101
  * @param changeOutputOptions - The destination address and the fee rate (in sats/byte) to use.
99
102
  * @returns This builder for chaining.
100
- * @throws If the available surplus is insufficient to cover the fee for the configured rate.
103
+ * @throws If the available surplus is insufficient to cover the fee for the configured rate or
104
+ * if a BCH change output was already added.
101
105
  */
102
106
  addBchChangeOutputIfNeeded(changeOutputOptions: BchChangeOutputOptions): this;
107
+ /**
108
+ * Add a fungible token change output for the configured category if the transaction's inputs
109
+ * contain more tokens of that category than its outputs. The change output is sent to the
110
+ * provided token address and carries the dust-minimum BCH amount.
111
+ *
112
+ * Should be called *after* all explicit token outputs for the category are added and *before*
113
+ * `addBchChangeOutputIfNeeded`.
114
+ *
115
+ * @param changeOutputOptions - The token category to handle and the destination token address.
116
+ * @returns This builder for chaining.
117
+ * @throws If the destination is not a token-supporting address, or if a corresponding change output
118
+ * or BCH change output was already added.
119
+ */
120
+ addTokenChangeOutputIfNeeded(changeOutputOptions: TokenChangeOutputOptions): this;
103
121
  /**
104
122
  * Build the transaction (skipping fee and burn checks) and return its encoded byte length.
105
123
  *
@@ -23,6 +23,7 @@ export class TransactionBuilder {
23
23
  this.inputs = [];
24
24
  this.outputs = [];
25
25
  this.locktime = 0;
26
+ this.changeLocks = {};
26
27
  this.provider = options.provider;
27
28
  this.options = {
28
29
  allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false,
@@ -43,7 +44,7 @@ export class TransactionBuilder {
43
44
  return this.addInputs([utxo], unlocker, options);
44
45
  }
45
46
  addInputs(utxos, unlocker, options) {
46
- utxos.forEach(validateInput);
47
+ utxos.forEach((utxo) => validateInput(utxo, this.changeLocks));
47
48
  if ((!unlocker && utxos.some((utxo) => !isUnlockableUtxo(utxo)))
48
49
  || (unlocker && utxos.some((utxo) => isUnlockableUtxo(utxo)))) {
49
50
  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');
@@ -74,7 +75,7 @@ export class TransactionBuilder {
74
75
  * @throws If any output is invalid.
75
76
  */
76
77
  addOutputs(outputs) {
77
- outputs.forEach((output) => validateOutput(output, this.provider.network));
78
+ outputs.forEach((output) => validateOutput(output, this.provider.network, this.changeLocks));
78
79
  this.outputs = this.outputs.concat(outputs);
79
80
  return this;
80
81
  }
@@ -87,7 +88,7 @@ export class TransactionBuilder {
87
88
  * @returns This builder for chaining.
88
89
  */
89
90
  addOpReturnOutput(chunks) {
90
- this.outputs.push(createOpReturnOutput(chunks));
91
+ this.addOutput(createOpReturnOutput(chunks));
91
92
  return this;
92
93
  }
93
94
  /**
@@ -95,9 +96,12 @@ export class TransactionBuilder {
95
96
  * fee is computed from the transaction size at the configured fee rate; dust-sized change is
96
97
  * simply absorbed into the fee.
97
98
  *
99
+ * Should be called *after* all explicit inputs and outputs are added.
100
+ *
98
101
  * @param changeOutputOptions - The destination address and the fee rate (in sats/byte) to use.
99
102
  * @returns This builder for chaining.
100
- * @throws If the available surplus is insufficient to cover the fee for the configured rate.
103
+ * @throws If the available surplus is insufficient to cover the fee for the configured rate or
104
+ * if a BCH change output was already added.
101
105
  */
102
106
  addBchChangeOutputIfNeeded(changeOutputOptions) {
103
107
  const totalBchInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
@@ -119,9 +123,47 @@ export class TransactionBuilder {
119
123
  const changeOutput = { to: changeOutputOptions.to, amount: changeAmount };
120
124
  const changeOutputDust = calculateDust(changeOutput);
121
125
  if (changeAmount < changeOutputDust) {
126
+ this.changeLocks.BCH = true;
127
+ return this;
128
+ }
129
+ this.addOutput(changeOutput);
130
+ this.changeLocks.BCH = true;
131
+ return this;
132
+ }
133
+ /**
134
+ * Add a fungible token change output for the configured category if the transaction's inputs
135
+ * contain more tokens of that category than its outputs. The change output is sent to the
136
+ * provided token address and carries the dust-minimum BCH amount.
137
+ *
138
+ * Should be called *after* all explicit token outputs for the category are added and *before*
139
+ * `addBchChangeOutputIfNeeded`.
140
+ *
141
+ * @param changeOutputOptions - The token category to handle and the destination token address.
142
+ * @returns This builder for chaining.
143
+ * @throws If the destination is not a token-supporting address, or if a corresponding change output
144
+ * or BCH change output was already added.
145
+ */
146
+ addTokenChangeOutputIfNeeded(changeOutputOptions) {
147
+ const { category, to } = changeOutputOptions;
148
+ const inputAmount = this.inputs
149
+ .filter((input) => input.token?.category === category)
150
+ .reduce((total, input) => total + input.token.amount, 0n);
151
+ const outputAmount = this.outputs
152
+ .filter((output) => output.token?.category === category)
153
+ .reduce((total, output) => total + output.token.amount, 0n);
154
+ const changeAmount = inputAmount - outputAmount;
155
+ if (changeAmount <= 0n) {
156
+ this.changeLocks[category] = true;
122
157
  return this;
123
158
  }
124
- this.outputs.push(changeOutput);
159
+ const changeOutput = {
160
+ to,
161
+ amount: 0n,
162
+ token: { amount: changeAmount, category },
163
+ };
164
+ changeOutput.amount = BigInt(calculateDust(changeOutput));
165
+ this.addOutput(changeOutput);
166
+ this.changeLocks[category] = true;
125
167
  return this;
126
168
  }
127
169
  /**
@@ -318,7 +360,15 @@ export class TransactionBuilder {
318
360
  }
319
361
  catch (e) {
320
362
  const reason = e.error ?? e.message;
321
- throw new FailedTransactionError(reason, getBitauthUri(this.getLibauthTemplate()));
363
+ const getBitauthUriWithFallback = () => {
364
+ try {
365
+ return getBitauthUri(this.getLibauthTemplate());
366
+ }
367
+ catch {
368
+ return 'Bitauth URI generation failed';
369
+ }
370
+ };
371
+ throw new FailedTransactionError(reason, getBitauthUriWithFallback());
322
372
  }
323
373
  }
324
374
  async getTxDetails(txid, raw) {
@@ -62,6 +62,10 @@ export interface BchChangeOutputOptions {
62
62
  to: string | Uint8Array;
63
63
  feeRate: number;
64
64
  }
65
+ export interface TokenChangeOutputOptions {
66
+ category: string;
67
+ to: string | Uint8Array;
68
+ }
65
69
  export interface TokenDetails {
66
70
  amount: bigint;
67
71
  category: string;
@@ -73,8 +73,7 @@ const generateLockingBytecodeParamsMapping = (transactionBuilder) => {
73
73
  if (isContractUnlocker(input.unlocker)) {
74
74
  const lockScriptName = getLockScriptName(input.unlocker.contract);
75
75
  const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName);
76
- const lockingBytecode = binToHex(addressToLockScript(input.unlocker.contract.address));
77
- mapping[lockingBytecode] = lockingScriptParams;
76
+ mapping[input.unlocker.contract.lockingBytecode] = lockingScriptParams;
78
77
  }
79
78
  }
80
79
  return mapping;
@@ -394,7 +393,7 @@ const generateTemplateScenarioTransactionOutputLockingBytecode = (csOutput, liba
394
393
  return lockingBytecodeParams;
395
394
  if (csOutput.to instanceof Uint8Array)
396
395
  return binToHex(csOutput.to);
397
- if (contract && [contract.address, contract.tokenAddress].includes(csOutput.to))
396
+ if (contract && contract.contractType !== 'p2s' && [contract.address, contract.tokenAddress].includes(csOutput.to))
398
397
  return {};
399
398
  return binToHex(addressToLockScript(csOutput.to));
400
399
  };
@@ -1,10 +1,13 @@
1
- import { bytecodeToScript, formatBitAuthScript } from '@cashscript/utils';
1
+ import { bytecodeToScript, formatBitAuthScript, sha256 } from '@cashscript/utils';
2
2
  import { HashType, SignatureAlgorithm, VmTarget } from '../interfaces.js';
3
3
  import { hexToBin, binToHex, isHex, decodeCashAddress, assertSuccess, decodeAuthenticationInstructions } from '@bitauth/libauth';
4
4
  import { zip } from '../utils.js';
5
5
  import SignatureTemplate from '../SignatureTemplate.js';
6
6
  export const DEFAULT_VM_TARGET = VmTarget.BCH_2026_05;
7
7
  export const getLockScriptName = (contract) => {
8
+ if (contract.contractType === 'p2s') {
9
+ return `${contract.artifact.contractName}_${binToHex(sha256(hexToBin(contract.lockingBytecode)))}_lock`;
10
+ }
8
11
  const result = decodeCashAddress(contract.address);
9
12
  if (typeof result === 'string')
10
13
  throw new Error(result);
@@ -56,3 +56,6 @@ export default class MockNetworkProvider implements NetworkProvider {
56
56
  */
57
57
  reset(): void;
58
58
  }
59
+ export declare class FailingMockNetworkProvider extends MockNetworkProvider {
60
+ sendRawTransaction(_txHex: string): Promise<string>;
61
+ }
@@ -98,4 +98,9 @@ export default class MockNetworkProvider {
98
98
  this.transactionMap = {};
99
99
  }
100
100
  }
101
+ export class FailingMockNetworkProvider extends MockNetworkProvider {
102
+ async sendRawTransaction(_txHex) {
103
+ throw new Error('broadcast failed');
104
+ }
105
+ }
101
106
  //# sourceMappingURL=MockNetworkProvider.js.map
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Transaction } from '@bitauth/libauth';
2
2
  import { Script } from '@cashscript/utils';
3
3
  import { Utxo, Output, Network, LibauthOutput, TokenDetails, AddressType, UnlockableUtxo, LibauthTokenDetails, ContractType } from './interfaces.js';
4
- export declare function validateInput(utxo: Utxo): void;
5
- export declare function validateOutput(output: Output, network: Network): void;
4
+ export declare function validateInput(utxo: Utxo, changeLocks: Record<string, boolean>): void;
5
+ export declare function validateOutput(output: Output, network: Network, changeLocks: Record<string, boolean>): void;
6
6
  export declare function isOpReturnOutput(output: Output): boolean;
7
7
  export declare function calculateDust(output: Output): number;
8
8
  export declare function getOutputSize(output: Output): number;
package/dist/utils.js CHANGED
@@ -2,14 +2,16 @@ import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLocki
2
2
  import { encodeInt, hash160, hash256, sha256, Op, scriptToBytecode, encodeNullDataScript, } from '@cashscript/utils';
3
3
  import { Network, } from './interfaces.js';
4
4
  import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
5
- import { OutputSatoshisTooSmallError, OutputTokenAmountTooSmallError, TokensToNonTokenAddressError, UndefinedInputError, OutputAddressNetworkMismatchError, OutputTokenCategoryInvalidError, OutputTokenCommitmentInvalidError, } from './Errors.js';
5
+ import { OutputSatoshisTooSmallError, OutputTokenAmountTooSmallError, TokensToNonTokenAddressError, UndefinedInputError, OutputAddressNetworkMismatchError, OutputTokenCategoryInvalidError, OutputTokenCommitmentInvalidError, OutputBchChangeLockedError, OutputTokenChangeLockedError, } from './Errors.js';
6
6
  // ////////// PARAMETER VALIDATION ////////////////////////////////////////////
7
- export function validateInput(utxo) {
7
+ export function validateInput(utxo, changeLocks) {
8
8
  if (!utxo) {
9
9
  throw new UndefinedInputError();
10
10
  }
11
+ validateChangeLocks(changeLocks, utxo.token?.category);
11
12
  }
12
- export function validateOutput(output, network) {
13
+ export function validateOutput(output, network, changeLocks) {
14
+ validateChangeLocks(changeLocks, output.token?.category);
13
15
  if (isOpReturnOutput(output))
14
16
  return;
15
17
  const minimumAmount = calculateDust(output);
@@ -39,6 +41,14 @@ export function validateOutput(output, network) {
39
41
  throw new OutputAddressNetworkMismatchError(output.to, networkPrefix);
40
42
  }
41
43
  }
44
+ function validateChangeLocks(changeLocks, category) {
45
+ if (changeLocks.BCH) {
46
+ throw new OutputBchChangeLockedError();
47
+ }
48
+ if (category && changeLocks[category]) {
49
+ throw new OutputTokenChangeLockedError(category);
50
+ }
51
+ }
42
52
  export function isOpReturnOutput(output) {
43
53
  return typeof output.to !== 'string' && output.to[0] === Op.OP_RETURN;
44
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cashscript",
3
- "version": "0.13.0-next.7",
3
+ "version": "0.13.0-next.9",
4
4
  "description": "Easily write and interact with Bitcoin Cash contracts",
5
5
  "keywords": [
6
6
  "bitcoin cash",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@bitauth/libauth": "^3.1.0-next.8",
45
- "@cashscript/utils": "^0.13.0-next.7",
45
+ "@cashscript/utils": "^0.13.0-next.9",
46
46
  "@electrum-cash/network": "^4.1.3",
47
47
  "fflate": "^0.8.2",
48
48
  "semver": "^7.7.2"
@@ -56,5 +56,5 @@
56
56
  "typescript": "^5.9.2",
57
57
  "vitest": "^4.0.15"
58
58
  },
59
- "gitHead": "4e8e91589e1218a7c04db1a4772f625efb0140e9"
59
+ "gitHead": "1bbda54a000f6744bdb67a6379c7bfb0b784dceb"
60
60
  }