@mysten/sui 2.17.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/grpc/proto/sui/rpc/v2/ledger_service.client.d.mts +4 -4
  3. package/dist/grpc/proto/sui/rpc/v2/move_package_service.client.d.mts +4 -4
  4. package/dist/grpc/proto/sui/rpc/v2/name_service.client.d.mts +4 -4
  5. package/dist/grpc/proto/sui/rpc/v2/state_service.client.d.mts +4 -4
  6. package/dist/grpc/proto/sui/rpc/v2/subscription_service.client.d.mts +4 -4
  7. package/dist/transactions/Transaction.d.mts +16 -2
  8. package/dist/transactions/Transaction.d.mts.map +1 -1
  9. package/dist/transactions/Transaction.mjs +8 -3
  10. package/dist/transactions/Transaction.mjs.map +1 -1
  11. package/dist/transactions/index.d.mts +2 -2
  12. package/dist/version.mjs +1 -1
  13. package/dist/version.mjs.map +1 -1
  14. package/dist/zklogin/jwt-utils.d.mts.map +1 -1
  15. package/dist/zklogin/jwt-utils.mjs.map +1 -1
  16. package/dist/zklogin/utils.d.mts.map +1 -1
  17. package/dist/zklogin/utils.mjs +9 -0
  18. package/dist/zklogin/utils.mjs.map +1 -1
  19. package/docs/bcs.md +11 -6
  20. package/docs/clients/core.md +10 -9
  21. package/docs/clients/grpc.md +1 -1
  22. package/docs/index.md +176 -22
  23. package/docs/llms-index.md +6 -9
  24. package/docs/migrations/0.38.md +2 -2
  25. package/docs/migrations/sui-1.0.md +2 -2
  26. package/docs/migrations/sui-2.0/json-rpc-migration.md +76 -33
  27. package/docs/plugins.md +29 -5
  28. package/docs/transactions/basics.md +279 -0
  29. package/docs/transactions/coins-and-balances.md +293 -0
  30. package/docs/transactions/offline.md +192 -0
  31. package/docs/transactions/reference.md +380 -0
  32. package/docs/transactions/signing-and-execution.md +401 -0
  33. package/package.json +26 -26
  34. package/src/transactions/Transaction.ts +36 -5
  35. package/src/transactions/index.ts +1 -0
  36. package/src/version.ts +1 -1
  37. package/src/zklogin/jwt-utils.ts +3 -0
  38. package/src/zklogin/utils.ts +17 -0
  39. package/docs/faucet.md +0 -26
  40. package/docs/hello-sui.md +0 -115
  41. package/docs/install.md +0 -61
  42. package/docs/transaction-building/basics.md +0 -299
  43. package/docs/transaction-building/gas.md +0 -61
  44. package/docs/transaction-building/intents.md +0 -62
  45. package/docs/transaction-building/offline.md +0 -73
  46. package/docs/transaction-building/sponsored-transactions.md +0 -22
