@mysten/sui 2.29.0 → 2.31.0
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/CHANGELOG.md +43 -0
- package/dist/bcs/bcs.d.mts +6 -6
- package/dist/bcs/bcs.d.mts.map +1 -1
- package/dist/bcs/bcs.mjs +5 -1
- package/dist/bcs/bcs.mjs.map +1 -1
- package/dist/bcs/index.d.mts +139 -47
- package/dist/bcs/index.d.mts.map +1 -1
- package/dist/bcs/types.d.mts.map +1 -1
- package/dist/client/core-resolver.d.mts.map +1 -1
- package/dist/client/core-resolver.mjs +4 -6
- package/dist/client/core-resolver.mjs.map +1 -1
- package/dist/client/transaction-resolver.mjs +35 -8
- package/dist/client/transaction-resolver.mjs.map +1 -1
- package/dist/cryptography/signature.d.mts +8 -8
- package/dist/graphql/generated/tada-env.d.mts +74 -0
- package/dist/grpc/proto/sui/rpc/v2/input.d.mts +21 -1
- package/dist/grpc/proto/sui/rpc/v2/input.d.mts.map +1 -1
- package/dist/grpc/proto/sui/rpc/v2/input.mjs +21 -0
- package/dist/grpc/proto/sui/rpc/v2/input.mjs.map +1 -1
- package/dist/grpc/proto/sui/rpc/v2/ledger_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/move_package_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/signature_verification_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/state_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/subscription_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/transaction_execution_service.client.d.mts +4 -4
- package/dist/jsonRpc/core.mjs +1 -1
- package/dist/jsonRpc/core.mjs.map +1 -1
- package/dist/transactions/Transaction.d.mts +48 -46
- package/dist/transactions/Transaction.d.mts.map +1 -1
- package/dist/transactions/Transaction.mjs +65 -24
- package/dist/transactions/Transaction.mjs.map +1 -1
- package/dist/transactions/data/internal.d.mts +12 -0
- package/dist/transactions/data/internal.d.mts.map +1 -1
- package/dist/transactions/data/internal.mjs +5 -1
- package/dist/transactions/data/internal.mjs.map +1 -1
- package/dist/transactions/data/v2.d.mts +5 -0
- package/dist/transactions/data/v2.d.mts.map +1 -1
- package/dist/transactions/index.d.mts +3 -2
- package/dist/transactions/intents/AllowanceBalance.mjs +147 -0
- package/dist/transactions/intents/AllowanceBalance.mjs.map +1 -0
- package/dist/transactions/intents/BalanceIntentNames.mjs +7 -0
- package/dist/transactions/intents/BalanceIntentNames.mjs.map +1 -0
- package/dist/transactions/intents/BalanceOptions.d.mts +27 -0
- package/dist/transactions/intents/BalanceOptions.d.mts.map +1 -0
- package/dist/transactions/intents/BalanceOptions.mjs +12 -0
- package/dist/transactions/intents/BalanceOptions.mjs.map +1 -0
- package/dist/transactions/intents/CoinWithBalance.d.mts.map +1 -1
- package/dist/transactions/intents/CoinWithBalance.mjs +35 -12
- package/dist/transactions/intents/CoinWithBalance.mjs.map +1 -1
- package/dist/transactions/intents/ResolveBalances.mjs +16 -0
- package/dist/transactions/intents/ResolveBalances.mjs.map +1 -0
- package/dist/transactions/resolution-utils.mjs +13 -0
- package/dist/transactions/resolution-utils.mjs.map +1 -0
- package/dist/transactions/resolve.d.mts +8 -0
- package/dist/transactions/resolve.d.mts.map +1 -1
- package/dist/transactions/resolve.mjs +7 -3
- package/dist/transactions/resolve.mjs.map +1 -1
- package/dist/version.mjs +1 -1
- package/dist/version.mjs.map +1 -1
- package/dist/zklogin/bcs.d.mts +14 -14
- package/docs/llms-index.md +1 -0
- package/docs/migrations/sui-2.0/json-rpc-migration.md +50 -1
- package/docs/transactions/allowances.md +176 -0
- package/docs/transactions/basics.md +19 -14
- package/docs/transactions/coins-and-balances.md +12 -9
- package/docs/transactions/offline.md +137 -88
- package/package.json +2 -2
- package/src/bcs/bcs.ts +4 -0
- package/src/bcs/types.ts +2 -1
- package/src/client/core-resolver.ts +8 -11
- package/src/client/transaction-resolver.ts +43 -9
- package/src/graphql/generated/schema.graphql +87 -42
- package/src/graphql/generated/tada-env.ts +84 -0
- package/src/grpc/proto/sui/rpc/v2/input.ts +22 -0
- package/src/jsonRpc/core.ts +3 -4
- package/src/transactions/Transaction.ts +71 -53
- package/src/transactions/data/internal.ts +4 -0
- package/src/transactions/index.ts +3 -0
- package/src/transactions/intents/AllowanceBalance.ts +204 -0
- package/src/transactions/intents/BalanceIntentNames.ts +5 -0
- package/src/transactions/intents/BalanceOptions.ts +45 -0
- package/src/transactions/intents/CoinWithBalance.ts +82 -32
- package/src/transactions/intents/ResolveBalances.ts +30 -0
- package/src/transactions/resolution-utils.ts +18 -0
- package/src/transactions/resolve.ts +35 -2
- package/src/version.ts +1 -1
|
@@ -294,6 +294,10 @@ export type WithdrawalTypeArg = InferOutput<typeof WithdrawalTypeArgSchema>;
|
|
|
294
294
|
export const WithdrawFromSchema = safeEnum({
|
|
295
295
|
Sender: literal(true),
|
|
296
296
|
Sponsor: literal(true),
|
|
297
|
+
SenderAllowance: object({
|
|
298
|
+
funder: SuiAddress,
|
|
299
|
+
allowance: ObjectID,
|
|
300
|
+
}),
|
|
297
301
|
});
|
|
298
302
|
export type WithdrawFrom = InferOutput<typeof WithdrawFromSchema>;
|
|
299
303
|
|
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
type TransactionObjectArgument,
|
|
19
19
|
type TransactionResult,
|
|
20
20
|
type TransactionCopyOptions,
|
|
21
|
+
type WithdrawalOptions,
|
|
21
22
|
} from './Transaction.js';
|
|
22
23
|
|
|
23
24
|
export { type SerializedTransactionDataV2 } from './data/v2.js';
|
|
@@ -48,3 +49,5 @@ export type {
|
|
|
48
49
|
export { Arguments } from './Arguments.js';
|
|
49
50
|
|
|
50
51
|
export { isArgument } from './utils.js';
|
|
52
|
+
|
|
53
|
+
export type { BalanceOptions, AllowanceReference } from './intents/BalanceOptions.js';
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { object, optional, parse, picklist, string } from 'valibot';
|
|
5
|
+
|
|
6
|
+
import { hasMvrName } from '../../client/mvr.js';
|
|
7
|
+
|
|
8
|
+
import { ALLOWANCE_BALANCE } from './BalanceIntentNames.js';
|
|
9
|
+
import { resolveBalances } from './ResolveBalances.js';
|
|
10
|
+
|
|
11
|
+
import { bcs } from '../../bcs/index.js';
|
|
12
|
+
import type { SuiClientTypes } from '../../client/index.js';
|
|
13
|
+
import { normalizeStructTag, normalizeSuiAddress } from '../../utils/sui-types.js';
|
|
14
|
+
import { TransactionCommands } from '../Commands.js';
|
|
15
|
+
import type { Argument } from '../data/internal.js';
|
|
16
|
+
import { Inputs } from '../Inputs.js';
|
|
17
|
+
import { getClient } from '../resolve.js';
|
|
18
|
+
import type { TransactionPlugin } from '../resolve.js';
|
|
19
|
+
import type { Transaction } from '../Transaction.js';
|
|
20
|
+
import type { AllowanceReference } from './BalanceOptions.js';
|
|
21
|
+
|
|
22
|
+
export { ALLOWANCE_BALANCE } from './BalanceIntentNames.js';
|
|
23
|
+
|
|
24
|
+
const AllowanceBalanceData = object({
|
|
25
|
+
objectId: string(),
|
|
26
|
+
funder: optional(string()),
|
|
27
|
+
appType: optional(string()),
|
|
28
|
+
type: string(),
|
|
29
|
+
amount: string(),
|
|
30
|
+
outputKind: picklist(['coin', 'balance']),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Read only the prefix needed for spending. The remaining settings and current_spend
|
|
34
|
+
// are checked by Move, so this does not need to model rate limits or their policy.
|
|
35
|
+
const AllowanceMetadata = bcs.struct('AllowanceMetadata', {
|
|
36
|
+
id: bcs.Address,
|
|
37
|
+
funder: bcs.Address,
|
|
38
|
+
spender: bcs.option(bcs.Address),
|
|
39
|
+
app: bcs.option(bcs.string()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export function allowanceBalance({
|
|
43
|
+
allowance,
|
|
44
|
+
amount,
|
|
45
|
+
type = '0x2::sui::SUI',
|
|
46
|
+
outputKind,
|
|
47
|
+
}: {
|
|
48
|
+
allowance: string | AllowanceReference;
|
|
49
|
+
amount: bigint;
|
|
50
|
+
type?: string;
|
|
51
|
+
outputKind: 'coin' | 'balance';
|
|
52
|
+
}) {
|
|
53
|
+
bcs.U64.validate(amount);
|
|
54
|
+
const objectId = normalizeSuiAddress(
|
|
55
|
+
typeof allowance === 'string' ? allowance : allowance.objectId,
|
|
56
|
+
);
|
|
57
|
+
const reference = typeof allowance === 'string' ? { objectId } : allowance;
|
|
58
|
+
return (tx: Transaction) => {
|
|
59
|
+
tx.addIntentResolver(ALLOWANCE_BALANCE, resolveBalances);
|
|
60
|
+
return tx.add(
|
|
61
|
+
TransactionCommands.Intent({
|
|
62
|
+
name: ALLOWANCE_BALANCE,
|
|
63
|
+
inputs: {
|
|
64
|
+
allowance: tx.object(objectId),
|
|
65
|
+
clock: tx.object.clock(),
|
|
66
|
+
...(reference.app ? { permit: tx.object(reference.app.permit) } : {}),
|
|
67
|
+
},
|
|
68
|
+
data: {
|
|
69
|
+
objectId,
|
|
70
|
+
funder: reference.funder ? normalizeSuiAddress(reference.funder) : undefined,
|
|
71
|
+
appType: reference.app ? normalizeStructTag(reference.app.type) : undefined,
|
|
72
|
+
type: normalizeStructTag(type),
|
|
73
|
+
amount: String(amount),
|
|
74
|
+
outputKind,
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const resolveAllowanceBalance: TransactionPlugin = async (
|
|
82
|
+
transactionData,
|
|
83
|
+
options,
|
|
84
|
+
next,
|
|
85
|
+
) => {
|
|
86
|
+
const ids = new Set<string>();
|
|
87
|
+
const namedTypes = new Set<string>();
|
|
88
|
+
for (const command of transactionData.commands) {
|
|
89
|
+
if (command.$kind !== '$Intent' || command.$Intent.name !== ALLOWANCE_BALANCE) continue;
|
|
90
|
+
const data = parse(AllowanceBalanceData, command.$Intent.data);
|
|
91
|
+
if (!data.funder) ids.add(data.objectId);
|
|
92
|
+
for (const type of [data.type, data.appType]) {
|
|
93
|
+
if (type && hasMvrName(type)) namedTypes.add(type);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [resolvedTypes, objects] = await Promise.all([
|
|
98
|
+
namedTypes.size ? getClient(options).core.mvr.resolve({ types: [...namedTypes] }) : undefined,
|
|
99
|
+
ids.size
|
|
100
|
+
? getClient(options)
|
|
101
|
+
.core.getObjects({ objectIds: [...ids], include: { content: true } })
|
|
102
|
+
.then((result) => result.objects)
|
|
103
|
+
: [],
|
|
104
|
+
]);
|
|
105
|
+
const allowances = new Map<string, SuiClientTypes.Object<{ content: true }>>();
|
|
106
|
+
for (const object of objects) {
|
|
107
|
+
if (object instanceof Error) throw object;
|
|
108
|
+
allowances.set(object.objectId, object);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let index = 0; index < transactionData.commands.length; index++) {
|
|
112
|
+
const command = transactionData.commands[index];
|
|
113
|
+
if (command.$kind !== '$Intent' || command.$Intent.name !== ALLOWANCE_BALANCE) continue;
|
|
114
|
+
const data = parse(AllowanceBalanceData, command.$Intent.data);
|
|
115
|
+
if (hasMvrName(data.type)) {
|
|
116
|
+
const resolved = resolvedTypes?.types[data.type];
|
|
117
|
+
if (!resolved) throw new Error(`No resolution found for type: ${data.type}`);
|
|
118
|
+
data.type = normalizeStructTag(resolved.type);
|
|
119
|
+
}
|
|
120
|
+
if (data.appType && hasMvrName(data.appType)) {
|
|
121
|
+
const resolved = resolvedTypes?.types[data.appType];
|
|
122
|
+
if (!resolved) throw new Error(`No resolution found for type: ${data.appType}`);
|
|
123
|
+
data.appType = normalizeStructTag(resolved.type);
|
|
124
|
+
}
|
|
125
|
+
let funder = data.funder;
|
|
126
|
+
if (!funder) {
|
|
127
|
+
const allowance = allowances.get(data.objectId);
|
|
128
|
+
const expectedType = normalizeStructTag(
|
|
129
|
+
`0x2::allowance::Allowance<0x2::balance::Balance<${data.type}>>`,
|
|
130
|
+
);
|
|
131
|
+
if (!allowance || allowance.type !== expectedType || allowance.owner.$kind !== 'Shared') {
|
|
132
|
+
throw new Error(`Expected a shared ${expectedType} allowance at ${data.objectId}`);
|
|
133
|
+
}
|
|
134
|
+
const metadata = AllowanceMetadata.parse(allowance.content);
|
|
135
|
+
if (metadata.app !== null && !data.appType) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Allowance ${data.objectId} is app-bound; pass allowance.app with the app type and SpendPermit`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
data.appType &&
|
|
142
|
+
(metadata.app === null || normalizeStructTag(metadata.app) !== data.appType)
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(`Allowance ${data.objectId} is not bound to app ${data.appType}`);
|
|
145
|
+
}
|
|
146
|
+
funder = metadata.funder;
|
|
147
|
+
const input = command.$Intent.inputs.allowance as Argument;
|
|
148
|
+
if (input.$kind === 'Input') {
|
|
149
|
+
transactionData.inputs[input.Input] = Inputs.SharedObjectRef({
|
|
150
|
+
objectId: data.objectId,
|
|
151
|
+
initialSharedVersion: allowance.owner.Shared.initialSharedVersion,
|
|
152
|
+
mutable: true,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Spending mutates the allowance even when the caller supplied the funder
|
|
157
|
+
// and we did not fetch metadata. Reuse and upgrade its existing input.
|
|
158
|
+
const allowanceArgument = command.$Intent.inputs.allowance as Argument;
|
|
159
|
+
if (allowanceArgument.$kind === 'Input') {
|
|
160
|
+
const input = transactionData.inputs[allowanceArgument.Input];
|
|
161
|
+
if (input.Object?.SharedObject) input.Object.SharedObject.mutable = true;
|
|
162
|
+
if (input.UnresolvedObject) input.UnresolvedObject.mutable = true;
|
|
163
|
+
}
|
|
164
|
+
const withdrawal = transactionData.addInput(
|
|
165
|
+
'withdrawal',
|
|
166
|
+
Inputs.FundsWithdrawal({
|
|
167
|
+
reservation: { $kind: 'MaxAmountU64', MaxAmountU64: data.amount },
|
|
168
|
+
typeArg: { $kind: 'Balance', Balance: data.type },
|
|
169
|
+
withdrawFrom: {
|
|
170
|
+
$kind: 'SenderAllowance',
|
|
171
|
+
SenderAllowance: { funder, allowance: data.objectId },
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
const commands = [
|
|
176
|
+
TransactionCommands.MoveCall({
|
|
177
|
+
target: data.appType
|
|
178
|
+
? '0x2::allowance::app_balance_spend'
|
|
179
|
+
: '0x2::allowance::balance_spend',
|
|
180
|
+
typeArguments: data.appType ? [data.type, data.appType] : [data.type],
|
|
181
|
+
arguments: [
|
|
182
|
+
command.$Intent.inputs.allowance as Argument,
|
|
183
|
+
...(data.appType ? [command.$Intent.inputs.permit as Argument] : []),
|
|
184
|
+
withdrawal,
|
|
185
|
+
command.$Intent.inputs.clock as Argument,
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
];
|
|
189
|
+
if (data.outputKind === 'coin') {
|
|
190
|
+
commands.push(
|
|
191
|
+
TransactionCommands.MoveCall({
|
|
192
|
+
target: '0x2::coin::from_balance',
|
|
193
|
+
typeArguments: [data.type],
|
|
194
|
+
arguments: [{ $kind: 'Result', Result: index }],
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
transactionData.replaceCommand(index, commands, {
|
|
199
|
+
NestedResult: [index + commands.length - 1, 0],
|
|
200
|
+
});
|
|
201
|
+
index += commands.length - 1;
|
|
202
|
+
}
|
|
203
|
+
return next();
|
|
204
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { TransactionObjectArgument } from '../Transaction.js';
|
|
5
|
+
|
|
6
|
+
/** An allowance with optional funder metadata and app authorization. */
|
|
7
|
+
export interface AllowanceReference {
|
|
8
|
+
objectId: string;
|
|
9
|
+
/** Skip the metadata lookup when the funder is already known. */
|
|
10
|
+
funder?: string;
|
|
11
|
+
/** Authorization for an app-bound allowance. */
|
|
12
|
+
app?: {
|
|
13
|
+
/** The app type A in SpendPermit<A>. */
|
|
14
|
+
type: string;
|
|
15
|
+
/** A SpendPermit<A> from the app, consumed by this spend. */
|
|
16
|
+
permit: TransactionObjectArgument;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type BalanceOptions = {
|
|
21
|
+
/** The coin type T, not Balance<T>. Defaults to SUI. */
|
|
22
|
+
type?: string;
|
|
23
|
+
balance: bigint | number | string;
|
|
24
|
+
} & (
|
|
25
|
+
| { allowance?: never; useGasCoin?: boolean }
|
|
26
|
+
| {
|
|
27
|
+
/** An allowance ID or reference with optional app authorization. Never falls back to sender funds. */
|
|
28
|
+
allowance: string | AllowanceReference;
|
|
29
|
+
useGasCoin?: never;
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export function normalizeBalance(options: BalanceOptions): bigint {
|
|
34
|
+
if (options.allowance !== undefined && options.useGasCoin !== undefined) {
|
|
35
|
+
throw new Error('useGasCoin cannot be combined with allowance');
|
|
36
|
+
}
|
|
37
|
+
const amount = options.balance;
|
|
38
|
+
if (typeof amount === 'string' && !/^[0-9]+$/.test(amount)) {
|
|
39
|
+
throw new Error('Amount must be a non-empty string of decimal digits');
|
|
40
|
+
}
|
|
41
|
+
if (typeof amount === 'number' && !Number.isSafeInteger(amount)) {
|
|
42
|
+
throw new Error('Amount must be a safe integer; use bigint or a string for larger amounts');
|
|
43
|
+
}
|
|
44
|
+
return BigInt(amount);
|
|
45
|
+
}
|
|
@@ -16,8 +16,11 @@ import {
|
|
|
16
16
|
union,
|
|
17
17
|
} from 'valibot';
|
|
18
18
|
|
|
19
|
+
import { ALLOWANCE_BALANCE, COIN_WITH_BALANCE } from './BalanceIntentNames.js';
|
|
20
|
+
import { resolveBalances } from './ResolveBalances.js';
|
|
21
|
+
|
|
19
22
|
import { bcs } from '../../bcs/index.js';
|
|
20
|
-
import { normalizeStructTag } from '../../utils/sui-types.js';
|
|
23
|
+
import { normalizeStructTag, normalizeSuiAddress } from '../../utils/sui-types.js';
|
|
21
24
|
import { TransactionCommands } from '../Commands.js';
|
|
22
25
|
import type { Argument } from '../data/internal.js';
|
|
23
26
|
import { Inputs } from '../Inputs.js';
|
|
@@ -26,7 +29,7 @@ import type { Transaction, TransactionResult } from '../Transaction.js';
|
|
|
26
29
|
import type { TransactionDataBuilder } from '../TransactionData.js';
|
|
27
30
|
import type { ClientWithCoreApi, SuiClientTypes } from '../../client/index.js';
|
|
28
31
|
|
|
29
|
-
export
|
|
32
|
+
export { COIN_WITH_BALANCE } from './BalanceIntentNames.js';
|
|
30
33
|
const SUI_TYPE = normalizeStructTag('0x2::sui::SUI');
|
|
31
34
|
|
|
32
35
|
export function coinWithBalance({
|
|
@@ -45,7 +48,7 @@ export function coinWithBalance({
|
|
|
45
48
|
return coinResult;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
tx.addIntentResolver(COIN_WITH_BALANCE,
|
|
51
|
+
tx.addIntentResolver(COIN_WITH_BALANCE, resolveBalances);
|
|
49
52
|
const coinType = type === 'gas' ? type : normalizeStructTag(type);
|
|
50
53
|
|
|
51
54
|
coinResult = tx.add(
|
|
@@ -80,7 +83,7 @@ export function createBalance({
|
|
|
80
83
|
return balanceResult;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
tx.addIntentResolver(COIN_WITH_BALANCE,
|
|
86
|
+
tx.addIntentResolver(COIN_WITH_BALANCE, resolveBalances);
|
|
84
87
|
const coinType = type === 'gas' ? type : normalizeStructTag(type);
|
|
85
88
|
|
|
86
89
|
balanceResult = tx.add(
|
|
@@ -126,6 +129,25 @@ export async function resolveCoinBalance(
|
|
|
126
129
|
throw new Error('Sender must be set to resolve CoinWithBalance');
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
if (transactionData.commands.some((command) => command.$Intent?.name === ALLOWANCE_BALANCE)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
'Resolve AllowanceBalance together with CoinWithBalance, or preserve both intents',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const reservedByType = new Map<string, bigint>();
|
|
138
|
+
for (const input of transactionData.inputs) {
|
|
139
|
+
if (!input.FundsWithdrawal) continue;
|
|
140
|
+
const { withdrawFrom, typeArg, reservation } = input.FundsWithdrawal;
|
|
141
|
+
const owner =
|
|
142
|
+
withdrawFrom.SenderAllowance?.funder ??
|
|
143
|
+
(withdrawFrom.Sender
|
|
144
|
+
? transactionData.sender
|
|
145
|
+
: (transactionData.gasData.owner ?? transactionData.sender));
|
|
146
|
+
if (normalizeSuiAddress(owner) !== normalizeSuiAddress(transactionData.sender)) continue;
|
|
147
|
+
const type = normalizeStructTag(typeArg.Balance);
|
|
148
|
+
reservedByType.set(type, (reservedByType.get(type) ?? 0n) + BigInt(reservation.MaxAmountU64));
|
|
149
|
+
}
|
|
150
|
+
|
|
129
151
|
// First pass: scan intents, collect per-type data, and resolve zero-balance intents in place.
|
|
130
152
|
for (const [i, command] of transactionData.commands.entries()) {
|
|
131
153
|
if (command.$kind !== '$Intent' || command.$Intent.name !== COIN_WITH_BALANCE) {
|
|
@@ -178,37 +200,48 @@ export async function resolveCoinBalance(
|
|
|
178
200
|
const coinsByType = new Map<string, SuiClientTypes.Coin[]>();
|
|
179
201
|
const addressBalanceByType = new Map<string, bigint>();
|
|
180
202
|
const client = buildOptions.client;
|
|
203
|
+
const assumeSufficientAddressBalances = buildOptions.assumeSufficientAddressBalances;
|
|
181
204
|
|
|
182
|
-
if (!client) {
|
|
205
|
+
if (!client && !assumeSufficientAddressBalances) {
|
|
183
206
|
throw new Error(
|
|
184
207
|
'Client must be provided to build or serialize transactions with CoinWithBalance intents',
|
|
185
208
|
);
|
|
186
209
|
}
|
|
187
210
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
if (assumeSufficientAddressBalances) {
|
|
212
|
+
for (const [coinType, balance] of totalByType) {
|
|
213
|
+
addressBalanceByType.set(coinType, balance);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
await Promise.all([
|
|
217
|
+
...[...coinTypes].map(async (coinType) => {
|
|
218
|
+
const { coins, addressBalance } = await getCoinsAndBalanceOfType({
|
|
219
|
+
coinType,
|
|
220
|
+
balance: totalByType.get(coinType)!,
|
|
221
|
+
client: client!,
|
|
222
|
+
owner: transactionData.sender!,
|
|
223
|
+
usedIds,
|
|
224
|
+
reserved: reservedByType.get(coinType) ?? 0n,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
coinsByType.set(coinType, coins);
|
|
228
|
+
addressBalanceByType.set(coinType, addressBalance);
|
|
229
|
+
}),
|
|
230
|
+
totalByType.has('gas')
|
|
231
|
+
? await client!.core
|
|
232
|
+
.getBalance({
|
|
233
|
+
owner: transactionData.sender!,
|
|
234
|
+
coinType: SUI_TYPE,
|
|
235
|
+
})
|
|
236
|
+
.then(({ balance }) => {
|
|
237
|
+
addressBalanceByType.set(
|
|
238
|
+
'gas',
|
|
239
|
+
availableAddressBalance(balance.addressBalance, reservedByType.get(SUI_TYPE) ?? 0n),
|
|
240
|
+
);
|
|
241
|
+
})
|
|
242
|
+
: null,
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
212
245
|
|
|
213
246
|
const exactBalanceByType = new Map<string, boolean>();
|
|
214
247
|
const usedAddressBalance = new Set<string>();
|
|
@@ -455,12 +488,14 @@ async function getCoinsAndBalanceOfType({
|
|
|
455
488
|
client,
|
|
456
489
|
owner,
|
|
457
490
|
usedIds,
|
|
491
|
+
reserved,
|
|
458
492
|
}: {
|
|
459
493
|
coinType: string;
|
|
460
494
|
balance: bigint;
|
|
461
495
|
client: ClientWithCoreApi;
|
|
462
496
|
owner: string;
|
|
463
497
|
usedIds: Set<string>;
|
|
498
|
+
reserved: bigint;
|
|
464
499
|
}): Promise<{
|
|
465
500
|
coins: SuiClientTypes.Coin[];
|
|
466
501
|
balance: bigint;
|
|
@@ -470,9 +505,14 @@ async function getCoinsAndBalanceOfType({
|
|
|
470
505
|
let remainingBalance = balance;
|
|
471
506
|
const coins: SuiClientTypes.Coin[] = [];
|
|
472
507
|
const balanceRequest = client.core.getBalance({ owner, coinType }).then(({ balance }) => {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
508
|
+
const addressBalance = availableAddressBalance(balance.addressBalance, reserved);
|
|
509
|
+
remainingBalance -= addressBalance;
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
...balance,
|
|
513
|
+
addressBalance: String(addressBalance),
|
|
514
|
+
balance: String(BigInt(balance.balance) - reserved),
|
|
515
|
+
};
|
|
476
516
|
});
|
|
477
517
|
|
|
478
518
|
const [allCoins, balanceResponse] = await Promise.all([loadMoreCoins(), balanceRequest]);
|
|
@@ -524,3 +564,13 @@ async function getCoinsAndBalanceOfType({
|
|
|
524
564
|
return coins;
|
|
525
565
|
}
|
|
526
566
|
}
|
|
567
|
+
|
|
568
|
+
function availableAddressBalance(balance: string, reserved: bigint): bigint {
|
|
569
|
+
const available = BigInt(balance) - reserved;
|
|
570
|
+
if (available < 0n) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`Insufficient address balance for existing withdrawals. Required: ${reserved}, Available: ${balance}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return available;
|
|
576
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { IntentResolverOptions } from '../resolve.js';
|
|
5
|
+
import type { TransactionDataBuilder } from '../TransactionData.js';
|
|
6
|
+
import { ALLOWANCE_BALANCE, COIN_WITH_BALANCE } from './BalanceIntentNames.js';
|
|
7
|
+
import { resolveAllowanceBalance } from './AllowanceBalance.js';
|
|
8
|
+
import { resolveCoinBalance } from './CoinWithBalance.js';
|
|
9
|
+
|
|
10
|
+
export async function resolveBalances(
|
|
11
|
+
transactionData: TransactionDataBuilder,
|
|
12
|
+
options: IntentResolverOptions,
|
|
13
|
+
next: () => Promise<void>,
|
|
14
|
+
) {
|
|
15
|
+
const intents = new Set(options.intentNames ?? [ALLOWANCE_BALANCE, COIN_WITH_BALANCE]);
|
|
16
|
+
const shouldResolve = (name: string) =>
|
|
17
|
+
intents.has(name) &&
|
|
18
|
+
!options.supportedIntents?.includes(name) &&
|
|
19
|
+
transactionData.commands.some((command) => command.$Intent?.name === name);
|
|
20
|
+
|
|
21
|
+
// Reserve allowance withdrawals before ordinary coin selection, including when
|
|
22
|
+
// the sender is also the funder. Keep separate intents for wallet negotiation.
|
|
23
|
+
if (shouldResolve(ALLOWANCE_BALANCE)) {
|
|
24
|
+
await resolveAllowanceBalance(transactionData, options, async () => {});
|
|
25
|
+
}
|
|
26
|
+
if (shouldResolve(COIN_WITH_BALANCE)) {
|
|
27
|
+
await resolveCoinBalance(transactionData, options, async () => {});
|
|
28
|
+
}
|
|
29
|
+
await next();
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { TransactionDataBuilder } from './TransactionData.js';
|
|
5
|
+
|
|
6
|
+
export function transactionUsesGasCoin(transactionData: TransactionDataBuilder) {
|
|
7
|
+
let usesGasCoin = false;
|
|
8
|
+
|
|
9
|
+
transactionData.mapArguments((arg) => {
|
|
10
|
+
if (arg.$kind === 'GasCoin') {
|
|
11
|
+
usesGasCoin = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return arg;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return usesGasCoin;
|
|
18
|
+
}
|
|
@@ -9,16 +9,31 @@ import type { BcsType } from '@mysten/bcs';
|
|
|
9
9
|
import { Inputs } from './Inputs.js';
|
|
10
10
|
import { bcs } from '../bcs/index.js';
|
|
11
11
|
import { coreClientResolveTransactionPlugin } from '../client/core-resolver.js';
|
|
12
|
+
import { transactionUsesGasCoin } from './resolution-utils.js';
|
|
12
13
|
|
|
13
14
|
export interface BuildTransactionOptions {
|
|
14
15
|
client?: ClientWithCoreApi;
|
|
15
16
|
onlyTransactionKind?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve `CoinWithBalance` intents from address balance without looking up balances or coins.
|
|
19
|
+
*
|
|
20
|
+
* If nothing else needs resolution, the transaction doesn't use `GasCoin`, and it already has a
|
|
21
|
+
* `ValidDuring` or `Validity` expiration, an unset gas payment is also set to `[]` (pay gas from
|
|
22
|
+
* address balance). Execution fails if the balances aren't there.
|
|
23
|
+
*/
|
|
24
|
+
assumeSufficientAddressBalances?: boolean;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export interface SerializeTransactionOptions extends BuildTransactionOptions {
|
|
19
28
|
supportedIntents?: string[];
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
/** Options supplied when a registered intent resolver runs. */
|
|
32
|
+
export interface IntentResolverOptions extends SerializeTransactionOptions {
|
|
33
|
+
/** Intent names assigned to this resolver for this serialization pass. */
|
|
34
|
+
intentNames?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
export type TransactionPlugin = (
|
|
23
38
|
transactionData: TransactionDataBuilder,
|
|
24
39
|
options: BuildTransactionOptions,
|
|
@@ -38,11 +53,23 @@ export function needsTransactionResolution(
|
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
if (!options.onlyTransactionKind) {
|
|
41
|
-
if (!data.gasData.price || !data.gasData.budget
|
|
56
|
+
if (!data.gasData.price || !data.gasData.budget) {
|
|
42
57
|
return true;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
if (data.gasData.payment
|
|
60
|
+
if (!data.gasData.payment) {
|
|
61
|
+
const assumesAddressBalanceGas =
|
|
62
|
+
options.assumeSufficientAddressBalances && !transactionUsesGasCoin(data);
|
|
63
|
+
|
|
64
|
+
// Address balance gas has no object version to protect against replay, so an offline build
|
|
65
|
+
// needs a ValidDuring expiration. Epoch expiration doesn't count.
|
|
66
|
+
if (
|
|
67
|
+
!assumesAddressBalanceGas ||
|
|
68
|
+
(data.expiration?.$kind !== 'ValidDuring' && data.expiration?.$kind !== 'Validity')
|
|
69
|
+
) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
} else if (data.gasData.payment.length === 0 && !data.expiration) {
|
|
46
73
|
return true;
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -56,7 +83,13 @@ export async function resolveTransactionPlugin(
|
|
|
56
83
|
next: () => Promise<void>,
|
|
57
84
|
) {
|
|
58
85
|
normalizeRawArguments(transactionData);
|
|
86
|
+
|
|
59
87
|
if (!needsTransactionResolution(transactionData, options)) {
|
|
88
|
+
// Payment can only be unset here when assumeSufficientAddressBalances applies
|
|
89
|
+
if (!options.onlyTransactionKind && !transactionData.gasData.payment) {
|
|
90
|
+
transactionData.gasData.payment = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
await validate(transactionData);
|
|
61
94
|
return next();
|
|
62
95
|
}
|
package/src/version.ts
CHANGED