@mysten/sui 2.29.0 → 2.31.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 (86) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/bcs/bcs.d.mts +6 -6
  3. package/dist/bcs/bcs.d.mts.map +1 -1
  4. package/dist/bcs/bcs.mjs +5 -1
  5. package/dist/bcs/bcs.mjs.map +1 -1
  6. package/dist/bcs/index.d.mts +139 -47
  7. package/dist/bcs/index.d.mts.map +1 -1
  8. package/dist/bcs/types.d.mts.map +1 -1
  9. package/dist/client/core-resolver.d.mts.map +1 -1
  10. package/dist/client/core-resolver.mjs +4 -6
  11. package/dist/client/core-resolver.mjs.map +1 -1
  12. package/dist/client/transaction-resolver.mjs +35 -8
  13. package/dist/client/transaction-resolver.mjs.map +1 -1
  14. package/dist/cryptography/signature.d.mts +8 -8
  15. package/dist/graphql/generated/tada-env.d.mts +74 -0
  16. package/dist/grpc/proto/sui/rpc/v2/input.d.mts +21 -1
  17. package/dist/grpc/proto/sui/rpc/v2/input.d.mts.map +1 -1
  18. package/dist/grpc/proto/sui/rpc/v2/input.mjs +21 -0
  19. package/dist/grpc/proto/sui/rpc/v2/input.mjs.map +1 -1
  20. package/dist/grpc/proto/sui/rpc/v2/ledger_service.client.d.mts +4 -4
  21. package/dist/grpc/proto/sui/rpc/v2/move_package_service.client.d.mts +4 -4
  22. package/dist/grpc/proto/sui/rpc/v2/signature_verification_service.client.d.mts +4 -4
  23. package/dist/grpc/proto/sui/rpc/v2/state_service.client.d.mts +4 -4
  24. package/dist/grpc/proto/sui/rpc/v2/subscription_service.client.d.mts +4 -4
  25. package/dist/grpc/proto/sui/rpc/v2/transaction_execution_service.client.d.mts +4 -4
  26. package/dist/jsonRpc/core.mjs +1 -1
  27. package/dist/jsonRpc/core.mjs.map +1 -1
  28. package/dist/transactions/Transaction.d.mts +48 -46
  29. package/dist/transactions/Transaction.d.mts.map +1 -1
  30. package/dist/transactions/Transaction.mjs +65 -24
  31. package/dist/transactions/Transaction.mjs.map +1 -1
  32. package/dist/transactions/data/internal.d.mts +12 -0
  33. package/dist/transactions/data/internal.d.mts.map +1 -1
  34. package/dist/transactions/data/internal.mjs +5 -1
  35. package/dist/transactions/data/internal.mjs.map +1 -1
  36. package/dist/transactions/data/v2.d.mts +5 -0
  37. package/dist/transactions/data/v2.d.mts.map +1 -1
  38. package/dist/transactions/index.d.mts +3 -2
  39. package/dist/transactions/intents/AllowanceBalance.mjs +147 -0
  40. package/dist/transactions/intents/AllowanceBalance.mjs.map +1 -0
  41. package/dist/transactions/intents/BalanceIntentNames.mjs +7 -0
  42. package/dist/transactions/intents/BalanceIntentNames.mjs.map +1 -0
  43. package/dist/transactions/intents/BalanceOptions.d.mts +27 -0
  44. package/dist/transactions/intents/BalanceOptions.d.mts.map +1 -0
  45. package/dist/transactions/intents/BalanceOptions.mjs +12 -0
  46. package/dist/transactions/intents/BalanceOptions.mjs.map +1 -0
  47. package/dist/transactions/intents/CoinWithBalance.d.mts.map +1 -1
  48. package/dist/transactions/intents/CoinWithBalance.mjs +35 -12
  49. package/dist/transactions/intents/CoinWithBalance.mjs.map +1 -1
  50. package/dist/transactions/intents/ResolveBalances.mjs +16 -0
  51. package/dist/transactions/intents/ResolveBalances.mjs.map +1 -0
  52. package/dist/transactions/resolution-utils.mjs +13 -0
  53. package/dist/transactions/resolution-utils.mjs.map +1 -0
  54. package/dist/transactions/resolve.d.mts +8 -0
  55. package/dist/transactions/resolve.d.mts.map +1 -1
  56. package/dist/transactions/resolve.mjs +7 -3
  57. package/dist/transactions/resolve.mjs.map +1 -1
  58. package/dist/version.mjs +1 -1
  59. package/dist/version.mjs.map +1 -1
  60. package/dist/zklogin/bcs.d.mts +14 -14
  61. package/docs/llms-index.md +1 -0
  62. package/docs/migrations/sui-2.0/json-rpc-migration.md +50 -1
  63. package/docs/transactions/allowances.md +176 -0
  64. package/docs/transactions/basics.md +19 -14
  65. package/docs/transactions/coins-and-balances.md +12 -9
  66. package/docs/transactions/offline.md +137 -88
  67. package/package.json +2 -2
  68. package/src/bcs/bcs.ts +4 -0
  69. package/src/bcs/types.ts +2 -1
  70. package/src/client/core-resolver.ts +8 -11
  71. package/src/client/transaction-resolver.ts +43 -9
  72. package/src/graphql/generated/schema.graphql +87 -42
  73. package/src/graphql/generated/tada-env.ts +84 -0
  74. package/src/grpc/proto/sui/rpc/v2/input.ts +22 -0
  75. package/src/jsonRpc/core.ts +3 -4
  76. package/src/transactions/Transaction.ts +71 -53
  77. package/src/transactions/data/internal.ts +4 -0
  78. package/src/transactions/index.ts +3 -0
  79. package/src/transactions/intents/AllowanceBalance.ts +204 -0
  80. package/src/transactions/intents/BalanceIntentNames.ts +5 -0
  81. package/src/transactions/intents/BalanceOptions.ts +45 -0
  82. package/src/transactions/intents/CoinWithBalance.ts +82 -32
  83. package/src/transactions/intents/ResolveBalances.ts +30 -0
  84. package/src/transactions/resolution-utils.ts +18 -0
  85. package/src/transactions/resolve.ts +35 -2
  86. package/src/version.ts +1 -1
