@msafe/sui3-sdk 1.0.17 → 1.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msafe/sui3-sdk",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "SDK for MSafe SUI V3",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -32,8 +32,8 @@
32
32
  "@changesets/changelog-github": "^0.7.0",
33
33
  "@changesets/cli": "^2.31.0",
34
34
  "@msafe/mpay-sdk-sui": "^1.0.25",
35
- "@msafe/sui-app-store": "0.0.338",
36
- "@msafe/sui3-utils": "^5.0.8",
35
+ "@msafe/sui-app-store": "^0.0.357",
36
+ "@msafe/sui3-utils": "^5.1.0",
37
37
  "@mysten/sui": "^2.16.2",
38
38
  "@mysten/wallet-standard": "^0.20.3",
39
39
  "@types/jest": "^30.0.0",
@@ -1,7 +1,7 @@
1
1
  import type { SuiClientTypes } from '@mysten/sui/client';
2
2
  import type { SuiGrpcClient } from '@mysten/sui/grpc';
3
3
  import { Transaction } from '@mysten/sui/transactions';
4
- import { normalizeSuiObjectId } from '@mysten/sui/utils';
4
+ import { normalizeStructTag, normalizeSuiObjectId } from '@mysten/sui/utils';
5
5
 
6
6
  import { createCoinReservationRef } from '@/utils/coinReservation';
7
7
  import { getAllCoins, SUI_COIN } from '@/utils/sui';
@@ -73,6 +73,29 @@ function collectUsedObjectIds(tx: Transaction): Set<string> {
73
73
  }, new Set<string>());
74
74
  }
75
75
 
