@msafe/sui3-sdk 1.0.23 → 1.0.25

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/index.d.cts CHANGED
@@ -334,6 +334,8 @@ declare class InvitationSDK {
334
334
  updateMSafeStatus(msafeAddress: string, status: UserMSafeStatus): Promise<void>;
335
335
  }
336
336
 
337
+ /** Protocol cap: a transaction may smash at most this many gas payment objects. */
338
+ declare const MAX_GAS_PAYMENT_OBJECTS = 256;
337
339
  type GasFundingMode = 'classic' | 'addressBalance' | 'mixedTopUp';
338
340
  interface GasFundingResult {
339
341
  mode: GasFundingMode;
@@ -347,11 +349,43 @@ declare class InsufficientGasFundsError extends Error {
347
349
  readonly addressBalance: bigint;
348
350
  constructor(gasBudget: bigint, coinBalance: bigint, addressBalance: bigint);
349
351
  }
352
+ /**
353
+ * Gas coins cannot cover a SUI amount that is split from the gas coin.
354
+ * Happens when the largest 256 coins are still short of amount + gas budget.
355
+ */
356
+ declare class GasPaymentLimitError extends Error {
357
+ readonly required: bigint;
358
+ readonly covered: bigint;
359
+ readonly coinCount: number;
360
+ constructor(required: bigint, covered: bigint, coinCount: number);
361
+ }
350
362
  /**
351
363
  * Gas budget must cover pre-rebate costs (computation + storage + overhead).
352
364
  * storageRebate is refunded after execution and must not reduce the budget.
353
365
  */
354
366
  declare function computeGasBudget(gasUsed: Pick<SuiClientTypes.GasCostSummary, 'computationCost' | 'storageCost'>, gasPrice: bigint): bigint;
367
+ type PaymentCoinRef = {
368
+ objectId: string;
369
+ version: string;
370
+ digest: string;
371
+ balance: bigint;
372
+ };
373
+ /**
374
+ * Pick the fewest large SUI coins that cover `cover`, capped at 256.
375
+ * Never attach every owned coin — accounts with hundreds of SUI objects
376
+ * exceed max_gas_payment_objects and trip gasless estimate
377
+ * ("gas_budget must be 0 for gasless transactions").
378
+ */
379
+ declare function pickGasPaymentCoins(coins: PaymentCoinRef[], cover: bigint): PaymentCoinRef[];
380
+ /** SUI that CoinWithBalance intents will split out of the gas coin. */
381
+ declare function gasCoinWithdrawal(tx: Transaction): bigint;
382
+ /**
383
+ * Balance gas payment coins must hold.
384
+ * A SUI withdrawal is taken from the gas coin only when address balance cannot
385
+ * cover that withdrawal by itself. Mysten redeems address balance for the whole
386
+ * amount in that case, and gas payment only has to pay the gas budget.
387
+ */
388
+ declare function gasPaymentCover(tx: Transaction, addressBalance: bigint, gasBudget: bigint): bigint;
355
389
  /** Object IDs a built payload already pins (inputs + gas payment). */
356
390
  declare function collectPayloadObjectIds(payload: string | Uint8Array): string[];
357
391
  /**
@@ -643,4 +677,4 @@ declare function getIntentionContent(intention: unknown): unknown;
643
677
  */
644
678
  declare function transactionFromContent(content: unknown): Transaction;
645
679
 
646
- export { AddressBookSDK, COIN_RESERVATION_MAGIC, COIN_TYPE_ARG_REGEX, Coin, CoinHelper, CreateHelper, type CreateMSafeInfo, DEV_API_URL, DEV_SYNCING_URL, ENV_CONFIGS, Formatter, type FutureIntention, type GasFundingMode, type GasFundingResult, type GasObjectReference, HexToUint8Array, type HistorySendTx, type HistoryVote, type IWallet, InfoTypeEnabledDto, InfoTypeKey, InsufficientGasFundsError, LOCAL_API_URL, LOCAL_SYNCING_URL, MAINNET_RPC_URL, MSafeAccount, MSafeClient, type MSafeConfig, type MSafeConfigOptions, MSafeEnabledDto, MSafeEnv, MSafeGlobals, type NotificationData, type NotificationInfo, NotificationSubscribeDto, NotificationUpdateDto, PREV_API_URL, PROD_API_URL, type Pagination, type PendingTx, type PendingVote, type ProposeIntention, type ReceiveTransaction, SUI_COIN, SignatureVerifier, type SimulateDryRunResponse, type SimulationFailedResult, type SimulationOption, type SimulationResult, type SimulationSuccessResult, TESTNET_RPC_URL, Uint8ArrayToHex, addPrefix, collectPayloadObjectIds, collectQueueExcludeObjectIds, computeGasBudget, createCoinReservationRef, estimateGasBudget, getAllCoins, getIntentionContent, getMSafeConfig, getPublicKeyFromChain, httpUrlToGrpcBaseUrl, isCoinReservationDigest, msafeChainToSuiNetwork, parkedPayloadHex, preferClassicGasPayment, prepareGasFunding, selectGasFunding, stringToBuffer, toSuiTransaction, transactionFromContent };
680
+ export { AddressBookSDK, COIN_RESERVATION_MAGIC, COIN_TYPE_ARG_REGEX, Coin, CoinHelper, CreateHelper, type CreateMSafeInfo, DEV_API_URL, DEV_SYNCING_URL, ENV_CONFIGS, Formatter, type FutureIntention, type GasFundingMode, type GasFundingResult, type GasObjectReference, GasPaymentLimitError, HexToUint8Array, type HistorySendTx, type HistoryVote, type IWallet, InfoTypeEnabledDto, InfoTypeKey, InsufficientGasFundsError, LOCAL_API_URL, LOCAL_SYNCING_URL, MAINNET_RPC_URL, MAX_GAS_PAYMENT_OBJECTS, MSafeAccount, MSafeClient, type MSafeConfig, type MSafeConfigOptions, MSafeEnabledDto, MSafeEnv, MSafeGlobals, type NotificationData, type NotificationInfo, NotificationSubscribeDto, NotificationUpdateDto, PREV_API_URL, PROD_API_URL, type Pagination, type PaymentCoinRef, type PendingTx, type PendingVote, type ProposeIntention, type ReceiveTransaction, SUI_COIN, SignatureVerifier, type SimulateDryRunResponse, type SimulationFailedResult, type SimulationOption, type SimulationResult, type SimulationSuccessResult, TESTNET_RPC_URL, Uint8ArrayToHex, addPrefix, collectPayloadObjectIds, collectQueueExcludeObjectIds, computeGasBudget, createCoinReservationRef, estimateGasBudget, gasCoinWithdrawal, gasPaymentCover, getAllCoins, getIntentionContent, getMSafeConfig, getPublicKeyFromChain, httpUrlToGrpcBaseUrl, isCoinReservationDigest, msafeChainToSuiNetwork, parkedPayloadHex, pickGasPaymentCoins, preferClassicGasPayment, prepareGasFunding, selectGasFunding, stringToBuffer, toSuiTransaction, transactionFromContent };
package/dist/index.d.ts CHANGED
@@ -334,6 +334,8 @@ declare class InvitationSDK {
334
334
  updateMSafeStatus(msafeAddress: string, status: UserMSafeStatus): Promise<void>;
335
335
  }
336
336
 
337
+ /** Protocol cap: a transaction may smash at most this many gas payment objects. */
338
+ declare const MAX_GAS_PAYMENT_OBJECTS = 256;
337
339
  type GasFundingMode = 'classic' | 'addressBalance' | 'mixedTopUp';
338
340
  interface GasFundingResult {
339
341
  mode: GasFundingMode;
@@ -347,11 +349,43 @@ declare class InsufficientGasFundsError extends Error {
347
349
  readonly addressBalance: bigint;
348
350
  constructor(gasBudget: bigint, coinBalance: bigint, addressBalance: bigint);
349
351
  }
352
+ /**
353
+ * Gas coins cannot cover a SUI amount that is split from the gas coin.
354
+ * Happens when the largest 256 coins are still short of amount + gas budget.
355
+ */
356
+ declare class GasPaymentLimitError extends Error {
357
+ readonly required: bigint;
358
+ readonly covered: bigint;
359
+ readonly coinCount: number;
360
+ constructor(required: bigint, covered: bigint, coinCount: number);
361
+ }
350
362
  /**
351
363
  * Gas budget must cover pre-rebate costs (computation + storage + overhead).
352
364
  * storageRebate is refunded after execution and must not reduce the budget.
353
365
  */
354
366
  declare function computeGasBudget(gasUsed: Pick<SuiClientTypes.GasCostSummary, 'computationCost' | 'storageCost'>, gasPrice: bigint): bigint;
367
+ type PaymentCoinRef = {
368
+ objectId: string;
369
+ version: string;
370
+ digest: string;
371
+ balance: bigint;
372
+ };
373
+ /**
374
+ * Pick the fewest large SUI coins that cover `cover`, capped at 256.
375
+ * Never attach every owned coin — accounts with hundreds of SUI objects
376
+ * exceed max_gas_payment_objects and trip gasless estimate
377
+ * ("gas_budget must be 0 for gasless transactions").
378
+ */
379
+ declare function pickGasPaymentCoins(coins: PaymentCoinRef[], cover: bigint): PaymentCoinRef[];
380
+ /** SUI that CoinWithBalance intents will split out of the gas coin. */
381
+ declare function gasCoinWithdrawal(tx: Transaction): bigint;
382
+ /**
383
+ * Balance gas payment coins must hold.
384
+ * A SUI withdrawal is taken from the gas coin only when address balance cannot
385
+ * cover that withdrawal by itself. Mysten redeems address balance for the whole
386
+ * amount in that case, and gas payment only has to pay the gas budget.
387
+ */
388
+ declare function gasPaymentCover(tx: Transaction, addressBalance: bigint, gasBudget: bigint): bigint;
355
389
  /** Object IDs a built payload already pins (inputs + gas payment). */
356
390
  declare function collectPayloadObjectIds(payload: string | Uint8Array): string[];
357
391
  /**
@@ -643,4 +677,4 @@ declare function getIntentionContent(intention: unknown): unknown;
643
677
  */
644
678
  declare function transactionFromContent(content: unknown): Transaction;
645
679
 
646
- export { AddressBookSDK, COIN_RESERVATION_MAGIC, COIN_TYPE_ARG_REGEX, Coin, CoinHelper, CreateHelper, type CreateMSafeInfo, DEV_API_URL, DEV_SYNCING_URL, ENV_CONFIGS, Formatter, type FutureIntention, type GasFundingMode, type GasFundingResult, type GasObjectReference, HexToUint8Array, type HistorySendTx, type HistoryVote, type IWallet, InfoTypeEnabledDto, InfoTypeKey, InsufficientGasFundsError, LOCAL_API_URL, LOCAL_SYNCING_URL, MAINNET_RPC_URL, MSafeAccount, MSafeClient, type MSafeConfig, type MSafeConfigOptions, MSafeEnabledDto, MSafeEnv, MSafeGlobals, type NotificationData, type NotificationInfo, NotificationSubscribeDto, NotificationUpdateDto, PREV_API_URL, PROD_API_URL, type Pagination, type PendingTx, type PendingVote, type ProposeIntention, type ReceiveTransaction, SUI_COIN, SignatureVerifier, type SimulateDryRunResponse, type SimulationFailedResult, type SimulationOption, type SimulationResult, type SimulationSuccessResult, TESTNET_RPC_URL, Uint8ArrayToHex, addPrefix, collectPayloadObjectIds, collectQueueExcludeObjectIds, computeGasBudget, createCoinReservationRef, estimateGasBudget, getAllCoins, getIntentionContent, getMSafeConfig, getPublicKeyFromChain, httpUrlToGrpcBaseUrl, isCoinReservationDigest, msafeChainToSuiNetwork, parkedPayloadHex, preferClassicGasPayment, prepareGasFunding, selectGasFunding, stringToBuffer, toSuiTransaction, transactionFromContent };
680
+ export { AddressBookSDK, COIN_RESERVATION_MAGIC, COIN_TYPE_ARG_REGEX, Coin, CoinHelper, CreateHelper, type CreateMSafeInfo, DEV_API_URL, DEV_SYNCING_URL, ENV_CONFIGS, Formatter, type FutureIntention, type GasFundingMode, type GasFundingResult, type GasObjectReference, GasPaymentLimitError, HexToUint8Array, type HistorySendTx, type HistoryVote, type IWallet, InfoTypeEnabledDto, InfoTypeKey, InsufficientGasFundsError, LOCAL_API_URL, LOCAL_SYNCING_URL, MAINNET_RPC_URL, MAX_GAS_PAYMENT_OBJECTS, MSafeAccount, MSafeClient, type MSafeConfig, type MSafeConfigOptions, MSafeEnabledDto, MSafeEnv, MSafeGlobals, type NotificationData, type NotificationInfo, NotificationSubscribeDto, NotificationUpdateDto, PREV_API_URL, PROD_API_URL, type Pagination, type PaymentCoinRef, type PendingTx, type PendingVote, type ProposeIntention, type ReceiveTransaction, SUI_COIN, SignatureVerifier, type SimulateDryRunResponse, type SimulationFailedResult, type SimulationOption, type SimulationResult, type SimulationSuccessResult, TESTNET_RPC_URL, Uint8ArrayToHex, addPrefix, collectPayloadObjectIds, collectQueueExcludeObjectIds, computeGasBudget, createCoinReservationRef, estimateGasBudget, gasCoinWithdrawal, gasPaymentCover, getAllCoins, getIntentionContent, getMSafeConfig, getPublicKeyFromChain, httpUrlToGrpcBaseUrl, isCoinReservationDigest, msafeChainToSuiNetwork, parkedPayloadHex, pickGasPaymentCoins, preferClassicGasPayment, prepareGasFunding, selectGasFunding, stringToBuffer, toSuiTransaction, transactionFromContent };
package/dist/index.js CHANGED
@@ -416,6 +416,10 @@ async function getAllCoins(input) {
416
416
 
417
417
  // src/utils/gasFunding.ts
418
418
  var GAS_SAFE_OVERHEAD = 1000n;
419
+ var MAX_GAS_PAYMENT_OBJECTS = 256;
420
+ var ESTIMATE_GAS_COVER = 1000000000n;
421
+ var MAX_PROBE_GAS_BUDGET = 50000000000n;
422
+ var COIN_WITH_BALANCE_INTENT = "CoinWithBalance";
419
423
  var InsufficientGasFundsError = class extends Error {
420
424
  gasBudget;
421
425
  coinBalance;
@@ -431,11 +435,105 @@ var InsufficientGasFundsError = class extends Error {
431
435
  this.addressBalance = addressBalance;
432
436
  }
433
437
  };
438
+ var GasPaymentLimitError = class extends Error {
439
+ required;
440
+ covered;
441
+ coinCount;
442
+ constructor(required, covered, coinCount) {
443
+ super(gasPaymentLimitMessage(required, covered, coinCount));
444
+ this.name = "GasPaymentLimitError";
445
+ this.required = required;
446
+ this.covered = covered;
447
+ this.coinCount = coinCount;
448
+ }
449
+ };
450
+ function formatSui(mist) {
451
+ const negative = mist < 0n;
452
+ const value = negative ? -mist : mist;
453
+ const whole = value / 1000000000n;
454
+ const fraction = (value % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
455
+ const text = fraction.length > 0 ? `${whole}.${fraction}` : `${whole}`;
456
+ return negative ? `-${text}` : text;
457
+ }
458
+ function gasPaymentLimitMessage(required, covered, coinCount) {
459
+ const requiredSui = formatSui(required);
460
+ const coveredSui = formatSui(covered);
461
+ if (coinCount >= MAX_GAS_PAYMENT_OBJECTS) {
462
+ return `This transfer needs ${requiredSui} SUI from gas coins, but the largest ${MAX_GAS_PAYMENT_OBJECTS} coins only cover ${coveredSui} SUI. Send a smaller amount, or consolidate coins into fewer objects first.`;
463
+ }
464
+ return `This transfer needs ${requiredSui} SUI from gas coins, but the available SUI coin objects only cover ${coveredSui} SUI.`;
465
+ }
434
466
  function computeGasBudget(gasUsed, gasPrice) {
435
467
  const { computationCost, storageCost } = gasUsed;
436
468
  const safeOverhead = GAS_SAFE_OVERHEAD * gasPrice;
437
469
  return BigInt(computationCost) + BigInt(storageCost) + safeOverhead;
438
470
  }
471
+ function compareBalanceDescending(a, b) {
472
+ if (a.balance === b.balance) {
473
+ return 0;
474
+ }
475
+ return a.balance < b.balance ? 1 : -1;
476
+ }
477
+ function pickGasPaymentCoins(coins, cover) {
478
+ if (cover <= 0n) {
479
+ return [];
480
+ }
481
+ const sorted = [...coins].filter((coin) => coin.balance > 0n).sort(compareBalanceDescending);
482
+ const selected = [];
483
+ let sum = 0n;
484
+ for (let index = 0; index < sorted.length; index += 1) {
485
+ if (selected.length >= MAX_GAS_PAYMENT_OBJECTS) {
486
+ break;
487
+ }
488
+ const coin = sorted[index];
489
+ selected.push(coin);
490
+ sum += coin.balance;
491
+ if (sum >= cover) {
492
+ break;
493
+ }
494
+ }
495
+ return selected;
496
+ }
497
+ function readBigInt(value) {
498
+ if (typeof value === "bigint") {
499
+ return value;
500
+ }
501
+ if (typeof value === "number" && Number.isFinite(value)) {
502
+ return BigInt(Math.trunc(value));
503
+ }
504
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
505
+ return BigInt(value);
506
+ }
507
+ return 0n;
508
+ }
509
+ function gasCoinWithdrawal(tx) {
510
+ return tx.getData().commands.reduce((sum, command) => {
511
+ if (command.$kind !== "$Intent" || command.$Intent.name !== COIN_WITH_BALANCE_INTENT) {
512
+ return sum;
513
+ }
514
+ const data = command.$Intent.data;
515
+ if (data?.type !== "gas") {
516
+ return sum;
517
+ }
518
+ return sum + readBigInt(data.balance);
519
+ }, 0n);
520
+ }
521
+ function gasPaymentCover(tx, addressBalance, gasBudget) {
522
+ const withdrawal = gasCoinWithdrawal(tx);
523
+ const fromGasCoin = withdrawal > addressBalance ? withdrawal : 0n;
524
+ return gasBudget + fromGasCoin;
525
+ }
526
+ function sumBalances(coins) {
527
+ return coins.reduce((sum, coin) => sum + coin.balance, 0n);
528
+ }
529
+ function pickGasPaymentCoinsForCover(coins, cover) {
530
+ const picked = pickGasPaymentCoins(coins, cover);
531
+ const covered = sumBalances(picked);
532
+ if (cover > 0n && covered < cover) {
533
+ throw new GasPaymentLimitError(cover, covered, picked.length);
534
+ }
535
+ return picked;
536
+ }
439
537
  function collectUsedObjectIds(tx) {
440
538
  const used = tx.getData().inputs.reduce((acc, input) => {
441
539
  const immOrOwned = input.Object?.ImmOrOwnedObject?.objectId;
@@ -579,8 +677,14 @@ async function selectGasFunding(input) {
579
677
  if (total < gasBudget) {
580
678
  throw new InsufficientGasFundsError(gasBudget, coinBalance, addressBalance);
581
679
  }
680
+ const cover = gasPaymentCover(tx, addressBalance, gasBudget);
681
+ if (cover > gasBudget) {
682
+ const picked = pickGasPaymentCoinsForCover(paymentCoins, cover);
683
+ tx.setGasPayment(toObjectRefs(picked));
684
+ return { mode: "classic", gasBudget, coinBalance, addressBalance };
685
+ }
582
686
  if (paymentCoins.length > 0 && coinBalance >= gasBudget) {
583
- tx.setGasPayment(toObjectRefs(paymentCoins));
687
+ tx.setGasPayment(toObjectRefs(pickGasPaymentCoins(paymentCoins, gasBudget)));
584
688
  return { mode: "classic", gasBudget, coinBalance, addressBalance };
585
689
  }
586
690
  if (paymentCoins.length > 0 && total >= gasBudget) {
@@ -589,7 +693,7 @@ async function selectGasFunding(input) {
589
693
  tx,
590
694
  suiClient,
591
695
  owner,
592
- paymentCoins,
696
+ paymentCoins: pickGasPaymentCoins(paymentCoins, 1n),
593
697
  topUpAmount
594
698
  });
595
699
  return { mode: "mixedTopUp", gasBudget, coinBalance, addressBalance };
@@ -634,8 +738,19 @@ async function estimateGasBudget(input) {
634
738
  const probe = mustResetGas ? cloneWithClearedGasConfig(tx) : Transaction.from(tx);
635
739
  probe.setSender(owner);
636
740
  probe.setGasPrice(gasPrice);
637
- if (funding.paymentCoins.length > 0) {
638
- probe.setGasPayment(toObjectRefs(funding.paymentCoins));
741
+ const fromGasCoin = gasPaymentCover(tx, funding.addressBalance, 0n);
742
+ if (fromGasCoin > 0n) {
743
+ const picked = pickGasPaymentCoins(funding.paymentCoins, fromGasCoin + ESTIMATE_GAS_COVER);
744
+ const covered = sumBalances(picked);
745
+ if (covered <= fromGasCoin) {
746
+ throw new GasPaymentLimitError(fromGasCoin, covered, picked.length);
747
+ }
748
+ probe.setGasPayment(toObjectRefs(picked));
749
+ const remainder = covered - fromGasCoin;
750
+ const probeBudget = remainder > MAX_PROBE_GAS_BUDGET ? MAX_PROBE_GAS_BUDGET : remainder;
751
+ probe.setGasBudget(probeBudget);
752
+ } else if (funding.paymentCoins.length > 0) {
753
+ probe.setGasPayment(toObjectRefs(pickGasPaymentCoins(funding.paymentCoins, ESTIMATE_GAS_COVER)));
639
754
  }
640
755
  let built;
641
756
  try {
@@ -710,7 +825,7 @@ async function preferClassicGasPayment(input) {
710
825
  if (funding.paymentCoins.length === 0) {
711
826
  return false;
712
827
  }
713
- tx.setGasPayment(toObjectRefs(funding.paymentCoins));
828
+ tx.setGasPayment(toObjectRefs(pickGasPaymentCoins(funding.paymentCoins, ESTIMATE_GAS_COVER)));
714
829
  return true;
715
830
  }
716
831
 
@@ -2100,6 +2215,7 @@ export {
2100
2215
  DEV_SYNCING_URL,
2101
2216
  ENV_CONFIGS,
2102
2217
  Formatter,
2218
+ GasPaymentLimitError,
2103
2219
  HexToUint8Array,
2104
2220
  InfoTypeEnabledDto,
2105
2221
  InfoTypeKey,
@@ -2107,6 +2223,7 @@ export {
2107
2223
  LOCAL_API_URL,
2108
2224
  LOCAL_SYNCING_URL,
2109
2225
  MAINNET_RPC_URL,
2226
+ MAX_GAS_PAYMENT_OBJECTS,
2110
2227
  MSafeAccount,
2111
2228
  MSafeClient,
2112
2229
  MSafeEnabledDto,
@@ -2126,6 +2243,8 @@ export {
2126
2243
  computeGasBudget,
2127
2244
  createCoinReservationRef,
2128
2245
  estimateGasBudget,
2246
+ gasCoinWithdrawal,
2247
+ gasPaymentCover,
2129
2248
  getAllCoins,
2130
2249
  getIntentionContent,
2131
2250
  getMSafeConfig,
@@ -2134,6 +2253,7 @@ export {
2134
2253
  isCoinReservationDigest,
2135
2254
  msafeChainToSuiNetwork,
2136
2255
  parkedPayloadHex,
2256
+ pickGasPaymentCoins,
2137
2257
  preferClassicGasPayment,
2138
2258
  prepareGasFunding,
2139
2259
  selectGasFunding,