@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/dist/index.cjs +120 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +125 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/MSafeAccount.ts +22 -3
- package/src/simulate/simulator.ts +19 -2
- package/src/utils/gasFunding.ts +125 -16
package/package.json
CHANGED
package/src/core/MSafeAccount.ts
CHANGED
|
@@ -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({
|
|
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
|
-
...
|
|
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: {
|
|
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,
|
package/src/utils/gasFunding.ts
CHANGED
|
@@ -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
|
-
|
|
62
|
+
const used = tx.getData().inputs.reduce<Set<string>>((acc, input) => {
|
|
63
63
|
const immOrOwned = input.Object?.ImmOrOwnedObject?.objectId;
|
|
64
64
|
if (immOrOwned) {
|
|
65
|
-
|
|
66
|
-
return
|
|
65
|
+
acc.add(normalizeSuiObjectId(immOrOwned));
|
|
66
|
+
return acc;
|
|
67
67
|
}
|
|
68
68
|
const unresolved = input.UnresolvedObject?.objectId;
|
|
69
69
|
if (unresolved) {
|
|
70
|
-
|
|
70
|
+
acc.add(normalizeSuiObjectId(unresolved));
|
|
71
71
|
}
|
|
72
|
-
return
|
|
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(
|
|
100
|
-
|
|
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 =
|
|
295
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
/**
|