@msafe/sui3-sdk 1.0.21 → 1.0.22

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.21",
3
+ "version": "1.0.22",
4
4
  "description": "SDK for MSafe SUI V3",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -25,7 +25,7 @@ import { OwnedCoin } from '@/types/assets';
25
25
  import { Pagination, PendingTx, SimulationResult } from '@/types/msafe';
26
26
  import { CoinHelper } from '@/utils';
27
27
  import { HexToUint8Array } from '@/utils/buffer';
28
- import { prepareGasFunding } from '@/utils/gasFunding';
28
+ import { parkedPayloadHex, prepareGasFunding } from '@/utils/gasFunding';
29
29
  import { BatchObjectOptions, getAllOwnedObjects, type OwnedObjectData } from '@/utils/iter/object';
30
30
  import { getIntentionContent, toSuiTransaction, transactionFromContent } from '@/utils/transaction';
31
31
 
@@ -138,11 +138,18 @@ export class MSafeAccount {
138
138
  async simulateIntention(
139
139
  request: Omit<IProposeIntentionRequest, 'msafeAddress' | 'signature'> & {
140
140
  txb?: Transaction;
141
+ preferAddressBalance?: boolean;
142
+ excludeObjectIds?: Iterable<string>;
141
143
  },
142
144
  ) {
143
145
  const txb = await this.buildTxFromIntention(request);
144
146
  txb.setSender(this.address);
145
- return this.simulator.simulate({ txb, sender: this.address });
147
+ return this.simulator.simulate({
148
+ txb,
149
+ sender: this.address,
150
+ preferAddressBalance: request.preferAddressBalance,
151
+ excludeObjectIds: request.excludeObjectIds,
152
+ });
146
153
  }
147
154
 
148
155
  async proposeIntention(input: Omit<IProposeIntentionRequest, 'signature' | 'msafeAddress'>) {
@@ -165,6 +172,8 @@ export class MSafeAccount {
165
172
  async proposeIntentionAndBuildVote(
166
173
  input: Omit<IProposeIntentionAndBuildAndVoteRequest, 'signature' | 'msafeAddress' | 'payload' | 'digest'> & {
167
174
  txb?: Transaction;
175
+ preferAddressBalance?: boolean;
176
+ excludeObjectIds?: Iterable<string>;
168
177
  },
169
178
  ): Promise<void> {
170
179
  const txb = await this.buildTxFromIntention(input);
@@ -176,14 +185,20 @@ export class MSafeAccount {
176
185
  owner: this.address,
177
186
  gasPrice: input.gasPrice,
178
187
  gasBudget: input.gasBudget,
188
+ preferAddressBalance: input.preferAddressBalance,
189
+ excludeObjectIds: input.excludeObjectIds,
179
190
  });
180
191
  const payload = await txb.build({ client: this.globals.suiClient });
181
192
  const digest = await txb.getDigest({ client: this.globals.suiClient });
182
193
 
183
194
  const signature = await this.wallet.signTransactionBlock({ transactionBlock: payload });
184
195
 
196
+ const apiInput = { ...input };
197
+ delete apiInput.txb;
198
+ delete apiInput.preferAddressBalance;
199
+ delete apiInput.excludeObjectIds;
185
200
  return this.backend.proposeIntentionAndBuildVote({
186
- ...input,
201
+ ...apiInput,
187
202
  payload: toHex(payload),
188
203
  digest,
189
204
  msafeAddress: this.address,
@@ -310,6 +325,10 @@ export class MSafeAccount {
310
325
 
311
326
  async simulateBuildTransaction(): Promise<SimulationResult> {
312
327
  const intention = await this.backend.getNextIntention({ msafeAddress: this.address });
328
+ const parked = parkedPayloadHex(intention.intention);
329
+ if (parked) {
330
+ return this.simulator.simulateBuilt(fromHex(parked));
331
+ }
313
332
  const txb = await this.buildTxFromIntention({
314
333
  application: intention.application,
315
334
  intention: intention.intention,
@@ -23,7 +23,12 @@ function gasObjectToReference(gas: SuiClientTypes.ChangedObject | null | undefin
23
23
  export class Simulator {
24
24
  constructor(private globals: MSafeGlobals) {}
25
25
 
26
- public async simulate(input: { txb: Transaction; sender: string }): Promise<SimulationResult> {
26
+ public async simulate(input: {
27
+ txb: Transaction;
28
+ sender: string;
29
+ preferAddressBalance?: boolean;
30
+ excludeObjectIds?: Iterable<string>;
31
+ }): Promise<SimulationResult> {
27
32
  const tx = this.copyTransaction(input.txb);
28
33
 
29
34
  const { suiClient } = this.globals;
@@ -40,6 +45,8 @@ export class Simulator {
40
45
  suiClient,
41
46
  owner: input.sender,
42
47
  gasPrice,
48
+ preferAddressBalance: input.preferAddressBalance,
49
+ excludeObjectIds: input.excludeObjectIds,
43
50
  });
44
51
  } catch (e: unknown) {
45
52
  const message = e instanceof Error ? e.message : String(e);
@@ -62,9 +69,19 @@ export class Simulator {
62
69
  };
63
70
  }
64
71
 
72
+ return this.inspectBuilt(built, gasPrice);
73
+ }
74
+
75
+ /** Dry-run a frozen payload without re-selecting gas. */
76
+ public async simulateBuilt(built: Uint8Array): Promise<SimulationResult> {
77
+ const gasPrice = await this.getGasPrice();
78
+ return this.inspectBuilt(built, gasPrice);
79
+ }
80
+
81
+ private async inspectBuilt(built: Uint8Array, gasPrice: bigint): Promise<SimulationResult> {
65
82
  let inspectResult: SimulateDryRunResponse;
66
83
  try {
67
- inspectResult = await suiClient.simulateTransaction({
84
+ inspectResult = await this.globals.suiClient.simulateTransaction({
68
85
  transaction: built,
69
86
  include: {
70
87
  effects: true,
@@ -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 { normalizeStructTag, normalizeSuiObjectId } from '@mysten/sui/utils';
4
+ import { fromHex, normalizeStructTag, normalizeSuiObjectId } from '@mysten/sui/utils';
5
5
 
6
6
  import { createCoinReservationRef } from '@/utils/coinReservation';
7
7
  import { getAllCoins, SUI_COIN } from '@/utils/sui';
@@ -59,18 +59,81 @@ interface SuiGasFunding {
59
59
  }
60
60
 
61
61
  function collectUsedObjectIds(tx: Transaction): Set<string> {
62
- return tx.getData().inputs.reduce<Set<string>>((used, input) => {
62
+ const used = tx.getData().inputs.reduce<Set<string>>((acc, input) => {
63
63
  const immOrOwned = input.Object?.ImmOrOwnedObject?.objectId;
64
64
  if (immOrOwned) {
65
- used.add(normalizeSuiObjectId(immOrOwned));
66
- return used;
65
+ acc.add(normalizeSuiObjectId(immOrOwned));
66
+ return acc;
67
67
  }
68
68
  const unresolved = input.UnresolvedObject?.objectId;
69
69
  if (unresolved) {
70
- used.add(normalizeSuiObjectId(unresolved));
70
+ acc.add(normalizeSuiObjectId(unresolved));
71
71
  }
72
- return used;
72
+ return acc;
73
73
  }, new Set<string>());
74
+
75
+ (tx.getData().gasData.payment ?? []).forEach((payment) => {
76
+ if (payment.objectId) {
77
+ used.add(normalizeSuiObjectId(payment.objectId));
78
+ }
79
+ });
80
+ return used;
81
+ }
82
+
83
+ function addExcludeObjectIds(used: Set<string>, excludeObjectIds?: Iterable<string>) {
84
+ Array.from(excludeObjectIds ?? [], (id) => used.add(normalizeSuiObjectId(id)));
85
+ return used;
86
+ }
87
+
88
+ function payloadToTransaction(payload: string | Uint8Array): Transaction {
89
+ if (typeof payload !== 'string') {
90
+ return Transaction.from(payload);
91
+ }
92
+ if (/^[0-9a-fA-F]+$/.test(payload) && payload.length % 2 === 0) {
93
+ return Transaction.from(fromHex(payload));
94
+ }
95
+ return Transaction.from(payload);
96
+ }
97
+
98
+ /** Object IDs a built payload already pins (inputs + gas payment). */
99
+ export function collectPayloadObjectIds(payload: string | Uint8Array): string[] {
100
+ return [...collectUsedObjectIds(payloadToTransaction(payload))];
101
+ }
102
+
103
+ /**
104
+ * Parked queue item: intention was saved with a built hex payload + digest
105
+ * so build-next can promote it without rebuilding.
106
+ */
107
+ export function parkedPayloadHex(intention: unknown): string | undefined {
108
+ if (!intention || typeof intention !== 'object') {
109
+ return undefined;
110
+ }
111
+ const { content, digest } = intention as { content?: unknown; digest?: unknown };
112
+ if (typeof content !== 'string' || typeof digest !== 'string' || !digest) {
113
+ return undefined;
114
+ }
115
+ if (!/^[0-9a-fA-F]+$/.test(content) || content.length < 64) {
116
+ return undefined;
117
+ }
118
+ return content;
119
+ }
120
+
121
+ /** Coins already pinned by the current pending tx and any parked future payloads. */
122
+ export function collectQueueExcludeObjectIds(input: {
123
+ pendingPayload?: string;
124
+ parkedPayloads?: Array<string | undefined>;
125
+ }): string[] {
126
+ const ids = new Set<string>();
127
+ if (input.pendingPayload) {
128
+ collectPayloadObjectIds(input.pendingPayload).forEach((id) => ids.add(id));
129
+ }
130
+ (input.parkedPayloads ?? []).forEach((payload) => {
131
+ if (!payload) {
132
+ return;
133
+ }
134
+ collectPayloadObjectIds(payload).forEach((id) => ids.add(id));
135
+ });
136
+ return [...ids];
74
137
  }
75
138
 
76
139
  /**
@@ -96,8 +159,13 @@ function txConsumesOwnedSuiCoins(tx: Transaction): boolean {
96
159
  });
97
160
  }
98
161
 
99
- async function loadSuiGasFunding(suiClient: SuiGrpcClient, owner: string, tx: Transaction): Promise<SuiGasFunding> {
100
- const usedObjectIds = collectUsedObjectIds(tx);
162
+ async function loadSuiGasFunding(
163
+ suiClient: SuiGrpcClient,
164
+ owner: string,
165
+ tx: Transaction,
166
+ excludeObjectIds?: Iterable<string>,
167
+ ): Promise<SuiGasFunding> {
168
+ const usedObjectIds = addExcludeObjectIds(collectUsedObjectIds(tx), excludeObjectIds);
101
169
  const [coins, balanceRes] = await Promise.all([
102
170
  getAllCoins({ suiClient, owner, coinType: SUI_COIN }),
103
171
  suiClient.getBalance({ owner, coinType: SUI_COIN }),
@@ -186,6 +254,7 @@ export async function selectGasFunding(input: {
186
254
  suiClient: SuiGrpcClient;
187
255
  owner: string;
188
256
  gasBudget: bigint;
257
+ excludeObjectIds?: Iterable<string>;
189
258
  }): Promise<GasFundingResult> {
190
259
  const { tx, suiClient, owner, gasBudget } = input;
191
260
  const { payment } = tx.getData().gasData;
@@ -196,7 +265,7 @@ export async function selectGasFunding(input: {
196
265
  // skips re-selection and fails InsufficientGas under a tight budget.
197
266
  // Selection below always overwrites payment via setGasPayment(...).
198
267
  if (payment != null && payment.length > 0) {
199
- const funding = await loadSuiGasFunding(suiClient, owner, tx);
268
+ const funding = await loadSuiGasFunding(suiClient, owner, tx, input.excludeObjectIds);
200
269
  return {
201
270
  mode: 'classic',
202
271
  gasBudget,
@@ -205,7 +274,7 @@ export async function selectGasFunding(input: {
205
274
  };
206
275
  }
207
276
 
208
- const funding = await loadSuiGasFunding(suiClient, owner, tx);
277
+ const funding = await loadSuiGasFunding(suiClient, owner, tx, input.excludeObjectIds);
209
278
  const { paymentCoins, coinBalance, addressBalance, total } = funding;
210
279
 
211
280
  if (total < gasBudget) {
@@ -289,13 +358,21 @@ export async function estimateGasBudget(input: {
289
358
  suiClient: SuiGrpcClient;
290
359
  owner: string;
291
360
  gasPrice: bigint;
361
+ excludeObjectIds?: Iterable<string>;
362
+ preferAddressBalance?: boolean;
292
363
  }): Promise<bigint> {
293
364
  const { tx, suiClient, owner, gasPrice } = input;
294
- const funding = await loadSuiGasFunding(suiClient, owner, tx);
295
- if (funding.total <= 0n) {
365
+ const funding = input.preferAddressBalance
366
+ ? {
367
+ ...(await loadSuiGasFunding(suiClient, owner, tx, input.excludeObjectIds)),
368
+ paymentCoins: [] as PaymentCoinRef[],
369
+ coinBalance: 0n,
370
+ }
371
+ : await loadSuiGasFunding(suiClient, owner, tx, input.excludeObjectIds);
372
+ const fundingTotal = input.preferAddressBalance ? funding.addressBalance : funding.total;
373
+ if (fundingTotal <= 0n) {
296
374
  throw new InsufficientGasFundsError(0n, funding.coinBalance, funding.addressBalance);
297
375
  }
298
-
299
376
  const { budget, payment } = tx.getData().gasData;
300
377
  // app-store / Mysten pre-build can leave payment:[] with budget "0".
301
378
  // Mysten's setGasBudget skips when budget is the string "0" (truthy), so clear first.
@@ -341,8 +418,18 @@ export async function prepareGasFunding(input: {
341
418
  owner: string;
342
419
  gasPrice: bigint;
343
420
  gasBudget?: bigint;
421
+ /**
422
+ * Force Address Balance gas (`payment: []`). Prefer {@link excludeObjectIds}
423
+ * for queue-behind-pending so unused SUI coins can still pay.
424
+ */
425
+ preferAddressBalance?: boolean;
426
+ /**
427
+ * Queue-behind-pending: do not pick coins the current pending / parked
428
+ * payloads already pin. Those versions will change when the earlier tx executes.
429
+ */
430
+ excludeObjectIds?: Iterable<string>;
344
431
  }): Promise<GasFundingResult> {
345
- const { tx, suiClient, owner, gasPrice } = input;
432
+ const { tx, suiClient, owner, gasPrice, excludeObjectIds } = input;
346
433
  const { payment, budget: existingBudget } = tx.getData().gasData;
347
434
  const bakedAddressBalanceGas = payment != null && payment.length === 0;
348
435
 
@@ -358,7 +445,14 @@ export async function prepareGasFunding(input: {
358
445
 
359
446
  if (gasBudget == null) {
360
447
  if (bakedAddressBalanceGas || existingPositive == null) {
361
- gasBudget = await estimateGasBudget({ tx, suiClient, owner, gasPrice });
448
+ gasBudget = await estimateGasBudget({
449
+ tx,
450
+ suiClient,
451
+ owner,
452
+ gasPrice,
453
+ excludeObjectIds,
454
+ preferAddressBalance: input.preferAddressBalance,
455
+ });
362
456
  } else {
363
457
  gasBudget = existingPositive;
364
458
  }
@@ -370,7 +464,22 @@ export async function prepareGasFunding(input: {
370
464
  if (bakedAddressBalanceGas) {
371
465
  tx.setExpiration(null);
372
466
  }
373
- return selectGasFunding({ tx, suiClient, owner, gasBudget });
467
+
468
+ if (input.preferAddressBalance) {
469
+ const funding = await loadSuiGasFunding(suiClient, owner, tx, excludeObjectIds);
470
+ if (funding.addressBalance < gasBudget) {
471
+ throw new InsufficientGasFundsError(gasBudget, funding.coinBalance, funding.addressBalance);
472
+ }
473
+ tx.setGasPayment([]);
474
+ return {
475
+ mode: 'addressBalance',
476
+ gasBudget,
477
+ coinBalance: funding.coinBalance,
478
+ addressBalance: funding.addressBalance,
479
+ };
480
+ }
481
+
482
+ return selectGasFunding({ tx, suiClient, owner, gasBudget, excludeObjectIds });
374
483
  }
375
484
 
376
485
  /**