@msafe/sui3-sdk 1.0.15 → 1.0.17
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 +457 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +449 -163
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/MSafeAccount.ts +16 -1
- package/src/simulate/simulator.ts +20 -12
- package/src/utils/coinReservation.ts +65 -0
- package/src/utils/gasFunding.ts +309 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/transaction.ts +16 -3
package/src/core/MSafeAccount.ts
CHANGED
|
@@ -25,6 +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
29
|
import { BatchObjectOptions, getAllOwnedObjects, type OwnedObjectData } from '@/utils/iter/object';
|
|
29
30
|
import { toSuiTransaction } from '@/utils/transaction';
|
|
30
31
|
|
|
@@ -214,8 +215,14 @@ export class MSafeAccount {
|
|
|
214
215
|
);
|
|
215
216
|
}
|
|
216
217
|
txb.setGasPrice(input.gasPrice);
|
|
217
|
-
txb.setGasBudget(input.gasBudget);
|
|
218
218
|
txb.setSender(this.address);
|
|
219
|
+
await prepareGasFunding({
|
|
220
|
+
tx: txb,
|
|
221
|
+
suiClient: this.globals.suiClient,
|
|
222
|
+
owner: this.address,
|
|
223
|
+
gasPrice: input.gasPrice,
|
|
224
|
+
gasBudget: input.gasBudget,
|
|
225
|
+
});
|
|
219
226
|
const payload = await txb.build({ client: this.globals.suiClient });
|
|
220
227
|
const digest = await txb.getDigest({ client: this.globals.suiClient });
|
|
221
228
|
|
|
@@ -334,6 +341,14 @@ export class MSafeAccount {
|
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
const rejectTxb = toSuiTransaction(buildRejectTxb(this.address));
|
|
344
|
+
rejectTxb.setGasPrice(input.gasPrice);
|
|
345
|
+
await prepareGasFunding({
|
|
346
|
+
tx: rejectTxb,
|
|
347
|
+
suiClient: this.suiClient,
|
|
348
|
+
owner: this.address,
|
|
349
|
+
gasPrice: input.gasPrice,
|
|
350
|
+
gasBudget: input.gasBudget,
|
|
351
|
+
});
|
|
337
352
|
const digest = await rejectTxb.getDigest({ client: this.suiClient });
|
|
338
353
|
const payload = await rejectTxb.build({ client: this.suiClient });
|
|
339
354
|
const signature = await this.wallet.signTransactionBlock({ transactionBlock: payload });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { SuiClientTypes } from '@mysten/sui/client';
|
|
2
2
|
import { Transaction } from '@mysten/sui/transactions';
|
|
3
3
|
|
|
4
|
-
// Default to 120% o the reference gas price
|
|
5
4
|
import { MSafeGlobals } from '@/globals/MSafeGlobals';
|
|
6
5
|
import type { GasObjectReference, SimulationResult, SimulateDryRunResponse } from '@/types';
|
|
6
|
+
import { computeGasBudget, prepareGasFunding } from '@/utils/gasFunding';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export { computeGasBudget, InsufficientGasFundsError } from '@/utils/gasFunding';
|
|
9
9
|
|
|
10
10
|
function formatExecutionError(error: SuiClientTypes.ExecutionError): string {
|
|
11
11
|
return error.message;
|
|
@@ -33,6 +33,23 @@ export class Simulator {
|
|
|
33
33
|
tx.setGasPrice(gasPrice);
|
|
34
34
|
tx.setSender(input.sender);
|
|
35
35
|
|
|
36
|
+
try {
|
|
37
|
+
// Resolve budget (no storageRebate) and select classic / address-balance / mixed top-up.
|
|
38
|
+
await prepareGasFunding({
|
|
39
|
+
tx,
|
|
40
|
+
suiClient,
|
|
41
|
+
owner: input.sender,
|
|
42
|
+
gasPrice,
|
|
43
|
+
});
|
|
44
|
+
} catch (e: unknown) {
|
|
45
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
gasPrice,
|
|
49
|
+
simulationError: message,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
let built: Uint8Array;
|
|
37
54
|
try {
|
|
38
55
|
built = await tx.build({ client: suiClient });
|
|
@@ -101,16 +118,7 @@ export class Simulator {
|
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
private toGasBudget(gasUsed: SuiClientTypes.GasCostSummary, gasPrice: bigint) {
|
|
104
|
-
|
|
105
|
-
const safeOverhead = GAS_SAFE_OVERHEAD * gasPrice;
|
|
106
|
-
|
|
107
|
-
const baseComputationCostWithOverhead = BigInt(computationCost) + safeOverhead;
|
|
108
|
-
|
|
109
|
-
// do not subtract rebate for now, because it may cause normally tx gas to be lower than empty tx gas
|
|
110
|
-
const gasBudget = baseComputationCostWithOverhead + BigInt(storageCost) - BigInt(storageRebate);
|
|
111
|
-
|
|
112
|
-
// Set the budget to max(computation, computation + storage - rebate)
|
|
113
|
-
return gasBudget > baseComputationCostWithOverhead ? gasBudget : baseComputationCostWithOverhead;
|
|
121
|
+
return computeGasBudget(gasUsed, gasPrice);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
private copyTransaction(tx: Transaction): Transaction {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { bcs, TypeTagSerializer } from '@mysten/sui/bcs';
|
|
2
|
+
import { deriveDynamicFieldID, fromBase58, fromHex, normalizeSuiAddress, toBase58, toHex } from '@mysten/sui/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compatibility coin-reservation ObjectRef (Mysten gas-smashing).
|
|
6
|
+
* Reserves address-balance SUI into gas payment without a persisted Coin object.
|
|
7
|
+
* Vendored because @mysten/sui does not export createCoinReservationRef publicly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SUI_ACCUMULATOR_ROOT_OBJECT_ID = normalizeSuiAddress('0xacc');
|
|
11
|
+
const ACCUMULATOR_KEY_TYPE_TAG = TypeTagSerializer.parseFromStr(
|
|
12
|
+
'0x2::accumulator::Key<0x2::balance::Balance<0x2::sui::SUI>>',
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const COIN_RESERVATION_MAGIC = new Uint8Array([
|
|
16
|
+
0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
|
|
17
|
+
0xac,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function isCoinReservationDigest(digestBase58: string): boolean {
|
|
21
|
+
const digestBytes = fromBase58(digestBase58);
|
|
22
|
+
const last20Bytes = digestBytes.slice(12, 32);
|
|
23
|
+
return last20Bytes.every((byte: number, i: number) => byte === COIN_RESERVATION_MAGIC[i]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deriveReservationObjectId(owner: string, chainIdentifier: string): string {
|
|
27
|
+
const keyBcs = bcs.Address.serialize(owner).toBytes();
|
|
28
|
+
const accumulatorId = deriveDynamicFieldID(SUI_ACCUMULATOR_ROOT_OBJECT_ID, ACCUMULATOR_KEY_TYPE_TAG, keyBcs);
|
|
29
|
+
|
|
30
|
+
const accBytes = fromHex(accumulatorId.slice(2));
|
|
31
|
+
const chainBytes = fromBase58(chainIdentifier);
|
|
32
|
+
if (chainBytes.length !== 32) {
|
|
33
|
+
throw new Error(`Invalid chain identifier length: expected 32 bytes, got ${chainBytes.length}`);
|
|
34
|
+
}
|
|
35
|
+
const xored = new Uint8Array(32);
|
|
36
|
+
for (let i = 0; i < 32; i++) {
|
|
37
|
+
// XOR is required to derive the reservation object ID from accumulator + chain id.
|
|
38
|
+
// eslint-disable-next-line no-bitwise
|
|
39
|
+
xored[i] = accBytes[i] ^ chainBytes[i];
|
|
40
|
+
}
|
|
41
|
+
return `0x${toHex(xored)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createCoinReservationRef(
|
|
45
|
+
reservedBalance: bigint,
|
|
46
|
+
owner: string,
|
|
47
|
+
chainIdentifier: string,
|
|
48
|
+
epoch: string,
|
|
49
|
+
): { objectId: string; version: string; digest: string } {
|
|
50
|
+
const digestBytes = new Uint8Array(32);
|
|
51
|
+
const view = new DataView(digestBytes.buffer);
|
|
52
|
+
view.setBigUint64(0, reservedBalance, true);
|
|
53
|
+
const epochNum = Number(epoch);
|
|
54
|
+
if (!Number.isSafeInteger(epochNum) || epochNum < 0 || epochNum > 0xffffffff) {
|
|
55
|
+
throw new Error(`Epoch ${epoch} out of u32 range for coin reservation digest`);
|
|
56
|
+
}
|
|
57
|
+
view.setUint32(8, epochNum, true);
|
|
58
|
+
digestBytes.set(COIN_RESERVATION_MAGIC, 12);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
objectId: deriveReservationObjectId(owner, chainIdentifier),
|
|
62
|
+
version: '0',
|
|
63
|
+
digest: toBase58(digestBytes),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type { SuiClientTypes } from '@mysten/sui/client';
|
|
2
|
+
import type { SuiGrpcClient } from '@mysten/sui/grpc';
|
|
3
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
4
|
+
import { normalizeSuiObjectId } from '@mysten/sui/utils';
|
|
5
|
+
|
|
6
|
+
import { createCoinReservationRef } from '@/utils/coinReservation';
|
|
7
|
+
import { getAllCoins, SUI_COIN } from '@/utils/sui';
|
|
8
|
+
|
|
9
|
+
const GAS_SAFE_OVERHEAD = 1000n;
|
|
10
|
+
|
|
11
|
+
export type GasFundingMode = 'classic' | 'addressBalance' | 'mixedTopUp';
|
|
12
|
+
|
|
13
|
+
export interface GasFundingResult {
|
|
14
|
+
mode: GasFundingMode;
|
|
15
|
+
gasBudget: bigint;
|
|
16
|
+
coinBalance: bigint;
|
|
17
|
+
addressBalance: bigint;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class InsufficientGasFundsError extends Error {
|
|
21
|
+
readonly gasBudget: bigint;
|
|
22
|
+
|
|
23
|
+
readonly coinBalance: bigint;
|
|
24
|
+
|
|
25
|
+
readonly addressBalance: bigint;
|
|
26
|
+
|
|
27
|
+
constructor(gasBudget: bigint, coinBalance: bigint, addressBalance: bigint) {
|
|
28
|
+
const total = coinBalance + addressBalance;
|
|
29
|
+
super(
|
|
30
|
+
`Insufficient gas funds: need ${gasBudget}, have coinBalance=${coinBalance} + addressBalance=${addressBalance} = ${total}`,
|
|
31
|
+
);
|
|
32
|
+
this.name = 'InsufficientGasFundsError';
|
|
33
|
+
this.gasBudget = gasBudget;
|
|
34
|
+
this.coinBalance = coinBalance;
|
|
35
|
+
this.addressBalance = addressBalance;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gas budget must cover pre-rebate costs (computation + storage + overhead).
|
|
41
|
+
* storageRebate is refunded after execution and must not reduce the budget.
|
|
42
|
+
*/
|
|
43
|
+
export function computeGasBudget(
|
|
44
|
+
gasUsed: Pick<SuiClientTypes.GasCostSummary, 'computationCost' | 'storageCost'>,
|
|
45
|
+
gasPrice: bigint,
|
|
46
|
+
): bigint {
|
|
47
|
+
const { computationCost, storageCost } = gasUsed;
|
|
48
|
+
const safeOverhead = GAS_SAFE_OVERHEAD * gasPrice;
|
|
49
|
+
return BigInt(computationCost) + BigInt(storageCost) + safeOverhead;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type PaymentCoinRef = { objectId: string; version: string; digest: string; balance: bigint };
|
|
53
|
+
|
|
54
|
+
interface SuiGasFunding {
|
|
55
|
+
paymentCoins: PaymentCoinRef[];
|
|
56
|
+
coinBalance: bigint;
|
|
57
|
+
addressBalance: bigint;
|
|
58
|
+
total: bigint;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectUsedObjectIds(tx: Transaction): Set<string> {
|
|
62
|
+
return tx.getData().inputs.reduce<Set<string>>((used, input) => {
|
|
63
|
+
const immOrOwned = input.Object?.ImmOrOwnedObject?.objectId;
|
|
64
|
+
if (immOrOwned) {
|
|
65
|
+
used.add(normalizeSuiObjectId(immOrOwned));
|
|
66
|
+
return used;
|
|
67
|
+
}
|
|
68
|
+
const unresolved = input.UnresolvedObject?.objectId;
|
|
69
|
+
if (unresolved) {
|
|
70
|
+
used.add(normalizeSuiObjectId(unresolved));
|
|
71
|
+
}
|
|
72
|
+
return used;
|
|
73
|
+
}, new Set<string>());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadSuiGasFunding(suiClient: SuiGrpcClient, owner: string, tx: Transaction): Promise<SuiGasFunding> {
|
|
77
|
+
const usedObjectIds = collectUsedObjectIds(tx);
|
|
78
|
+
const [coins, balanceRes] = await Promise.all([
|
|
79
|
+
getAllCoins({ suiClient, owner, coinType: SUI_COIN }),
|
|
80
|
+
suiClient.getBalance({ owner, coinType: SUI_COIN }),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const paymentCoins = coins
|
|
84
|
+
.filter((coin) => !usedObjectIds.has(normalizeSuiObjectId(coin.objectId)) && BigInt(coin.balance) > 0n)
|
|
85
|
+
.map((coin) => ({
|
|
86
|
+
objectId: coin.objectId,
|
|
87
|
+
version: coin.version,
|
|
88
|
+
digest: coin.digest,
|
|
89
|
+
balance: BigInt(coin.balance),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const coinBalance = paymentCoins.reduce((sum, coin) => sum + coin.balance, 0n);
|
|
93
|
+
const addressBalance = BigInt(balanceRes.balance.addressBalance);
|
|
94
|
+
return {
|
|
95
|
+
paymentCoins,
|
|
96
|
+
coinBalance,
|
|
97
|
+
addressBalance,
|
|
98
|
+
total: coinBalance + addressBalance,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toObjectRefs(coins: PaymentCoinRef[]) {
|
|
103
|
+
return coins.map(({ objectId, version, digest }) => ({ objectId, version, digest }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Top up classic gas payment from address balance via compatibility coin reservation.
|
|
108
|
+
*
|
|
109
|
+
* Protocol gas-smashing (real Coin listed first): the reservation is withdrawn from
|
|
110
|
+
* address balance into that gas coin — the gas-payment equivalent of redeem_funds +
|
|
111
|
+
* mergeCoins into the primary gas coin, and it satisfies the upfront gas-budget check.
|
|
112
|
+
* (A PTB-only redeem+merge cannot fund gasBudget > coinBalance because budget is
|
|
113
|
+
* checked against gas payment before commands execute.)
|
|
114
|
+
*/
|
|
115
|
+
async function applyMixedTopUp(input: {
|
|
116
|
+
tx: Transaction;
|
|
117
|
+
suiClient: SuiGrpcClient;
|
|
118
|
+
owner: string;
|
|
119
|
+
paymentCoins: PaymentCoinRef[];
|
|
120
|
+
topUpAmount: bigint;
|
|
121
|
+
}): Promise<void> {
|
|
122
|
+
const { tx, suiClient, owner, paymentCoins, topUpAmount } = input;
|
|
123
|
+
if (paymentCoins.length === 0) {
|
|
124
|
+
throw new Error('mixed gas top-up requires at least one SUI coin object');
|
|
125
|
+
}
|
|
126
|
+
if (topUpAmount <= 0n) {
|
|
127
|
+
tx.setGasPayment(toObjectRefs(paymentCoins));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const [{ chainIdentifier }, { systemState }] = await Promise.all([
|
|
132
|
+
suiClient.core.getChainIdentifier(),
|
|
133
|
+
suiClient.core.getCurrentSystemState(),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const reservation = createCoinReservationRef(topUpAmount, owner, chainIdentifier, systemState.epoch);
|
|
137
|
+
|
|
138
|
+
// Real Coin first so reservation is smashed into the gas coin (not into address balance).
|
|
139
|
+
tx.setGasPayment([...toObjectRefs(paymentCoins), reservation]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Select gas funding before tx.build() according to mixed coin/addressBalance policy:
|
|
144
|
+
* 1) budget without storageRebate (caller supplies computeGasBudget result)
|
|
145
|
+
* 2-3) fail if total < budget
|
|
146
|
+
* 4) coins exist and coinBal >= budget → classic setGasPayment(coins)
|
|
147
|
+
* 5) coins exist and total >= budget → mixed top-up via coin reservation
|
|
148
|
+
* 6) no usable coins and addrBal >= budget → Address Balance gas (payment: [])
|
|
149
|
+
*/
|
|
150
|
+
export async function selectGasFunding(input: {
|
|
151
|
+
tx: Transaction;
|
|
152
|
+
suiClient: SuiGrpcClient;
|
|
153
|
+
owner: string;
|
|
154
|
+
gasBudget: bigint;
|
|
155
|
+
}): Promise<GasFundingResult> {
|
|
156
|
+
const { tx, suiClient, owner, gasBudget } = input;
|
|
157
|
+
const { payment } = tx.getData().gasData;
|
|
158
|
+
|
|
159
|
+
// Only respect an explicit *classic* gas payment (non-empty ObjectRefs).
|
|
160
|
+
// Do NOT respect payment: [] — Mysten auto-pick and plain-tx
|
|
161
|
+
// Transaction.from(builtHex) often bake address-balance gas; respecting it
|
|
162
|
+
// skips re-selection and fails InsufficientGas under a tight budget.
|
|
163
|
+
// Selection below always overwrites payment via setGasPayment(...).
|
|
164
|
+
if (payment != null && payment.length > 0) {
|
|
165
|
+
const funding = await loadSuiGasFunding(suiClient, owner, tx);
|
|
166
|
+
return {
|
|
167
|
+
mode: 'classic',
|
|
168
|
+
gasBudget,
|
|
169
|
+
coinBalance: funding.coinBalance,
|
|
170
|
+
addressBalance: funding.addressBalance,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const funding = await loadSuiGasFunding(suiClient, owner, tx);
|
|
175
|
+
const { paymentCoins, coinBalance, addressBalance, total } = funding;
|
|
176
|
+
|
|
177
|
+
if (total < gasBudget) {
|
|
178
|
+
throw new InsufficientGasFundsError(gasBudget, coinBalance, addressBalance);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Prefer classic / mixed whenever owned SUI coins exist. Pure address-balance
|
|
182
|
+
// gas (payment: []) is stricter on budget and is what Mysten auto-picks when
|
|
183
|
+
// addressBalance > 0 — avoid it unless there is no usable gas coin.
|
|
184
|
+
if (paymentCoins.length > 0 && coinBalance >= gasBudget) {
|
|
185
|
+
tx.setGasPayment(toObjectRefs(paymentCoins));
|
|
186
|
+
return { mode: 'classic', gasBudget, coinBalance, addressBalance };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (paymentCoins.length > 0 && total >= gasBudget) {
|
|
190
|
+
const topUpAmount = gasBudget - coinBalance; // > 0 and <= addressBalance
|
|
191
|
+
await applyMixedTopUp({
|
|
192
|
+
tx,
|
|
193
|
+
suiClient,
|
|
194
|
+
owner,
|
|
195
|
+
paymentCoins,
|
|
196
|
+
topUpAmount,
|
|
197
|
+
});
|
|
198
|
+
return { mode: 'mixedTopUp', gasBudget, coinBalance, addressBalance };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (addressBalance >= gasBudget) {
|
|
202
|
+
tx.setGasPayment([]);
|
|
203
|
+
return { mode: 'addressBalance', gasBudget, coinBalance, addressBalance };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new InsufficientGasFundsError(gasBudget, coinBalance, addressBalance);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Estimate gas budget (no storageRebate) using a probe build/simulate.
|
|
211
|
+
* Prefers classic coin gas when coin objects exist so the estimate matches the
|
|
212
|
+
* cheaper payment path. Budget is left unset so Mysten can dry-run with MAX_GAS
|
|
213
|
+
* overrides (avoids locking the full balance away from GasCoin splits).
|
|
214
|
+
*/
|
|
215
|
+
export async function estimateGasBudget(input: {
|
|
216
|
+
tx: Transaction;
|
|
217
|
+
suiClient: SuiGrpcClient;
|
|
218
|
+
owner: string;
|
|
219
|
+
gasPrice: bigint;
|
|
220
|
+
}): Promise<bigint> {
|
|
221
|
+
const { tx, suiClient, owner, gasPrice } = input;
|
|
222
|
+
const funding = await loadSuiGasFunding(suiClient, owner, tx);
|
|
223
|
+
if (funding.total <= 0n) {
|
|
224
|
+
throw new InsufficientGasFundsError(0n, funding.coinBalance, funding.addressBalance);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const probe = Transaction.from(tx);
|
|
228
|
+
probe.setSender(owner);
|
|
229
|
+
probe.setGasPrice(gasPrice);
|
|
230
|
+
|
|
231
|
+
// Prefer classic coins for the probe when available (cheaper than address-balance gas).
|
|
232
|
+
if (funding.paymentCoins.length > 0) {
|
|
233
|
+
probe.setGasPayment(toObjectRefs(funding.paymentCoins));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let built: Uint8Array;
|
|
237
|
+
try {
|
|
238
|
+
built = await probe.build({ client: suiClient });
|
|
239
|
+
} catch (e: unknown) {
|
|
240
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
241
|
+
throw new Error(`Failed to estimate gas budget: ${message}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const inspect = await suiClient.simulateTransaction({
|
|
245
|
+
transaction: built,
|
|
246
|
+
include: { effects: true },
|
|
247
|
+
});
|
|
248
|
+
const effects = (inspect.Transaction ?? inspect.FailedTransaction)?.effects;
|
|
249
|
+
if (!effects?.gasUsed) {
|
|
250
|
+
throw new Error('Failed to estimate gas budget: simulateTransaction returned no gasUsed');
|
|
251
|
+
}
|
|
252
|
+
if (effects.status.success !== true) {
|
|
253
|
+
const err = effects.status.success === false ? effects.status.error?.message : undefined;
|
|
254
|
+
throw new Error(`Failed to estimate gas budget: ${err ?? 'simulation failed'}`);
|
|
255
|
+
}
|
|
256
|
+
return computeGasBudget(effects.gasUsed, gasPrice);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve gasBudget (no rebate) and apply gas funding selection before build.
|
|
261
|
+
*/
|
|
262
|
+
export async function prepareGasFunding(input: {
|
|
263
|
+
tx: Transaction;
|
|
264
|
+
suiClient: SuiGrpcClient;
|
|
265
|
+
owner: string;
|
|
266
|
+
gasPrice: bigint;
|
|
267
|
+
gasBudget?: bigint;
|
|
268
|
+
}): Promise<GasFundingResult> {
|
|
269
|
+
const { tx, suiClient, owner, gasPrice } = input;
|
|
270
|
+
const { payment, budget: existingBudget } = tx.getData().gasData;
|
|
271
|
+
const bakedAddressBalanceGas = payment != null && payment.length === 0;
|
|
272
|
+
|
|
273
|
+
// Never trust a budget baked into plain-tx / Mysten auto AB-gas txs.
|
|
274
|
+
// Those budgets are often under-estimated for payment: [] and would skip
|
|
275
|
+
// estimateGasBudget if we kept existingBudget.
|
|
276
|
+
let { gasBudget } = input;
|
|
277
|
+
if (gasBudget == null) {
|
|
278
|
+
if (bakedAddressBalanceGas || existingBudget == null) {
|
|
279
|
+
gasBudget = await estimateGasBudget({ tx, suiClient, owner, gasPrice });
|
|
280
|
+
} else {
|
|
281
|
+
gasBudget = BigInt(existingBudget);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
tx.setGasBudget(gasBudget);
|
|
286
|
+
return selectGasFunding({ tx, suiClient, owner, gasBudget });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @deprecated Use {@link prepareGasFunding} / {@link selectGasFunding}.
|
|
291
|
+
* Kept as a thin classic-only helper for callers that only want coin gas when available.
|
|
292
|
+
*/
|
|
293
|
+
export async function preferClassicGasPayment(input: {
|
|
294
|
+
tx: Transaction;
|
|
295
|
+
suiClient: SuiGrpcClient;
|
|
296
|
+
owner: string;
|
|
297
|
+
}): Promise<boolean> {
|
|
298
|
+
const { tx, suiClient, owner } = input;
|
|
299
|
+
const { payment } = tx.getData().gasData;
|
|
300
|
+
if (payment != null) {
|
|
301
|
+
return payment.length > 0;
|
|
302
|
+
}
|
|
303
|
+
const funding = await loadSuiGasFunding(suiClient, owner, tx);
|
|
304
|
+
if (funding.paymentCoins.length === 0) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
tx.setGasPayment(toObjectRefs(funding.paymentCoins));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/transaction.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { Transaction, isTransaction } from '@mysten/sui/transactions';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Normalize a transaction value to {@link Transaction}
|
|
4
|
+
* Normalize a transaction value to {@link Transaction}.
|
|
5
|
+
* Prefer V2 restore paths — avoid deprecated V1 serialize() when possible
|
|
6
|
+
* (V1 drops CallArg::FundsWithdrawal).
|
|
5
7
|
*/
|
|
6
8
|
export function toSuiTransaction(txb: Transaction | unknown): Transaction {
|
|
7
9
|
if (isTransaction(txb)) {
|
|
8
10
|
return txb as Transaction;
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
if (typeof txb === 'string' || txb instanceof Uint8Array) {
|
|
14
|
+
return Transaction.from(txb);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Last resort for true legacy TransactionBlock (pre-Transaction API).
|
|
18
|
+
// Modern Transaction instances are handled by isTransaction above.
|
|
19
|
+
const legacy = txb as { serialize?: () => string };
|
|
20
|
+
if (typeof legacy?.serialize === 'function') {
|
|
21
|
+
return Transaction.from(legacy.serialize()) as Transaction;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error('Unsupported transaction value for toSuiTransaction');
|
|
12
25
|
}
|