@@ -0,0 +1,401 @@
1
+ # Signing and Execution
2
+
3
+ > Sign transactions and execute them on the Sui network
4
+
5
+ Once you've built a transaction, you need to sign it and submit it to the network. For background on
6
+ how transaction signing and finality work at the protocol level, see the Sui documentation on
7
+ [transaction lifecycle](https://docs.sui.io/concepts/transactions/transaction-lifecycle) and
8
+ [transaction auth](https://docs.sui.io/concepts/transactions/transaction-auth).
9
+
10
+ ## Simulating transactions
11
+
12
+ Use `simulateTransaction` to dry-run a transaction without executing it. This is useful for
13
+ estimating gas costs, checking return values, and validating transactions before executing.
14
+
15
+ ```typescript
16
+
17
+ const grpcClient = new SuiGrpcClient({
18
+ network: 'mainnet',
19
+ baseUrl: 'https://fullnode.mainnet.sui.io:443',
20
+ });
21
+
22
+ const result = await grpcClient.simulateTransaction({
23
+ transaction: tx,
24
+ include: {
25
+ effects: true,
26
+ balanceChanges: true,
27
+ commandResults: true,
28
+ },
29
+ });
30
+
31
+ if (result.$kind === 'FailedTransaction') {
32
+ console.error('Simulation failed:', result.FailedTransaction.status.error?.message);
33
+ } else {
34
+ console.log('Balance changes:', result.Transaction.balanceChanges);
35
+ console.log('Command results:', result.commandResults);
36
+ }
37
+ ```
38
+
39
+ ### `include` options
40
+
41
+ The `include` parameter controls what data is returned from the simulation. All fields are optional
42
+ and default to `false`:
43
+
44
+ | Field | Description |
45
+ | ---------------- | -------------------------------------------------------------------------------------- |
46
+ | `effects` | Execution effects: created, mutated, and deleted objects, gas usage |
47
+ | `events` | Move events emitted during execution |
48
+ | `balanceChanges` | Token balance changes for each affected address and coin type |
49
+ | `objectTypes` | Map of object ID to type string for all changed objects |
50
+ | `transaction` | The full transaction data (sender, commands, gas config) |
51
+ | `bcs` | Raw BCS-encoded transaction bytes |
52
+ | `commandResults` | BCS-encoded return values and mutated references from each command _(simulation only)_ |
53
+
54
+ The `commandResults` field is unique to simulation. It is not available on `executeTransaction`.
55
+ Each entry contains `returnValues` and `mutatedReferences`, both as BCS-encoded `Uint8Array` values
56
+ that you can decode with the [BCS library](/bcs).
57
+
58
+ ## With a keypair (backend or scripts)
59
+
60
+ The simplest approach. The keypair signs the transaction and submits it to the network in one call:
61
+
62
+ ```typescript
63
+
64
+ const keypair = Ed25519Keypair.fromSecretKey('suiprivkey1...');
65
+ const grpcClient = new SuiGrpcClient({
66
+ network: 'mainnet',
67
+ baseUrl: 'https://fullnode.mainnet.sui.io:443',
68
+ });
69
+
70
+ const result = await keypair.signAndExecuteTransaction({
71
+ transaction: tx,
72
+ client: grpcClient,
73
+ });
74
+ ```
75
+
76
+ `fromSecretKey` accepts a Bech32-encoded private key (`suiprivkey1...`) or a raw 32-byte
77
+ `Uint8Array`. You can also use `Ed25519Keypair.deriveKeypair(mnemonic)` to derive from a mnemonic
78
+ phrase.
79
+
80
+ This method automatically sets the sender to the keypair's address, builds the transaction, signs
81
+ it, and executes it. The result includes the transaction data and effects by default.
82
+
83
+ Available keypair types:
84
+
85
+ - `Ed25519Keypair` from `@mysten/sui/keypairs/ed25519`
86
+ - `Secp256k1Keypair` from `@mysten/sui/keypairs/secp256k1`
87
+ - `Secp256r1Keypair` from `@mysten/sui/keypairs/secp256r1`
88
+ - `PasskeyKeypair` from `@mysten/sui/keypairs/passkey`
89
+
90
+ ## With dapp-kit (React frontend)
91
+
92
+ In a React app, use the `useSignAndExecuteTransaction` hook. The user's connected wallet signs and
93
+ submits the transaction:
94
+
95
+ ```tsx
96
+
97
+ function SendButton() {
98
+ const { mutate: signAndExecute } = useSignAndExecuteTransaction();
99
+
100
+ function handleClick() {
101
+ const tx = new Transaction();
102
+ // ... build your transaction ...
103
+
104
+ signAndExecute(
105
+ { transaction: tx },
106
+ {
107
+ onSuccess: (result) => {
108
+ console.log('Transaction digest:', result.digest);
109
+ },
110
+ onError: (error) => {
111
+ console.error('Transaction failed:', error);
112
+ },
113
+ },
114
+ );
115
+ }
116
+
117
+ return <button onClick={handleClick}>Send</button>;
118
+ }
119
+ ```
120
+
121
+ See the [dapp-kit documentation](/dapp-kit) for full setup instructions.
122
+
123
+ ## Sign without executing
124
+
125
+ Sometimes you need the signature without immediately executing, such as for multi-sig, sponsored
126
+ transactions, or delayed execution.
127
+
128
+ ### With a keypair
129
+
130
+ ```typescript
131
+ // Build the transaction bytes first
132
+ tx.setSender(keypair.toSuiAddress());
133
+ const bytes = await tx.build({ client: grpcClient });
134
+
135
+ // Sign the bytes
136
+ const { signature } = await keypair.signTransaction(bytes);
137
+ ```
138
+
139
+ ### With dapp-kit
140
+
141
+ ```tsx
142
+
143
+ function SignButton() {
144
+ const { mutate: signTransaction } = useSignTransaction();
145
+
146
+ function handleClick() {
147
+ const tx = new Transaction();
148
+ // ... build your transaction ...
149
+
150
+ signTransaction(
151
+ { transaction: tx },
152
+ {
153
+ onSuccess: ({ bytes, signature }) => {
154
+ // Send bytes + signature to your backend, sponsor, etc.
155
+ },
156
+ },
157
+ );
158
+ }
159
+
160
+ return <button onClick={handleClick}>Sign</button>;
161
+ }
162
+ ```
163
+
164
+ ## Manual execution
165
+
166
+ When you already have the transaction bytes and signature(s), execute directly through the client:
167
+
168
+ ```typescript
169
+ const result = await grpcClient.executeTransaction({
170
+ transaction: bytes, // Uint8Array
171
+ signatures: [signature], // string[]
172
+ include: {
173
+ effects: true,
174
+ events: true,
175
+ balanceChanges: true,
176
+ objectTypes: true,
177
+ },
178
+ });
179
+ ```
180
+
181
+ The `include` parameter controls what data is returned with the result:
182
+
183
+ | Field | Description |
184
+ | ---------------- | -------------------------------------------------------- |
185
+ | `transaction` | The full transaction data (sender, commands, gas) |
186
+ | `effects` | Execution effects (created, mutated, or deleted objects) |
187
+ | `events` | Move events emitted during execution |
188
+ | `balanceChanges` | Token balance changes for each affected address |
189
+ | `objectTypes` | Map of object ID to type for changed objects |
190
+ | `bcs` | Raw BCS bytes of the transaction |
191
+
192
+ ## Observing results
193
+
194
+ ### Checking success or failure
195
+
196
+ Every transaction result is a discriminated union. Always check which variant you received:
197
+
198
+ ```typescript
199
+ const result = await keypair.signAndExecuteTransaction({
200
+ transaction: tx,
201
+ client: grpcClient,
202
+ });
203
+
204
+ if (result.$kind === 'FailedTransaction') {
205
+ const { status } = result.FailedTransaction;
206
+ console.error('Transaction failed:', status.error?.message);
207
+ } else {
208
+ console.log('Transaction succeeded:', result.Transaction.digest);
209
+ }
210
+ ```
211
+
212
+ You can also use the shorthand:
213
+
214
+ ```typescript
215
+ const tx = result.Transaction ?? result.FailedTransaction;
216
+ if (!tx.status.success) {
217
+ throw new Error(`Failed: ${tx.status.error?.message}`);
218
+ }
219
+ ```
220
+
221
+ ### Waiting for indexing
222
+
223
+ After a transaction executes, read APIs (like `getBalance` or `getObject`) might not immediately
224
+ show the effects. You must also wait before executing a subsequent transaction that depends on
225
+ objects created or modified by the first one. Use `waitForTransaction` to ensure consistency:
226
+
227
+ ```typescript
228
+ const result = await keypair.signAndExecuteTransaction({
229
+ transaction: tx,
230
+ client: grpcClient,
231
+ });
232
+
233
+ // Wait for the transaction to be indexed
234
+ await grpcClient.waitForTransaction({ result });
235
+
236
+ // Now reads will reflect the transaction's effects
237
+ const { balance } = await grpcClient.getBalance({ owner: myAddress });
238
+ ```
239
+
240
+ You can also wait by digest:
241
+
242
+ ```typescript
243
+ await grpcClient.waitForTransaction({ digest: result.Transaction.digest });
244
+ ```
245
+
246
+ ## Gas configuration
247
+
248
+ Every Sui transaction costs gas. The SDK handles gas automatically in most cases. It sets the gas
249
+ price, estimates the budget, and selects how to pay. You only need to configure gas explicitly for
250
+ special cases.
251
+
252
+ ### Defaults
253
+
254
+ When you don't configure gas explicitly, the SDK:
255
+
256
+ 1. **Gas price**: Uses the network's reference gas price
257
+ 2. **Gas budget**: Simulates the transaction and uses the result to set an appropriate budget
258
+ 3. **Gas payment**: Uses address balances, falling back to coin objects when the balance is below
259
+ the budget
260
+
261
+ For most transactions, the defaults work well and you don't need to change anything.
262
+
263
+ ### Explicit gas settings
264
+
265
+ ```typescript
266
+ // Override the reference gas price
267
+ tx.setGasPrice(1500);
268
+
269
+ // Override the simulated budget (in MIST)
270
+ tx.setGasBudget(50_000_000);
271
+ ```
272
+
273
+ ### Gas payment with coin objects
274
+
275
+ Specify exactly which coins to use for gas. The system merges these coins into a single gas coin
276
+ before execution:
277
+
278
+ ```typescript
279
+ tx.setGasPayment([
280
+ { objectId: '0xCoin1', version: '1', digest: 'abc...' },
281
+ { objectId: '0xCoin2', version: '2', digest: 'def...' },
282
+ ]);
283
+ ```
284
+
285
+ The coins you provide must not overlap with any object inputs in your transaction.
286
+
287
+ ### Gas payment with address balance
288
+
289
+ To pay gas from your address balance instead of coin objects, pass an empty array:
290
+
291
+ ```typescript
292
+ tx.setGasPayment([]);
293
+ ```
294
+
295
+ This is particularly useful for [offline building](./offline) and
296
+ [sponsored transactions](#sponsored-transactions), because there are no coin object versions to look
297
+ up or coordinate.
298
+
299
+ ### The gas coin (`tx.gas`)
300
+
301
+ `tx.gas` references the coin used for gas payment. You can split SUI from it, merge coins into it,
302
+ or transfer it to another address:
303
+
304
+ ```typescript
305
+ const [coin] = tx.splitCoins(tx.gas, [1_000_000_000]);
306
+ tx.transferObjects([coin], '0xRecipientAddress');
307
+ ```
308
+
309
+ > **Warning:** `tx.gas` only works when gas is paid from coin objects. If gas is paid from address balances
310
+ > (through `setGasPayment([])`), `tx.gas` is not available. Use [`tx.coin()`](./coins-and-balances)
311
+ > instead, which works in both cases.
312
+
313
+ ## Sponsored transactions
314
+
315
+ In a sponsored transaction, someone other than the sender pays for gas. There are two flows
316
+ depending on whether gas is paid from coin objects or address balances.
317
+
318
+ ### Coin-based sponsorship
319
+
320
+ The traditional flow where the sponsor provides specific gas coin objects:
321
+
322
+ ```typescript
323
+ // 1. User builds transaction kind bytes (no gas info)
324
+ const tx = new Transaction();
325
+ // ... add commands ...
326
+ const kindBytes = await tx.build({ client: grpcClient, onlyTransactionKind: true });
327
+
328
+ // 2. Sponsor wraps with gas info
329
+ const sponsoredTx = Transaction.fromKind(kindBytes);
330
+ sponsoredTx.setSender(userAddress);
331
+ sponsoredTx.setGasOwner(sponsorAddress);
332
+ sponsoredTx.setGasPayment(sponsorGasCoins);
333
+
334
+ // 3. Build the full transaction
335
+ const fullBytes = await sponsoredTx.build({ client: grpcClient });
336
+
337
+ // 4. Both parties sign
338
+ const { signature: userSignature } = await userKeypair.signTransaction(fullBytes);
339
+ const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(fullBytes);
340
+
341
+ // 5. Execute with both signatures
342
+ const result = await grpcClient.executeTransaction({
343
+ transaction: fullBytes,
344
+ signatures: [userSignature, sponsorSignature],
345
+ });
346
+ ```
347
+
348
+ > **Warning:** The user must wait for the sponsor to set gas coins before signing, because gas coins are part of
349
+ > the signed transaction data.
350
+
351
+ ### Address balance sponsorship
352
+
353
+ When the sponsor pays from their address balance instead of specific coins, the flow is simpler
354
+ because there are no coin object references to coordinate:
355
+
356
+ ```typescript
357
+ // 1. User builds and signs the transaction first
358
+ const tx = new Transaction();
359
+ tx.setSender(userAddress);
360
+ tx.setGasOwner(sponsorAddress);
361
+ tx.setGasPayment([]); // empty array = use address balance for gas
362
+ // ... add commands ...
363
+
364
+ const bytes = await tx.build({ client: grpcClient });
365
+ const { signature: userSignature } = await userKeypair.signTransaction(bytes);
366
+
367
+ // 2. Sponsor signs (can happen asynchronously)
368
+ const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(bytes);
369
+
370
+ // 3. Either party executes
371
+ const result = await grpcClient.executeTransaction({
372
+ transaction: bytes,
373
+ signatures: [userSignature, sponsorSignature],
374
+ });
375
+ ```
376
+
377
+ The key advantage: the sender can sign before the sponsor, enabling simpler async flows. No need to
378
+ coordinate gas coin selection.
379
+
380
+ ### Sending tokens in sponsored transactions
381
+
382
+ When building sponsored transactions, set `useGasCoin: false` so the SDK doesn't try to split the
383
+ gas coin (which belongs to the sponsor):
384
+
385
+ ```typescript
386
+
387
+ const tx = new Transaction();
388
+
389
+ // Send to address balance (preferred)
390
+ tx.moveCall({
391
+ target: '0x2::balance::send_funds',
392
+ typeArguments: ['0x2::sui::SUI'],
393
+ arguments: [
394
+ tx.balance({ balance: 1_000_000_000n, useGasCoin: false }),
395
+ tx.pure.address('0xRecipientAddress'),
396
+ ],
397
+ });
398
+
399
+ // Or send as a coin object
400
+ tx.transferObjects([tx.coin({ balance: 1_000_000_000, useGasCoin: false })], '0xRecipientAddress');
401
+ ```
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "author": "Mysten Labs <build@mystenlabs.com>",
4
4
  "description": "Sui TypeScript API",
5
5
  "homepage": "https://sdk.mystenlabs.com",
6
- "version": "2.17.0",
6
+ "version": "2.18.0",
7
7
  "license": "Apache-2.0",
8
8
  "sideEffects": false,
9
9
  "files": [
@@ -131,45 +131,45 @@
131
131
  "access": "public"
132
132
  },
133
133
  "devDependencies": {
134
- "@0no-co/graphqlsp": "^1.15.2",
135
- "@graphql-codegen/add": "^6.0.0",
136
- "@graphql-codegen/cli": "^6.1.1",
137
- "@graphql-codegen/typed-document-node": "^6.1.5",
138
- "@graphql-codegen/typescript": "5.0.7",
139
- "@graphql-codegen/typescript-document-nodes": "5.0.7",
140
- "@graphql-codegen/typescript-operations": "^5.0.7",
141
- "@grpc/grpc-js": "^1.13.3",
134
+ "@0no-co/graphqlsp": "^1.17.0",
135
+ "@graphql-codegen/add": "^7.0.1",
136
+ "@graphql-codegen/cli": "^7.1.2",
137
+ "@graphql-codegen/typed-document-node": "^7.0.3",
138
+ "@graphql-codegen/typescript": "6.0.2",
139
+ "@graphql-codegen/typescript-document-nodes": "6.0.1",
140
+ "@graphql-codegen/typescript-operations": "^6.0.3",
141
+ "@grpc/grpc-js": ">=1.14.4",
142
142
  "@parcel/watcher": "^2.5.4",
143
143
  "@protobuf-ts/grpc-transport": "^2.11.1",
144
- "@types/node": "^25.0.8",
144
+ "@types/node": "^25.9.3",
145
145
  "@types/tmp": "^0.2.6",
146
146
  "cross-env": "^10.1.0",
147
147
  "graphql-config": "^5.1.5",
148
- "msw": "^2.12.7",
149
- "tmp": "^0.2.5",
148
+ "msw": "^2.14.6",
149
+ "tmp": "^0.2.7",
150
150
  "ts-retry-promise": "^0.8.1",
151
- "typescript": "^5.9.3",
152
- "vite": "^8.0.5",
151
+ "typescript": "^6.0.3",
152
+ "vite": "^8.0.16",
153
153
  "vite-tsconfig-paths": "^6.0.4",
154
- "vitest": "^4.0.17",
155
- "wait-on": "^9.0.3"
154
+ "vitest": "^4.1.8",
155
+ "wait-on": "^9.0.10"
156
156
  },
157
157
  "dependencies": {
158
158
  "@graphql-typed-document-node/core": "^3.2.0",
159
- "@noble/curves": "^2.0.1",
160
- "@noble/hashes": "^2.0.1",
159
+ "@noble/curves": "^2.2.0",
160
+ "@noble/hashes": "^2.2.0",
161
161
  "@protobuf-ts/grpcweb-transport": "^2.11.1",
162
162
  "@protobuf-ts/runtime": "^2.11.1",
163
163
  "@protobuf-ts/runtime-rpc": "^2.11.1",
164
- "@scure/base": "^2.0.0",
165
- "@scure/bip32": "^2.0.1",
166
- "@scure/bip39": "^2.0.1",
167
- "gql.tada": "^1.9.0",
168
- "graphql": "^16.12.0",
164
+ "@scure/base": "^2.2.0",
165
+ "@scure/bip32": "^2.2.0",
166
+ "@scure/bip39": "^2.2.0",
167
+ "gql.tada": "^1.10.1",
168
+ "graphql": "^16.14.2",
169
169
  "poseidon-lite": "0.2.1",
170
- "valibot": "^1.2.0",
171
- "@mysten/bcs": "^2.0.5",
172
- "@mysten/utils": "^0.3.3"
170
+ "valibot": "^1.4.1",
171
+ "@mysten/utils": "^0.4.0",
172
+ "@mysten/bcs": "^2.1.0"
173
173
  },
174
174
  "scripts": {
175
175
  "clean": "rm -rf tsconfig.tsbuildinfo ./dist",
@@ -139,6 +139,17 @@ type TransactionLike = {
139
139
  getData(): unknown;
140
140
  };
141
141
 
142
+ export interface TransactionCopyOptions {
143
+ /**
144
+ * A map of intent names to resolvers for any custom intents used in the transaction being copied.
145
+ *
146
+ * Built-in intents (such as `CoinWithBalance`) are handled automatically. Providing resolvers for
147
+ * custom intents lets `Transaction.from` copy a transaction synchronously even when it still
148
+ * contains unresolved intents, without first awaiting `prepareForSerialization`.
149
+ */
150
+ intentResolvers?: Record<string, TransactionPlugin>;
151
+ }
152
+
142
153
  /**
143
154
  * Transaction Builder
144
155
  */
@@ -175,8 +186,15 @@ export class Transaction {
175
186
  * There are two supported serialized formats:
176
187
  * - A string returned from `Transaction#serialize`. The serialized format must be compatible, or it will throw an error.
177
188
  * - A byte array (or base64-encoded bytes) containing BCS transaction data.
189
+ *
190
+ * When copying an in-memory transaction that uses custom intents, pass resolvers for those intents
191
+ * via `options.intentResolvers` so the copy can be created synchronously without first awaiting
192
+ * `prepareForSerialization`. Built-in intents (such as `CoinWithBalance`) are handled automatically.
178
193
  */
179
- static from(transaction: string | Uint8Array | TransactionLike) {
194
+ static from(
195
+ transaction: string | Uint8Array | TransactionLike,
196
+ options: TransactionCopyOptions = {},
197
+ ) {
180
198
  const newTransaction = new Transaction();
181
199
 
182
200
  if (isTransaction(transaction)) {
@@ -195,14 +213,27 @@ export class Transaction {
195
213
  newTransaction.#commandSection = newTransaction.#data.commands.slice();
196
214
  newTransaction.#availableResults = new Set(newTransaction.#commandSection.map((_, i) => i));
197
215
 
198
- if (!newTransaction.isPreparedForSerialization({ supportedIntents: [COIN_WITH_BALANCE] })) {
216
+ // Built-in intents are resolvable by default. Caller-supplied resolvers cover custom intents,
217
+ // and take precedence so a built-in resolver can be overridden if needed.
218
+ const intentResolvers = new Map<string, TransactionPlugin>([
219
+ [COIN_WITH_BALANCE, resolveCoinBalance],
220
+ ...Object.entries(options.intentResolvers ?? {}),
221
+ ]);
222
+
223
+ if (
224
+ !newTransaction.isPreparedForSerialization({
225
+ supportedIntents: [...intentResolvers.keys()],
226
+ })
227
+ ) {
199
228
  throw new Error(
200
- 'Transaction has unresolved intents or async thunks. Call `prepareForSerialization` before copying.',
229
+ 'Transaction has unresolved intents or async thunks. Provide resolvers for any custom intents via the `intentResolvers` option, or call `prepareForSerialization` before copying.',
201
230
  );
202
231
  }
203
232
 
204
- if (newTransaction.#data.commands.some((cmd) => cmd.$Intent?.name === COIN_WITH_BALANCE)) {
205
- newTransaction.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance);
233
+ // Register every resolver so the copy can resolve its intents on build. Resolvers for intents
234
+ // that aren't present are harmless — a resolver only runs when its intent appears in the data.
235
+ for (const [intent, resolver] of intentResolvers) {
236
+ newTransaction.addIntentResolver(intent, resolver);
206
237
  }
207
238
 
208
239
  return newTransaction;
@@ -17,6 +17,7 @@ export {
17
17
  type TransactionObjectInput,
18
18
  type TransactionObjectArgument,
19
19
  type TransactionResult,
20
+ type TransactionCopyOptions,
20
21
  } from './Transaction.js';
21
22
 
22
23
  export { type SerializedTransactionDataV2 } from './data/v2.js';
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  // This file is generated by genversion.mjs. Do not edit it directly.
5
5
 
6
- export const PACKAGE_VERSION = '2.17.0';
6
+ export const PACKAGE_VERSION = '2.18.0';
@@ -116,6 +116,9 @@ export function extractClaimValue<R>(claim: Claim, claimName: string): R {
116
116
  return value;
117
117
  }
118
118
 
119
+ // TODO: root cause of the claim-escaping bug — jwtDecode resolves JSON escapes, but the
120
+ // circuit derives the address seed from raw JWT bytes, so escaped claim values decode
121
+ // differently here than the circuit hashes. Real fix: parse claims over raw bytes.
119
122
  export function decodeJwt(jwt: string): Omit<JwtPayload, 'iss' | 'aud' | 'sub'> & {
120
123
  iss: string;
121
124
  aud: string;
@@ -87,6 +87,20 @@ export function hashASCIIStrToField(str: string, maxSize: number) {
87
87
  return poseidonHash(packed);
88
88
  }
89
89
 
90
+ // Reject claim inputs whose decoded form reveals a JSON escape ('"', '\', control char):
91
+ // the circuit hashes raw JWT bytes, so an escaped value would derive a different address.
92
+ function assertNoJsonEscape(value: string, label: string) {
93
+ for (let i = 0; i < value.length; i++) {
94
+ const c = value.charCodeAt(i);
95
+ if (c < 0x20 || c === 0x22 || c === 0x5c) {
96
+ throw new Error(
97
+ `zkLogin ${label} contains a JSON-escaped character (code ${c}); the circuit ` +
98
+ `hashes raw JWT bytes, so claim values with escapes are not supported`,
99
+ );
100
+ }
101
+ }
102
+ }
103
+
90
104
  export function genAddressSeed(
91
105
  salt: string | bigint,
92
106
  name: string,
@@ -96,6 +110,9 @@ export function genAddressSeed(
96
110
  max_value_length = MAX_KEY_CLAIM_VALUE_LENGTH,
97
111
  max_aud_length = MAX_AUD_VALUE_LENGTH,
98
112
  ): bigint {
113
+ assertNoJsonEscape(name, 'key claim name');
114
+ assertNoJsonEscape(value, 'key claim value');
115
+ assertNoJsonEscape(aud, 'aud');
99
116
  return poseidonHash([
100
117
  hashASCIIStrToField(name, max_name_length),
101
118
  hashASCIIStrToField(value, max_value_length),
package/docs/faucet.md DELETED
@@ -1,26 +0,0 @@
1
- # Faucet
2
-
3
- > Request test SUI tokens from the faucet on Devnet, Testnet, or local networks.
4
-
5
- Devnet, Testnet, and local networks include faucets that mint SUI. You can use the Sui TypeScript
6
- SDK to call a network's faucet and provide SUI to the address you provide.
7
-
8
- To request SUI from a faucet, import the `requestSuiFromFaucetV2` function from the
9
- `@mysten/sui/faucet` package to your project.
10
-
11
- ```typescript
12
-
13
- ```
14
-
15
- Use `requestSuiFromFaucetV2` in your TypeScript code to request SUI from the network's faucet.
16
-
17
- ```typescript
18
- await requestSuiFromFaucetV2({
19
- host: getFaucetHost('testnet'),
20
- recipient: <RECIPIENT_ADDRESS>,
21
- });
22
- ```
23
-
24
- > **Note:** Faucets on Devnet and Testnet are rate limited. If you run the script too many times, you surpass
25
- > the limit and must wait to successfully run it again. For Testnet, the best way to get SUI is
26
- > through the Web UI: `faucet.sui.io`.