@@ -294,6 +294,10 @@ export type WithdrawalTypeArg = InferOutput<typeof WithdrawalTypeArgSchema>;
294
294
  export const WithdrawFromSchema = safeEnum({
295
295
  Sender: literal(true),
296
296
  Sponsor: literal(true),
297
+ SenderAllowance: object({
298
+ funder: SuiAddress,
299
+ allowance: ObjectID,
300
+ }),
297
301
  });
298
302
  export type WithdrawFrom = InferOutput<typeof WithdrawFromSchema>;
299
303
 
@@ -18,6 +18,7 @@ export {
18
18
  type TransactionObjectArgument,
19
19
  type TransactionResult,
20
20
  type TransactionCopyOptions,
21
+ type WithdrawalOptions,
21
22
  } from './Transaction.js';
22
23
 
23
24
  export { type SerializedTransactionDataV2 } from './data/v2.js';
@@ -48,3 +49,5 @@ export type {
48
49
  export { Arguments } from './Arguments.js';
49
50
 
50
51
  export { isArgument } from './utils.js';
52
+
53
+ export type { BalanceOptions, AllowanceReference } from './intents/BalanceOptions.js';
@@ -0,0 +1,204 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { object, optional, parse, picklist, string } from 'valibot';
5
+
6
+ import { hasMvrName } from '../../client/mvr.js';
7
+
8
+ import { ALLOWANCE_BALANCE } from './BalanceIntentNames.js';
9
+ import { resolveBalances } from './ResolveBalances.js';
10
+
11
+ import { bcs } from '../../bcs/index.js';
12
+ import type { SuiClientTypes } from '../../client/index.js';
13
+ import { normalizeStructTag, normalizeSuiAddress } from '../../utils/sui-types.js';
14
+ import { TransactionCommands } from '../Commands.js';
15
+ import type { Argument } from '../data/internal.js';
16
+ import { Inputs } from '../Inputs.js';
17
+ import { getClient } from '../resolve.js';
18
+ import type { TransactionPlugin } from '../resolve.js';
19
+ import type { Transaction } from '../Transaction.js';
20
+ import type { AllowanceReference } from './BalanceOptions.js';
21
+
22
+ export { ALLOWANCE_BALANCE } from './BalanceIntentNames.js';
23
+
24
+ const AllowanceBalanceData = object({
25
+ objectId: string(),
26
+ funder: optional(string()),
27
+ appType: optional(string()),
28
+ type: string(),
29
+ amount: string(),
30
+ outputKind: picklist(['coin', 'balance']),
31
+ });
32
+
33
+ // Read only the prefix needed for spending. The remaining settings and current_spend
34
+ // are checked by Move, so this does not need to model rate limits or their policy.
35
+ const AllowanceMetadata = bcs.struct('AllowanceMetadata', {
36
+ id: bcs.Address,
37
+ funder: bcs.Address,
38
+ spender: bcs.option(bcs.Address),
39
+ app: bcs.option(bcs.string()),
40
+ });
41
+
42
+ export function allowanceBalance({
43
+ allowance,
44
+ amount,
45
+ type = '0x2::sui::SUI',
46
+ outputKind,
47
+ }: {
48
+ allowance: string | AllowanceReference;
49
+ amount: bigint;
50
+ type?: string;
51
+ outputKind: 'coin' | 'balance';
52
+ }) {
53
+ bcs.U64.validate(amount);
54
+ const objectId = normalizeSuiAddress(
55
+ typeof allowance === 'string' ? allowance : allowance.objectId,
56
+ );
57
+ const reference = typeof allowance === 'string' ? { objectId } : allowance;
58
+ return (tx: Transaction) => {
59
+ tx.addIntentResolver(ALLOWANCE_BALANCE, resolveBalances);
60
+ return tx.add(
61
+ TransactionCommands.Intent({
62
+ name: ALLOWANCE_BALANCE,
63
+ inputs: {
64
+ allowance: tx.object(objectId),
65
+ clock: tx.object.clock(),
66
+ ...(reference.app ? { permit: tx.object(reference.app.permit) } : {}),
67
+ },
68
+ data: {
69
+ objectId,
70
+ funder: reference.funder ? normalizeSuiAddress(reference.funder) : undefined,
71
+ appType: reference.app ? normalizeStructTag(reference.app.type) : undefined,
72
+ type: normalizeStructTag(type),
73
+ amount: String(amount),
74
+ outputKind,
75
+ },
76
+ }),
77
+ );
78
+ };
79
+ }
80
+
81
+ export const resolveAllowanceBalance: TransactionPlugin = async (
82
+ transactionData,
83
+ options,
84
+ next,
85
+ ) => {
86
+ const ids = new Set<string>();
87
+ const namedTypes = new Set<string>();
88
+ for (const command of transactionData.commands) {
89
+ if (command.$kind !== '$Intent' || command.$Intent.name !== ALLOWANCE_BALANCE) continue;
90
+ const data = parse(AllowanceBalanceData, command.$Intent.data);
91
+ if (!data.funder) ids.add(data.objectId);
92
+ for (const type of [data.type, data.appType]) {
93
+ if (type && hasMvrName(type)) namedTypes.add(type);
94
+ }
95
+ }
96
+
97
+ const [resolvedTypes, objects] = await Promise.all([
98
+ namedTypes.size ? getClient(options).core.mvr.resolve({ types: [...namedTypes] }) : undefined,
99
+ ids.size
100
+ ? getClient(options)
101
+ .core.getObjects({ objectIds: [...ids], include: { content: true } })
102
+ .then((result) => result.objects)
103
+ : [],
104
+ ]);
105
+ const allowances = new Map<string, SuiClientTypes.Object<{ content: true }>>();
106
+ for (const object of objects) {
107
+ if (object instanceof Error) throw object;
108
+ allowances.set(object.objectId, object);
109
+ }
110
+
111
+ for (let index = 0; index < transactionData.commands.length; index++) {
112
+ const command = transactionData.commands[index];
113
+ if (command.$kind !== '$Intent' || command.$Intent.name !== ALLOWANCE_BALANCE) continue;
114
+ const data = parse(AllowanceBalanceData, command.$Intent.data);
115
+ if (hasMvrName(data.type)) {
116
+ const resolved = resolvedTypes?.types[data.type];
117
+ if (!resolved) throw new Error(`No resolution found for type: ${data.type}`);
118
+ data.type = normalizeStructTag(resolved.type);
119
+ }
120
+ if (data.appType && hasMvrName(data.appType)) {
121
+ const resolved = resolvedTypes?.types[data.appType];
122
+ if (!resolved) throw new Error(`No resolution found for type: ${data.appType}`);
123
+ data.appType = normalizeStructTag(resolved.type);
124
+ }
125
+ let funder = data.funder;
126
+ if (!funder) {
127
+ const allowance = allowances.get(data.objectId);
128
+ const expectedType = normalizeStructTag(
129
+ `0x2::allowance::Allowance<0x2::balance::Balance<${data.type}>>`,
130
+ );
131
+ if (!allowance || allowance.type !== expectedType || allowance.owner.$kind !== 'Shared') {
132
+ throw new Error(`Expected a shared ${expectedType} allowance at ${data.objectId}`);
133
+ }
134
+ const metadata = AllowanceMetadata.parse(allowance.content);
135
+ if (metadata.app !== null && !data.appType) {
136
+ throw new Error(
137
+ `Allowance ${data.objectId} is app-bound; pass allowance.app with the app type and SpendPermit`,
138
+ );
139
+ }
140
+ if (
141
+ data.appType &&
142
+ (metadata.app === null || normalizeStructTag(metadata.app) !== data.appType)
143
+ ) {
144
+ throw new Error(`Allowance ${data.objectId} is not bound to app ${data.appType}`);
145
+ }
146
+ funder = metadata.funder;
147
+ const input = command.$Intent.inputs.allowance as Argument;
148
+ if (input.$kind === 'Input') {
149
+ transactionData.inputs[input.Input] = Inputs.SharedObjectRef({
150
+ objectId: data.objectId,
151
+ initialSharedVersion: allowance.owner.Shared.initialSharedVersion,
152
+ mutable: true,
153
+ });
154
+ }
155
+ }
156
+ // Spending mutates the allowance even when the caller supplied the funder
157
+ // and we did not fetch metadata. Reuse and upgrade its existing input.
158
+ const allowanceArgument = command.$Intent.inputs.allowance as Argument;
159
+ if (allowanceArgument.$kind === 'Input') {
160
+ const input = transactionData.inputs[allowanceArgument.Input];
161
+ if (input.Object?.SharedObject) input.Object.SharedObject.mutable = true;
162
+ if (input.UnresolvedObject) input.UnresolvedObject.mutable = true;
163
+ }
164
+ const withdrawal = transactionData.addInput(
165
+ 'withdrawal',
166
+ Inputs.FundsWithdrawal({
167
+ reservation: { $kind: 'MaxAmountU64', MaxAmountU64: data.amount },
168
+ typeArg: { $kind: 'Balance', Balance: data.type },
169
+ withdrawFrom: {
170
+ $kind: 'SenderAllowance',
171
+ SenderAllowance: { funder, allowance: data.objectId },
172
+ },
173
+ }),
174
+ );
175
+ const commands = [
176
+ TransactionCommands.MoveCall({
177
+ target: data.appType
178
+ ? '0x2::allowance::app_balance_spend'
179
+ : '0x2::allowance::balance_spend',
180
+ typeArguments: data.appType ? [data.type, data.appType] : [data.type],
181
+ arguments: [
182
+ command.$Intent.inputs.allowance as Argument,
183
+ ...(data.appType ? [command.$Intent.inputs.permit as Argument] : []),
184
+ withdrawal,
185
+ command.$Intent.inputs.clock as Argument,
186
+ ],
187
+ }),
188
+ ];
189
+ if (data.outputKind === 'coin') {
190
+ commands.push(
191
+ TransactionCommands.MoveCall({
192
+ target: '0x2::coin::from_balance',
193
+ typeArguments: [data.type],
194
+ arguments: [{ $kind: 'Result', Result: index }],
195
+ }),
196
+ );
197
+ }
198
+ transactionData.replaceCommand(index, commands, {
199
+ NestedResult: [index + commands.length - 1, 0],
200
+ });
201
+ index += commands.length - 1;
202
+ }
203
+ return next();
204
+ };
@@ -0,0 +1,5 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const ALLOWANCE_BALANCE = 'AllowanceBalance';
5
+ export const COIN_WITH_BALANCE = 'CoinWithBalance';
@@ -0,0 +1,45 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { TransactionObjectArgument } from '../Transaction.js';
5
+
6
+ /** An allowance with optional funder metadata and app authorization. */
7
+ export interface AllowanceReference {
8
+ objectId: string;
9
+ /** Skip the metadata lookup when the funder is already known. */
10
+ funder?: string;
11
+ /** Authorization for an app-bound allowance. */
12
+ app?: {
13
+ /** The app type A in SpendPermit<A>. */
14
+ type: string;
15
+ /** A SpendPermit<A> from the app, consumed by this spend. */
16
+ permit: TransactionObjectArgument;
17
+ };
18
+ }
19
+
20
+ export type BalanceOptions = {
21
+ /** The coin type T, not Balance<T>. Defaults to SUI. */
22
+ type?: string;
23
+ balance: bigint | number | string;
24
+ } & (
25
+ | { allowance?: never; useGasCoin?: boolean }
26
+ | {
27
+ /** An allowance ID or reference with optional app authorization. Never falls back to sender funds. */
28
+ allowance: string | AllowanceReference;
29
+ useGasCoin?: never;
30
+ }
31
+ );
32
+
33
+ export function normalizeBalance(options: BalanceOptions): bigint {
34
+ if (options.allowance !== undefined && options.useGasCoin !== undefined) {
35
+ throw new Error('useGasCoin cannot be combined with allowance');
36
+ }
37
+ const amount = options.balance;
38
+ if (typeof amount === 'string' && !/^[0-9]+$/.test(amount)) {
39
+ throw new Error('Amount must be a non-empty string of decimal digits');
40
+ }
41
+ if (typeof amount === 'number' && !Number.isSafeInteger(amount)) {
42
+ throw new Error('Amount must be a safe integer; use bigint or a string for larger amounts');
43
+ }
44
+ return BigInt(amount);
45
+ }
@@ -16,8 +16,11 @@ import {
16
16
  union,
17
17
  } from 'valibot';
18
18
 
19
+ import { ALLOWANCE_BALANCE, COIN_WITH_BALANCE } from './BalanceIntentNames.js';
20
+ import { resolveBalances } from './ResolveBalances.js';
21
+
19
22
  import { bcs } from '../../bcs/index.js';
20
- import { normalizeStructTag } from '../../utils/sui-types.js';
23
+ import { normalizeStructTag, normalizeSuiAddress } from '../../utils/sui-types.js';
21
24
  import { TransactionCommands } from '../Commands.js';
22
25
  import type { Argument } from '../data/internal.js';
23
26
  import { Inputs } from '../Inputs.js';
@@ -26,7 +29,7 @@ import type { Transaction, TransactionResult } from '../Transaction.js';
26
29
  import type { TransactionDataBuilder } from '../TransactionData.js';
27
30
  import type { ClientWithCoreApi, SuiClientTypes } from '../../client/index.js';
28
31
 
29
- export const COIN_WITH_BALANCE = 'CoinWithBalance';
32
+ export { COIN_WITH_BALANCE } from './BalanceIntentNames.js';
30
33
  const SUI_TYPE = normalizeStructTag('0x2::sui::SUI');
31
34
 
32
35
  export function coinWithBalance({
@@ -45,7 +48,7 @@ export function coinWithBalance({
45
48
  return coinResult;
46
49
  }
47
50
 
48
- tx.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance);
51
+ tx.addIntentResolver(COIN_WITH_BALANCE, resolveBalances);
49
52
  const coinType = type === 'gas' ? type : normalizeStructTag(type);
50
53
 
51
54
  coinResult = tx.add(
@@ -80,7 +83,7 @@ export function createBalance({
80
83
  return balanceResult;
81
84
  }
82
85
 
83
- tx.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance);
86
+ tx.addIntentResolver(COIN_WITH_BALANCE, resolveBalances);
84
87
  const coinType = type === 'gas' ? type : normalizeStructTag(type);
85
88
 
86
89
  balanceResult = tx.add(
@@ -126,6 +129,25 @@ export async function resolveCoinBalance(
126
129
  throw new Error('Sender must be set to resolve CoinWithBalance');
127
130
  }
128
131
 
132
+ if (transactionData.commands.some((command) => command.$Intent?.name === ALLOWANCE_BALANCE)) {
133
+ throw new Error(
134
+ 'Resolve AllowanceBalance together with CoinWithBalance, or preserve both intents',
135
+ );
136
+ }
137
+ const reservedByType = new Map<string, bigint>();
138
+ for (const input of transactionData.inputs) {
139
+ if (!input.FundsWithdrawal) continue;
140
+ const { withdrawFrom, typeArg, reservation } = input.FundsWithdrawal;
141
+ const owner =
142
+ withdrawFrom.SenderAllowance?.funder ??
143
+ (withdrawFrom.Sender
144
+ ? transactionData.sender
145
+ : (transactionData.gasData.owner ?? transactionData.sender));
146
+ if (normalizeSuiAddress(owner) !== normalizeSuiAddress(transactionData.sender)) continue;
147
+ const type = normalizeStructTag(typeArg.Balance);
148
+ reservedByType.set(type, (reservedByType.get(type) ?? 0n) + BigInt(reservation.MaxAmountU64));
149
+ }
150
+
129
151
  // First pass: scan intents, collect per-type data, and resolve zero-balance intents in place.
130
152
  for (const [i, command] of transactionData.commands.entries()) {
131
153
  if (command.$kind !== '$Intent' || command.$Intent.name !== COIN_WITH_BALANCE) {
@@ -178,37 +200,48 @@ export async function resolveCoinBalance(
178
200
  const coinsByType = new Map<string, SuiClientTypes.Coin[]>();
179
201
  const addressBalanceByType = new Map<string, bigint>();
180
202
  const client = buildOptions.client;
203
+ const assumeSufficientAddressBalances = buildOptions.assumeSufficientAddressBalances;
181
204
 
182
- if (!client) {
205
+ if (!client && !assumeSufficientAddressBalances) {
183
206
  throw new Error(
184
207
  'Client must be provided to build or serialize transactions with CoinWithBalance intents',
185
208
  );
186
209
  }
187
210
 
188
- await Promise.all([
189
- ...[...coinTypes].map(async (coinType) => {
190
- const { coins, addressBalance } = await getCoinsAndBalanceOfType({
191
- coinType,
192
- balance: totalByType.get(coinType)!,
193
- client,
194
- owner: transactionData.sender!,
195
- usedIds,
196
- });
197
-
198
- coinsByType.set(coinType, coins);
199
- addressBalanceByType.set(coinType, addressBalance);
200
- }),
201
- totalByType.has('gas')
202
- ? await client.core
203
- .getBalance({
204
- owner: transactionData.sender!,
205
- coinType: SUI_TYPE,
206
- })
207
- .then(({ balance }) => {
208
- addressBalanceByType.set('gas', BigInt(balance.addressBalance));
209
- })
210
- : null,
211
- ]);
211
+ if (assumeSufficientAddressBalances) {
212
+ for (const [coinType, balance] of totalByType) {
213
+ addressBalanceByType.set(coinType, balance);
214
+ }
215
+ } else {
216
+ await Promise.all([
217
+ ...[...coinTypes].map(async (coinType) => {
218
+ const { coins, addressBalance } = await getCoinsAndBalanceOfType({
219
+ coinType,
220
+ balance: totalByType.get(coinType)!,
221
+ client: client!,
222
+ owner: transactionData.sender!,
223
+ usedIds,
224
+ reserved: reservedByType.get(coinType) ?? 0n,
225
+ });
226
+
227
+ coinsByType.set(coinType, coins);
228
+ addressBalanceByType.set(coinType, addressBalance);
229
+ }),
230
+ totalByType.has('gas')
231
+ ? await client!.core
232
+ .getBalance({
233
+ owner: transactionData.sender!,
234
+ coinType: SUI_TYPE,
235
+ })
236
+ .then(({ balance }) => {
237
+ addressBalanceByType.set(
238
+ 'gas',
239
+ availableAddressBalance(balance.addressBalance, reservedByType.get(SUI_TYPE) ?? 0n),
240
+ );
241
+ })
242
+ : null,
243
+ ]);
244
+ }
212
245
 
213
246
  const exactBalanceByType = new Map<string, boolean>();
214
247
  const usedAddressBalance = new Set<string>();
@@ -455,12 +488,14 @@ async function getCoinsAndBalanceOfType({
455
488
  client,
456
489
  owner,
457
490
  usedIds,
491
+ reserved,
458
492
  }: {
459
493
  coinType: string;
460
494
  balance: bigint;
461
495
  client: ClientWithCoreApi;
462
496
  owner: string;
463
497
  usedIds: Set<string>;
498
+ reserved: bigint;
464
499
  }): Promise<{
465
500
  coins: SuiClientTypes.Coin[];
466
501
  balance: bigint;
@@ -470,9 +505,14 @@ async function getCoinsAndBalanceOfType({
470
505
  let remainingBalance = balance;
471
506
  const coins: SuiClientTypes.Coin[] = [];
472
507
  const balanceRequest = client.core.getBalance({ owner, coinType }).then(({ balance }) => {
473
- remainingBalance -= BigInt(balance.addressBalance);
474
-
475
- return balance;
508
+ const addressBalance = availableAddressBalance(balance.addressBalance, reserved);
509
+ remainingBalance -= addressBalance;
510
+
511
+ return {
512
+ ...balance,
513
+ addressBalance: String(addressBalance),
514
+ balance: String(BigInt(balance.balance) - reserved),
515
+ };
476
516
  });
477
517
 
478
518
  const [allCoins, balanceResponse] = await Promise.all([loadMoreCoins(), balanceRequest]);
@@ -524,3 +564,13 @@ async function getCoinsAndBalanceOfType({
524
564
  return coins;
525
565
  }
526
566
  }
567
+
568
+ function availableAddressBalance(balance: string, reserved: bigint): bigint {
569
+ const available = BigInt(balance) - reserved;
570
+ if (available < 0n) {
571
+ throw new Error(
572
+ `Insufficient address balance for existing withdrawals. Required: ${reserved}, Available: ${balance}`,
573
+ );
574
+ }
575
+ return available;
576
+ }
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { IntentResolverOptions } from '../resolve.js';
5
+ import type { TransactionDataBuilder } from '../TransactionData.js';
6
+ import { ALLOWANCE_BALANCE, COIN_WITH_BALANCE } from './BalanceIntentNames.js';
7
+ import { resolveAllowanceBalance } from './AllowanceBalance.js';
8
+ import { resolveCoinBalance } from './CoinWithBalance.js';
9
+
10
+ export async function resolveBalances(
11
+ transactionData: TransactionDataBuilder,
12
+ options: IntentResolverOptions,
13
+ next: () => Promise<void>,
14
+ ) {
15
+ const intents = new Set(options.intentNames ?? [ALLOWANCE_BALANCE, COIN_WITH_BALANCE]);
16
+ const shouldResolve = (name: string) =>
17
+ intents.has(name) &&
18
+ !options.supportedIntents?.includes(name) &&
19
+ transactionData.commands.some((command) => command.$Intent?.name === name);
20
+
21
+ // Reserve allowance withdrawals before ordinary coin selection, including when
22
+ // the sender is also the funder. Keep separate intents for wallet negotiation.
23
+ if (shouldResolve(ALLOWANCE_BALANCE)) {
24
+ await resolveAllowanceBalance(transactionData, options, async () => {});
25
+ }
26
+ if (shouldResolve(COIN_WITH_BALANCE)) {
27
+ await resolveCoinBalance(transactionData, options, async () => {});
28
+ }
29
+ await next();
30
+ }
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { TransactionDataBuilder } from './TransactionData.js';
5
+
6
+ export function transactionUsesGasCoin(transactionData: TransactionDataBuilder) {
7
+ let usesGasCoin = false;
8
+
9
+ transactionData.mapArguments((arg) => {
10
+ if (arg.$kind === 'GasCoin') {
11
+ usesGasCoin = true;
12
+ }
13
+
14
+ return arg;
15
+ });
16
+
17
+ return usesGasCoin;
18
+ }
@@ -9,16 +9,31 @@ import type { BcsType } from '@mysten/bcs';
9
9
  import { Inputs } from './Inputs.js';
10
10
  import { bcs } from '../bcs/index.js';
11
11
  import { coreClientResolveTransactionPlugin } from '../client/core-resolver.js';
12
+ import { transactionUsesGasCoin } from './resolution-utils.js';
12
13
 
13
14
  export interface BuildTransactionOptions {
14
15
  client?: ClientWithCoreApi;
15
16
  onlyTransactionKind?: boolean;
17
+ /**
18
+ * Resolve `CoinWithBalance` intents from address balance without looking up balances or coins.
19
+ *
20
+ * If nothing else needs resolution, the transaction doesn't use `GasCoin`, and it already has a
21
+ * `ValidDuring` or `Validity` expiration, an unset gas payment is also set to `[]` (pay gas from
22
+ * address balance). Execution fails if the balances aren't there.
23
+ */
24
+ assumeSufficientAddressBalances?: boolean;
16
25
  }
17
26
 
18
27
  export interface SerializeTransactionOptions extends BuildTransactionOptions {
19
28
  supportedIntents?: string[];
20
29
  }
21
30
 
31
+ /** Options supplied when a registered intent resolver runs. */
32
+ export interface IntentResolverOptions extends SerializeTransactionOptions {
33
+ /** Intent names assigned to this resolver for this serialization pass. */
34
+ intentNames?: readonly string[];
35
+ }
36
+
22
37
  export type TransactionPlugin = (
23
38
  transactionData: TransactionDataBuilder,
24
39
  options: BuildTransactionOptions,
@@ -38,11 +53,23 @@ export function needsTransactionResolution(
38
53
  }
39
54
 
40
55
  if (!options.onlyTransactionKind) {
41
- if (!data.gasData.price || !data.gasData.budget || !data.gasData.payment) {
56
+ if (!data.gasData.price || !data.gasData.budget) {
42
57
  return true;
43
58
  }
44
59
 
45
- if (data.gasData.payment.length === 0 && !data.expiration) {
60
+ if (!data.gasData.payment) {
61
+ const assumesAddressBalanceGas =
62
+ options.assumeSufficientAddressBalances && !transactionUsesGasCoin(data);
63
+
64
+ // Address balance gas has no object version to protect against replay, so an offline build
65
+ // needs a ValidDuring expiration. Epoch expiration doesn't count.
66
+ if (
67
+ !assumesAddressBalanceGas ||
68
+ (data.expiration?.$kind !== 'ValidDuring' && data.expiration?.$kind !== 'Validity')
69
+ ) {
70
+ return true;
71
+ }
72
+ } else if (data.gasData.payment.length === 0 && !data.expiration) {
46
73
  return true;
47
74
  }
48
75
  }
@@ -56,7 +83,13 @@ export async function resolveTransactionPlugin(
56
83
  next: () => Promise<void>,
57
84
  ) {
58
85
  normalizeRawArguments(transactionData);
86
+
59
87
  if (!needsTransactionResolution(transactionData, options)) {
88
+ // Payment can only be unset here when assumeSufficientAddressBalances applies
89
+ if (!options.onlyTransactionKind && !transactionData.gasData.payment) {
90
+ transactionData.gasData.payment = [];
91
+ }
92
+
60
93
  await validate(transactionData);
61
94
  return next();
62
95
  }
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.29.0';
6
+ export const PACKAGE_VERSION = '2.31.0';