@mysten/sui 2.30.0 → 2.31.1
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 +5 -3
- 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/graphql/generated/tada-env.d.mts +74 -0
- package/dist/grpc/proto/sui/forking/v1alpha/forking_service.client.d.mts +4 -4
- 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/name_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/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 +47 -42
- 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 +31 -10
- 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/resolve.d.mts.map +1 -1
- package/dist/transactions/resolve.mjs +1 -1
- 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/transactions/allowances.md +176 -0
- package/docs/transactions/coins-and-balances.md +12 -9
- package/package.json +1 -1
- package/src/bcs/bcs.ts +4 -0
- package/src/bcs/types.ts +2 -1
- package/src/client/core-resolver.ts +12 -6
- 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 +70 -49
- 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 +51 -8
- package/src/transactions/intents/ResolveBalances.ts +30 -0
- package/src/transactions/resolve.ts +6 -0
- package/src/version.ts +1 -1
|
@@ -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) {
|
|
@@ -199,6 +221,7 @@ export async function resolveCoinBalance(
|
|
|
199
221
|
client: client!,
|
|
200
222
|
owner: transactionData.sender!,
|
|
201
223
|
usedIds,
|
|
224
|
+
reserved: reservedByType.get(coinType) ?? 0n,
|
|
202
225
|
});
|
|
203
226
|
|
|
204
227
|
coinsByType.set(coinType, coins);
|
|
@@ -211,7 +234,10 @@ export async function resolveCoinBalance(
|
|
|
211
234
|
coinType: SUI_TYPE,
|
|
212
235
|
})
|
|
213
236
|
.then(({ balance }) => {
|
|
214
|
-
addressBalanceByType.set(
|
|
237
|
+
addressBalanceByType.set(
|
|
238
|
+
'gas',
|
|
239
|
+
availableAddressBalance(balance.addressBalance, reservedByType.get(SUI_TYPE) ?? 0n),
|
|
240
|
+
);
|
|
215
241
|
})
|
|
216
242
|
: null,
|
|
217
243
|
]);
|
|
@@ -462,12 +488,14 @@ async function getCoinsAndBalanceOfType({
|
|
|
462
488
|
client,
|
|
463
489
|
owner,
|
|
464
490
|
usedIds,
|
|
491
|
+
reserved,
|
|
465
492
|
}: {
|
|
466
493
|
coinType: string;
|
|
467
494
|
balance: bigint;
|
|
468
495
|
client: ClientWithCoreApi;
|
|
469
496
|
owner: string;
|
|
470
497
|
usedIds: Set<string>;
|
|
498
|
+
reserved: bigint;
|
|
471
499
|
}): Promise<{
|
|
472
500
|
coins: SuiClientTypes.Coin[];
|
|
473
501
|
balance: bigint;
|
|
@@ -477,9 +505,14 @@ async function getCoinsAndBalanceOfType({
|
|
|
477
505
|
let remainingBalance = balance;
|
|
478
506
|
const coins: SuiClientTypes.Coin[] = [];
|
|
479
507
|
const balanceRequest = client.core.getBalance({ owner, coinType }).then(({ balance }) => {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
};
|
|
483
516
|
});
|
|
484
517
|
|
|
485
518
|
const [allCoins, balanceResponse] = await Promise.all([loadMoreCoins(), balanceRequest]);
|
|
@@ -531,3 +564,13 @@ async function getCoinsAndBalanceOfType({
|
|
|
531
564
|
return coins;
|
|
532
565
|
}
|
|
533
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
|
+
}
|
|
@@ -28,6 +28,12 @@ export interface SerializeTransactionOptions extends BuildTransactionOptions {
|
|
|
28
28
|
supportedIntents?: string[];
|
|
29
29
|
}
|
|
30
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
|
+
|
|
31
37
|
export type TransactionPlugin = (
|
|
32
38
|
transactionData: TransactionDataBuilder,
|
|
33
39
|
options: BuildTransactionOptions,
|
package/src/version.ts
CHANGED