cashscript 0.13.0-next.7 → 0.13.0-next.8

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/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}.`);
@@ -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,14 @@ 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
+ try {
364
+ const bitauthUri = getBitauthUri(this.getLibauthTemplate());
365
+ throw new FailedTransactionError(reason, bitauthUri);
366
+ }
367
+ catch {
368
+ // Preserve the original broadcast failure reason if URI generation fails
369
+ throw new FailedTransactionError(reason, 'Bitauth URI generation failed');
370
+ }
322
371
  }
323
372
  }
324
373
  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;
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.8",
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.8",
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": "579998062f2f9c52393568b09b580407228cb79f"
60
60
  }