76
+ /**
77
+ * Mysten CoinWithBalance with useGasCoin=false (type is 0x2::sui::SUI, not "gas")
78
+ * loads every unused SUI coin as a mutable input at resolve time. Those coins
79
+ * cannot also be gas payment or the node rejects:
80
+ * "Mutable object ... cannot appear more than one in one transaction".
81
+ */
82
+ function txConsumesOwnedSuiCoins(tx: Transaction): boolean {
83
+ return tx.getData().commands.some((cmd) => {
84
+ if (cmd.$kind !== '$Intent' || cmd.$Intent.name !== 'CoinWithBalance') {
85
+ return false;
86
+ }
87
+ const type = (cmd.$Intent.data as { type?: string } | undefined)?.type;
88
+ if (!type || type === 'gas') {
89
+ return false;
90
+ }
91
+ try {
92
+ return normalizeStructTag(type) === normalizeStructTag(SUI_COIN);
93
+ } catch {
94
+ return false;
95
+ }
96
+ });
97
+ }
98
+
76
99
  async function loadSuiGasFunding(suiClient: SuiGrpcClient, owner: string, tx: Transaction): Promise<SuiGasFunding> {
77
100
  const usedObjectIds = collectUsedObjectIds(tx);
78
101
  const [coins, balanceRes] = await Promise.all([
@@ -80,6 +103,18 @@ async function loadSuiGasFunding(suiClient: SuiGrpcClient, owner: string, tx: Tr
80
103
  suiClient.getBalance({ owner, coinType: SUI_COIN }),
81
104
  ]);
82
105
 
106
+ const addressBalance = BigInt(balanceRes.balance.addressBalance);
107
+
108
+ // Intent will take every owned SUI coin at resolve time — leave them off gas payment.
109
+ if (txConsumesOwnedSuiCoins(tx)) {
110
+ return {
111
+ paymentCoins: [],
112
+ coinBalance: 0n,
113
+ addressBalance,
114
+ total: addressBalance,
115
+ };
116
+ }
117
+
83
118
  const paymentCoins = coins
84
119
  .filter((coin) => !usedObjectIds.has(normalizeSuiObjectId(coin.objectId)) && BigInt(coin.balance) > 0n)
85
120
  .map((coin) => ({
@@ -90,7 +125,6 @@ async function loadSuiGasFunding(suiClient: SuiGrpcClient, owner: string, tx: Tr
90
125
  }));
91
126
 
92
127
  const coinBalance = paymentCoins.reduce((sum, coin) => sum + coin.balance, 0n);
93
- const addressBalance = BigInt(balanceRes.balance.addressBalance);
94
128
  return {
95
129
  paymentCoins,
96
130
  coinBalance,
@@ -212,6 +246,44 @@ export async function selectGasFunding(input: {
212
246
  * cheaper payment path. Budget is left unset so Mysten can dry-run with MAX_GAS
213
247
  * overrides (avoids locking the full balance away from GasCoin splits).
214
248
  */
249
+ /**
250
+ * Clone a tx with gas budget/payment/expiration cleared.
251
+ * Needed because Mysten treats budget "0" as set (truthy) and skips re-estimation,
252
+ * and app-store helper.build() often pre-builds with payment:[] + budget:"0".
253
+ */
254
+ function cloneWithClearedGasConfig(tx: Transaction): Transaction {
255
+ const data = structuredClone(tx.getData()) as {
256
+ version: number;
257
+ sender: string | null;
258
+ expiration: unknown;
259
+ gasData: {
260
+ budget: string | null;
261
+ price: string | null;
262
+ owner: string | null;
263
+ payment: unknown;
264
+ };
265
+ inputs: unknown;
266
+ commands: unknown;
267
+ };
268
+ data.gasData.budget = null;
269
+ data.gasData.price = null;
270
+ data.gasData.payment = null;
271
+ // Address-balance gas bakes ValidDuring; clear so classic gas probe is valid.
272
+ data.expiration = null;
273
+ return Transaction.from(JSON.stringify(data));
274
+ }
275
+
276
+ function isPositiveBudget(budget: string | number | bigint | null | undefined): boolean {
277
+ if (budget == null) {
278
+ return false;
279
+ }
280
+ try {
281
+ return BigInt(budget) > 0n;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
215
287
  export async function estimateGasBudget(input: {
216
288
  tx: Transaction;
217
289
  suiClient: SuiGrpcClient;
@@ -224,7 +296,11 @@ export async function estimateGasBudget(input: {
224
296
  throw new InsufficientGasFundsError(0n, funding.coinBalance, funding.addressBalance);
225
297
  }
226
298
 
227
- const probe = Transaction.from(tx);
299
+ const { budget, payment } = tx.getData().gasData;
300
+ // app-store / Mysten pre-build can leave payment:[] with budget "0".
301
+ // Mysten's setGasBudget skips when budget is the string "0" (truthy), so clear first.
302
+ const mustResetGas = (payment != null && payment.length === 0) || (budget != null && !isPositiveBudget(budget));
303
+ const probe = mustResetGas ? cloneWithClearedGasConfig(tx) : Transaction.from(tx);
228
304
  probe.setSender(owner);
229
305
  probe.setGasPrice(gasPrice);
230
306
 
@@ -273,16 +349,27 @@ export async function prepareGasFunding(input: {
273
349
  // Never trust a budget baked into plain-tx / Mysten auto AB-gas txs.
274
350
  // Those budgets are often under-estimated for payment: [] and would skip
275
351
  // estimateGasBudget if we kept existingBudget.
352
+ // Also never trust budget "0" / 0n — Mysten treats "0" as set and skips resolve.
276
353
  let { gasBudget } = input;
354
+ if (gasBudget != null && gasBudget <= 0n) {
355
+ gasBudget = undefined;
356
+ }
357
+ const existingPositive = isPositiveBudget(existingBudget) ? BigInt(existingBudget as string) : null;
358
+
277
359
  if (gasBudget == null) {
278
- if (bakedAddressBalanceGas || existingBudget == null) {
360
+ if (bakedAddressBalanceGas || existingPositive == null) {
279
361
  gasBudget = await estimateGasBudget({ tx, suiClient, owner, gasPrice });
280
362
  } else {
281
- gasBudget = BigInt(existingBudget);
363
+ gasBudget = existingPositive;
282
364
  }
283
365
  }
284
366
 
285
367
  tx.setGasBudget(gasBudget);
368
+ // Drop ValidDuring from a prior address-balance pre-build; classic gas must not keep it,
369
+ // and AB gas will re-attach expiration during the final build when payment is [].
370
+ if (bakedAddressBalanceGas) {
371
+ tx.setExpiration(null);
372
+ }
286
373
  return selectGasFunding({ tx, suiClient, owner, gasBudget });
287
374
  }
288
375