@ember-finance/sdk 2.27.0 → 2.29.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/dist/src/api/v2/models/chain-vault-details.d.ts +6 -0
- package/dist/src/sui-vaults/on-chain-calls/atomic-liquidity.d.ts +80 -0
- package/dist/src/sui-vaults/on-chain-calls/atomic-liquidity.js +318 -7
- package/dist/src/sui-vaults/on-chain-calls/operator.d.ts +2 -1
- package/dist/src/sui-vaults/on-chain-calls/operator.js +24 -1
- package/dist/src/sui-vaults/on-chain-calls/user.js +1 -4
- package/dist/src/sui-vaults/utils/common.d.ts +8 -0
- package/dist/src/sui-vaults/utils/common.js +16 -0
- package/package.json +1 -1
|
@@ -116,6 +116,12 @@ export interface ChainVaultDetails {
|
|
|
116
116
|
* @memberof ChainVaultDetails
|
|
117
117
|
*/
|
|
118
118
|
status: ChainVaultDetailsStatusEnum;
|
|
119
|
+
/**
|
|
120
|
+
* Display order of this chain\'s vault among the parent\'s detailsByChain entries, ascending. Map key order is not stable, so sort by this. Ties are possible; break them by chainIdentifier.
|
|
121
|
+
* @type {number}
|
|
122
|
+
* @memberof ChainVaultDetails
|
|
123
|
+
*/
|
|
124
|
+
rank: number;
|
|
119
125
|
/**
|
|
120
126
|
* True if this on-chain vault gates deposits through a parking lot (async deposits) — i.e. it has an active deposit parking lot. Only set for EVM vaults; always false/absent for Sui. Convenience flag equivalent to `parkingLot != null`.
|
|
121
127
|
* @type {boolean}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { BaseClient as SuiClient } from "@mysten/sui/client";
|
|
2
2
|
import { FeeMultiplierTier, InstantWithdrawQuote } from "../../atomic/instant-withdraw.js";
|
|
3
|
+
/** A queued holdback awaiting release, in collateral base units. */
|
|
4
|
+
export interface SuiHoldbackEntry {
|
|
5
|
+
receiver: string;
|
|
6
|
+
amount: bigint;
|
|
7
|
+
createdAt: bigint;
|
|
8
|
+
sequenceNumber: bigint;
|
|
9
|
+
}
|
|
10
|
+
/** A Suilend reward the vault's strategy position can claim. */
|
|
11
|
+
export interface SuiSuilendReward {
|
|
12
|
+
/** Slot on the reserve's deposit reward manager. */
|
|
13
|
+
rewardIndex: number;
|
|
14
|
+
isDepositReward: boolean;
|
|
15
|
+
rewardCoinType: string;
|
|
16
|
+
/** Coin type of the reserve the reward accrues on: the vault's collateral. */
|
|
17
|
+
rewardSourceType: string;
|
|
18
|
+
/** Claimable now, in reward-coin base units. 0 when the vault has no sub-account. */
|
|
19
|
+
pending: bigint;
|
|
20
|
+
/** The allowlisted sub-account `pending` was priced against; claim to this one. */
|
|
21
|
+
subAccount?: string;
|
|
22
|
+
campaignEndsAtMs: bigint;
|
|
23
|
+
}
|
|
3
24
|
/** Strategy an atomic vault's idle collateral can be supplied to. */
|
|
4
25
|
export type SuiAtomicStrategy = "none" | "suilend";
|
|
5
26
|
/** An atomic vault's strategy and, for Suilend, the market it supplies to. */
|
|
@@ -56,6 +77,65 @@ export declare class SuiAtomicLiquidityReader {
|
|
|
56
77
|
} | null>;
|
|
57
78
|
/** Caller's ALV (non-transferable) share balance from the `shares` table. */
|
|
58
79
|
shareOf(vaultId: string, account: string): Promise<bigint>;
|
|
80
|
+
/**
|
|
81
|
+
* Source-vault shares an atomic vault has collected from swaps and not yet redeemed.
|
|
82
|
+
* @param vaultId Object id of the atomic vault
|
|
83
|
+
* @param sourceVaultId Object id of the source vault
|
|
84
|
+
* @returns The collected share balance, in source-share base units
|
|
85
|
+
*/
|
|
86
|
+
getCollectedEmberShares(vaultId: string, sourceVaultId: string): Promise<bigint>;
|
|
87
|
+
/**
|
|
88
|
+
* What redeeming a source vault's shares depends on.
|
|
89
|
+
* @param sourceVaultId Object id of the source vault
|
|
90
|
+
* @returns Its receipt coin type (`R`), minimum withdrawal shares, and whether
|
|
91
|
+
* withdrawals are paused
|
|
92
|
+
*/
|
|
93
|
+
getEmberShareSource(sourceVaultId: string): Promise<{
|
|
94
|
+
receiptCoinType: string;
|
|
95
|
+
minWithdrawalShares: bigint;
|
|
96
|
+
withdrawalsPaused: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* The holdback queue for one source share, oldest first.
|
|
100
|
+
* @param vaultId Object id of the atomic vault
|
|
101
|
+
* @param sourceVaultId Object id of the source vault
|
|
102
|
+
* @param maxEntries Read at most this many entries from the head
|
|
103
|
+
* @returns The queue length and the entries read
|
|
104
|
+
*/
|
|
105
|
+
getHoldbackQueue(vaultId: string, sourceVaultId: string, maxEntries: number): Promise<{
|
|
106
|
+
length: bigint;
|
|
107
|
+
entries: SuiHoldbackEntry[];
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* Suilend rewards the vault's strategy position has accrued and not yet claimed,
|
|
111
|
+
* one entry per reward campaign the position earns from.
|
|
112
|
+
* @param vaultId Object id of the atomic vault
|
|
113
|
+
* @param packageId The protocol package currently accepted by ProtocolConfig; the
|
|
114
|
+
* claim the amount is simulated through is version-gated
|
|
115
|
+
* @param protocolConfigId Object id of the ProtocolConfig
|
|
116
|
+
* @returns Claimable rewards, with everything a claim needs. Empty when the vault
|
|
117
|
+
* is not on Suilend.
|
|
118
|
+
*/
|
|
119
|
+
getSuilendRewards(vaultId: string, packageId: string, protocolConfigId: string): Promise<SuiSuilendReward[]>;
|
|
120
|
+
private simulateRewardClaim;
|
|
121
|
+
/**
|
|
122
|
+
* Coins sent to the atomic vault's address and not yet taken into it: withdrawal
|
|
123
|
+
* proceeds from source vaults (collateral) and redemptions a source returned
|
|
124
|
+
* (source shares). Each has to be claimed into the vault before it counts.
|
|
125
|
+
* @param vaultId Object id of the atomic vault
|
|
126
|
+
* @returns Non-empty coins, split into collateral and returned shares
|
|
127
|
+
*/
|
|
128
|
+
getReceivedCoins(vaultId: string): Promise<{
|
|
129
|
+
collateral: {
|
|
130
|
+
objectId: string;
|
|
131
|
+
amount: bigint;
|
|
132
|
+
}[];
|
|
133
|
+
returnedShares: {
|
|
134
|
+
objectId: string;
|
|
135
|
+
coinType: string;
|
|
136
|
+
amount: bigint;
|
|
137
|
+
}[];
|
|
138
|
+
}>;
|
|
59
139
|
/**
|
|
60
140
|
* The strategy an atomic vault is on, and for Suilend the market it supplies to.
|
|
61
141
|
* Plain object reads, no simulation.
|
|
@@ -3,6 +3,63 @@ import { Transaction } from "@mysten/sui/transactions";
|
|
|
3
3
|
import { SUI_ZERO_ADDRESS } from "./protocol-config-reader.js";
|
|
4
4
|
import { tableIdOf, unwrapMoveFields } from "../utils/common.js";
|
|
5
5
|
import { computeSwapQuote, computePlainInstantQuote } from "../../atomic/instant-withdraw.js";
|
|
6
|
+
// queue::Queue<T> and atomic_liquidity_vault::HoldbackEntry, in field order.
|
|
7
|
+
const QUEUE_BCS = bcs.struct("Queue", {
|
|
8
|
+
id: bcs.Address,
|
|
9
|
+
table: bcs.struct("Table", { id: bcs.Address, size: bcs.u64() }),
|
|
10
|
+
head: bcs.u64(),
|
|
11
|
+
tail: bcs.u64()
|
|
12
|
+
});
|
|
13
|
+
const HOLDBACK_ENTRY_BCS = bcs.struct("HoldbackEntry", {
|
|
14
|
+
share_vault_id: bcs.Address,
|
|
15
|
+
receiver: bcs.Address,
|
|
16
|
+
amount: bcs.u64(),
|
|
17
|
+
created_at: bcs.u64(),
|
|
18
|
+
sequence_number: bcs.u128()
|
|
19
|
+
});
|
|
20
|
+
/** Top-level type arguments, e.g. `A, B<C, D>` -> [A, B<C, D>]. */
|
|
21
|
+
const splitTypeArgs = (args) => {
|
|
22
|
+
const out = [];
|
|
23
|
+
let depth = 0;
|
|
24
|
+
let start = 0;
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === "<")
|
|
27
|
+
depth++;
|
|
28
|
+
else if (args[i] === ">")
|
|
29
|
+
depth--;
|
|
30
|
+
else if (args[i] === "," && depth === 0) {
|
|
31
|
+
out.push(args.slice(start, i).trim());
|
|
32
|
+
start = i + 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
out.push(args.slice(start).trim());
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
// A missing dynamic field is a legitimate "none" here; any other failure is not.
|
|
39
|
+
const dynamicFieldOrNull = async (suiClient, args) => {
|
|
40
|
+
try {
|
|
41
|
+
const res = await suiClient.core.getDynamicField(args);
|
|
42
|
+
return res.dynamicField;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (/not found/i.test(error?.message ?? ""))
|
|
46
|
+
return null;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const suilendKey = (key) => ({
|
|
51
|
+
type: "vector<u8>",
|
|
52
|
+
bcs: bcs.vector(bcs.u8()).serialize(new TextEncoder().encode(key)).toBytes()
|
|
53
|
+
});
|
|
54
|
+
// Move renders type names without the 0x prefix inside JSON.
|
|
55
|
+
const withHexPrefix = (coinType) => coinType.startsWith("0x") ? coinType : `0x${coinType}`;
|
|
56
|
+
const sameCoinType = (a, b) => {
|
|
57
|
+
const norm = (t) => {
|
|
58
|
+
const [address, ...rest] = withHexPrefix(t).split("::");
|
|
59
|
+
return [`0x${address.slice(2).replace(/^0+/, "")}`, ...rest].join("::");
|
|
60
|
+
};
|
|
61
|
+
return norm(a) === norm(b);
|
|
62
|
+
};
|
|
6
63
|
// Indexed by the Move `strategy_kind` u8.
|
|
7
64
|
const SUI_ATOMIC_STRATEGIES = ["none", "suilend"];
|
|
8
65
|
/** Sui fixed-point scale (1e9) used by the Atomic Liquidity Vault Move math. */
|
|
@@ -113,6 +170,266 @@ export class SuiAtomicLiquidityReader {
|
|
|
113
170
|
return 0n;
|
|
114
171
|
return toBig(await this.readTableValue(tableId, "address", account));
|
|
115
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Source-vault shares an atomic vault has collected from swaps and not yet redeemed.
|
|
175
|
+
* @param vaultId Object id of the atomic vault
|
|
176
|
+
* @param sourceVaultId Object id of the source vault
|
|
177
|
+
* @returns The collected share balance, in source-share base units
|
|
178
|
+
*/
|
|
179
|
+
async getCollectedEmberShares(vaultId, sourceVaultId) {
|
|
180
|
+
const vault = await this.suiClient.core.getObject({ objectId: vaultId });
|
|
181
|
+
// The key type lives in the package that defines the vault type.
|
|
182
|
+
const packageId = vault.object.type.split("::")[0];
|
|
183
|
+
const field = await dynamicFieldOrNull(this.suiClient, {
|
|
184
|
+
parentId: vaultId,
|
|
185
|
+
name: {
|
|
186
|
+
type: `${packageId}::atomic_liquidity_vault::CollectedEmberSharesKey`,
|
|
187
|
+
bcs: bcs.Address.serialize(sourceVaultId).toBytes()
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return field ? BigInt(bcs.u64().parse(field.value.bcs)) : 0n;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* What redeeming a source vault's shares depends on.
|
|
194
|
+
* @param sourceVaultId Object id of the source vault
|
|
195
|
+
* @returns Its receipt coin type (`R`), minimum withdrawal shares, and whether
|
|
196
|
+
* withdrawals are paused
|
|
197
|
+
*/
|
|
198
|
+
async getEmberShareSource(sourceVaultId) {
|
|
199
|
+
const res = await this.suiClient.core.getObject({
|
|
200
|
+
objectId: sourceVaultId,
|
|
201
|
+
include: { json: true }
|
|
202
|
+
});
|
|
203
|
+
const fields = unwrapMoveFields(res.object?.json);
|
|
204
|
+
if (!fields)
|
|
205
|
+
throw new Error(`Source vault ${sourceVaultId} not found`);
|
|
206
|
+
const type = res.object.type;
|
|
207
|
+
const [, receiptCoinType] = splitTypeArgs(type.slice(type.indexOf("<") + 1, -1));
|
|
208
|
+
if (!receiptCoinType)
|
|
209
|
+
throw new Error(`${sourceVaultId} is not a Vault<T, R>: ${type}`);
|
|
210
|
+
// Mirrors vault::is_withdrawals_paused: the legacy global bit, or the
|
|
211
|
+
// per-operation flag kept as a dynamic field.
|
|
212
|
+
let withdrawalsPaused = !!fields.paused;
|
|
213
|
+
if (!withdrawalsPaused) {
|
|
214
|
+
const field = await dynamicFieldOrNull(this.suiClient, {
|
|
215
|
+
parentId: sourceVaultId,
|
|
216
|
+
name: {
|
|
217
|
+
type: "vector<u8>",
|
|
218
|
+
bcs: bcs
|
|
219
|
+
.vector(bcs.u8())
|
|
220
|
+
.serialize(new TextEncoder().encode("paused_withdrawals"))
|
|
221
|
+
.toBytes()
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
withdrawalsPaused = field ? bcs.bool().parse(field.value.bcs) : false;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
receiptCoinType,
|
|
228
|
+
minWithdrawalShares: toBig(fields.min_withdrawal_shares),
|
|
229
|
+
withdrawalsPaused
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The holdback queue for one source share, oldest first.
|
|
234
|
+
* @param vaultId Object id of the atomic vault
|
|
235
|
+
* @param sourceVaultId Object id of the source vault
|
|
236
|
+
* @param maxEntries Read at most this many entries from the head
|
|
237
|
+
* @returns The queue length and the entries read
|
|
238
|
+
*/
|
|
239
|
+
async getHoldbackQueue(vaultId, sourceVaultId, maxEntries) {
|
|
240
|
+
const fields = await this.vaultFields(vaultId);
|
|
241
|
+
const tableId = tableIdOf(fields.holdback_queues);
|
|
242
|
+
if (!tableId)
|
|
243
|
+
return { length: 0n, entries: [] };
|
|
244
|
+
const queueField = await dynamicFieldOrNull(this.suiClient, {
|
|
245
|
+
parentId: tableId,
|
|
246
|
+
name: {
|
|
247
|
+
type: "0x2::object::ID",
|
|
248
|
+
bcs: bcs.Address.serialize(sourceVaultId).toBytes()
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
if (!queueField)
|
|
252
|
+
return { length: 0n, entries: [] };
|
|
253
|
+
const queue = QUEUE_BCS.parse(queueField.value.bcs);
|
|
254
|
+
const head = BigInt(queue.head);
|
|
255
|
+
const length = BigInt(queue.tail) - head;
|
|
256
|
+
const count = length < BigInt(maxEntries) ? Number(length) : maxEntries;
|
|
257
|
+
const entries = await Promise.all(Array.from({ length: count }, async (_, i) => {
|
|
258
|
+
const entry = await this.suiClient.core.getDynamicField({
|
|
259
|
+
parentId: queue.table.id,
|
|
260
|
+
name: {
|
|
261
|
+
type: "u64",
|
|
262
|
+
bcs: bcs
|
|
263
|
+
.u64()
|
|
264
|
+
.serialize(head + BigInt(i))
|
|
265
|
+
.toBytes()
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
const e = HOLDBACK_ENTRY_BCS.parse(entry.dynamicField.value.bcs);
|
|
269
|
+
return {
|
|
270
|
+
receiver: e.receiver,
|
|
271
|
+
amount: BigInt(e.amount),
|
|
272
|
+
createdAt: BigInt(e.created_at),
|
|
273
|
+
sequenceNumber: BigInt(e.sequence_number)
|
|
274
|
+
};
|
|
275
|
+
}));
|
|
276
|
+
return { length, entries };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Suilend rewards the vault's strategy position has accrued and not yet claimed,
|
|
280
|
+
* one entry per reward campaign the position earns from.
|
|
281
|
+
* @param vaultId Object id of the atomic vault
|
|
282
|
+
* @param packageId The protocol package currently accepted by ProtocolConfig; the
|
|
283
|
+
* claim the amount is simulated through is version-gated
|
|
284
|
+
* @param protocolConfigId Object id of the ProtocolConfig
|
|
285
|
+
* @returns Claimable rewards, with everything a claim needs. Empty when the vault
|
|
286
|
+
* is not on Suilend.
|
|
287
|
+
*/
|
|
288
|
+
async getSuilendRewards(vaultId, packageId, protocolConfigId) {
|
|
289
|
+
const fields = await this.vaultFields(vaultId);
|
|
290
|
+
const target = await this.getStrategyTarget(vaultId);
|
|
291
|
+
if (target.strategy !== "suilend")
|
|
292
|
+
return [];
|
|
293
|
+
const obligationField = await dynamicFieldOrNull(this.suiClient, {
|
|
294
|
+
parentId: vaultId,
|
|
295
|
+
name: suilendKey("al_strategy_suilend_obligation_id")
|
|
296
|
+
});
|
|
297
|
+
if (!obligationField)
|
|
298
|
+
return [];
|
|
299
|
+
const obligationId = bcs.Address.parse(obligationField.value.bcs);
|
|
300
|
+
const [obligation, market] = await Promise.all([
|
|
301
|
+
this.suiClient.core.getObject({
|
|
302
|
+
objectId: obligationId,
|
|
303
|
+
include: { json: true }
|
|
304
|
+
}),
|
|
305
|
+
this.suiClient.core.getObject({
|
|
306
|
+
objectId: target.suilendMarketId,
|
|
307
|
+
include: { json: true }
|
|
308
|
+
})
|
|
309
|
+
]);
|
|
310
|
+
const obligationFields = unwrapMoveFields(obligation.object?.json) ?? {};
|
|
311
|
+
const reserves = unwrapMoveFields(market.object?.json)?.reserves ?? [];
|
|
312
|
+
const subAccount = fields.sub_accounts?.[0];
|
|
313
|
+
const rewards = [];
|
|
314
|
+
// Every deposit earns, not only the committed collateral: the permissionless
|
|
315
|
+
// claim_rewards_and_deposit can add foreign collateral to the obligation.
|
|
316
|
+
for (const deposit of obligationFields.deposits ?? []) {
|
|
317
|
+
const manager = obligationFields.user_reward_managers?.[Number(deposit.user_reward_manager_index)];
|
|
318
|
+
const userRewards = manager?.rewards ?? [];
|
|
319
|
+
const lastUpdateMs = BigInt(manager?.last_update_time_ms ?? 0);
|
|
320
|
+
const poolRewards = reserves[Number(deposit.reserve_array_index)]
|
|
321
|
+
?.deposits_pool_reward_manager?.pool_rewards ?? [];
|
|
322
|
+
const rewardSourceType = withHexPrefix(deposit.coin_type);
|
|
323
|
+
for (let index = 0; index < poolRewards.length; index++) {
|
|
324
|
+
const pool = poolRewards[index];
|
|
325
|
+
if (!pool)
|
|
326
|
+
continue;
|
|
327
|
+
// Suilend only materialises a user slot when the obligation is next
|
|
328
|
+
// refreshed, so a campaign that ran after the last refresh has accrued
|
|
329
|
+
// without one. Claiming refreshes first, so the simulation sees it.
|
|
330
|
+
const tracked = !!userRewards[index];
|
|
331
|
+
if (!tracked && BigInt(pool.end_time_ms) <= lastUpdateMs)
|
|
332
|
+
continue;
|
|
333
|
+
const rewardCoinType = withHexPrefix(pool.coin_type);
|
|
334
|
+
const pending = subAccount
|
|
335
|
+
? await this.simulateRewardClaim({
|
|
336
|
+
packageId,
|
|
337
|
+
protocolConfigId,
|
|
338
|
+
vaultId,
|
|
339
|
+
operator: fields.operator,
|
|
340
|
+
subAccount,
|
|
341
|
+
target,
|
|
342
|
+
rewardSourceType,
|
|
343
|
+
rewardCoinType,
|
|
344
|
+
rewardIndex: index
|
|
345
|
+
})
|
|
346
|
+
: 0n;
|
|
347
|
+
if (!tracked && pending === 0n)
|
|
348
|
+
continue;
|
|
349
|
+
rewards.push({
|
|
350
|
+
rewardIndex: index,
|
|
351
|
+
isDepositReward: true,
|
|
352
|
+
rewardCoinType,
|
|
353
|
+
rewardSourceType,
|
|
354
|
+
campaignEndsAtMs: BigInt(pool.end_time_ms),
|
|
355
|
+
...(subAccount ? { subAccount } : {}),
|
|
356
|
+
pending
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return rewards;
|
|
361
|
+
}
|
|
362
|
+
async simulateRewardClaim(args) {
|
|
363
|
+
const tx = new Transaction();
|
|
364
|
+
tx.setSender(args.operator);
|
|
365
|
+
tx.moveCall({
|
|
366
|
+
target: `${args.packageId}::gateway::atomic_claim_suilend_strategy_reward`,
|
|
367
|
+
typeArguments: [
|
|
368
|
+
args.target.collateralType,
|
|
369
|
+
args.target.suilendPoolType,
|
|
370
|
+
args.rewardSourceType,
|
|
371
|
+
args.rewardCoinType
|
|
372
|
+
],
|
|
373
|
+
arguments: [
|
|
374
|
+
tx.object(args.vaultId),
|
|
375
|
+
tx.object(args.protocolConfigId),
|
|
376
|
+
tx.object(args.target.suilendMarketId),
|
|
377
|
+
tx.pure.address(args.subAccount),
|
|
378
|
+
tx.pure.u64(args.rewardIndex),
|
|
379
|
+
tx.pure.bool(true),
|
|
380
|
+
tx.object.clock()
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
const sim = await this.suiClient.core.simulateTransaction({
|
|
384
|
+
transaction: tx,
|
|
385
|
+
checksEnabled: false,
|
|
386
|
+
include: { events: true }
|
|
387
|
+
});
|
|
388
|
+
if (sim.$kind !== "Transaction") {
|
|
389
|
+
throw new Error(`Simulating the Suilend reward claim for slot ${args.rewardIndex} failed: ${sim.FailedTransaction?.status?.error?.message ?? "unknown"}`);
|
|
390
|
+
}
|
|
391
|
+
const event = (sim.Transaction.events ?? []).find(e => e.eventType.includes("::events::StrategyRewardCollectedEvent"));
|
|
392
|
+
return toBig(event?.json?.amount);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Coins sent to the atomic vault's address and not yet taken into it: withdrawal
|
|
396
|
+
* proceeds from source vaults (collateral) and redemptions a source returned
|
|
397
|
+
* (source shares). Each has to be claimed into the vault before it counts.
|
|
398
|
+
* @param vaultId Object id of the atomic vault
|
|
399
|
+
* @returns Non-empty coins, split into collateral and returned shares
|
|
400
|
+
*/
|
|
401
|
+
async getReceivedCoins(vaultId) {
|
|
402
|
+
const { collateralType } = await this.getStrategyTarget(vaultId);
|
|
403
|
+
const collateral = [];
|
|
404
|
+
const returnedShares = [];
|
|
405
|
+
let cursor = null;
|
|
406
|
+
do {
|
|
407
|
+
const page = await this.suiClient.core.listOwnedObjects({
|
|
408
|
+
owner: vaultId,
|
|
409
|
+
cursor,
|
|
410
|
+
include: { json: true }
|
|
411
|
+
});
|
|
412
|
+
for (const object of page.objects ?? []) {
|
|
413
|
+
const type = object.type ?? "";
|
|
414
|
+
if (!type.startsWith("0x2::coin::Coin<") &&
|
|
415
|
+
!/^0x0*2::coin::Coin</.test(type)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const amount = toBig(unwrapMoveFields(object.json)?.balance);
|
|
419
|
+
if (amount === 0n)
|
|
420
|
+
continue;
|
|
421
|
+
const coinType = type.slice(type.indexOf("<") + 1, -1);
|
|
422
|
+
if (sameCoinType(coinType, collateralType)) {
|
|
423
|
+
collateral.push({ objectId: object.objectId, amount });
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
returnedShares.push({ objectId: object.objectId, coinType, amount });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
cursor = page.hasNextPage ? page.cursor : null;
|
|
430
|
+
} while (cursor);
|
|
431
|
+
return { collateral, returnedShares };
|
|
432
|
+
}
|
|
116
433
|
/**
|
|
117
434
|
* The strategy an atomic vault is on, and for Suilend the market it supplies to.
|
|
118
435
|
* Plain object reads, no simulation.
|
|
@@ -147,13 +464,7 @@ export class SuiAtomicLiquidityReader {
|
|
|
147
464
|
}
|
|
148
465
|
const marketField = await this.suiClient.core.getDynamicField({
|
|
149
466
|
parentId: vaultId,
|
|
150
|
-
name:
|
|
151
|
-
type: "vector<u8>",
|
|
152
|
-
bcs: bcs
|
|
153
|
-
.vector(bcs.u8())
|
|
154
|
-
.serialize(new TextEncoder().encode("al_strategy_suilend_market_id"))
|
|
155
|
-
.toBytes()
|
|
156
|
-
}
|
|
467
|
+
name: suilendKey("al_strategy_suilend_market_id")
|
|
157
468
|
});
|
|
158
469
|
const suilendMarketId = bcs.Address.parse(marketField.dynamicField.value.bcs);
|
|
159
470
|
const market = await this.suiClient.core.getObject({ objectId: suilendMarketId });
|
|
@@ -255,7 +255,8 @@ export declare class OperatorCalls extends OnChainCalls {
|
|
|
255
255
|
*/
|
|
256
256
|
atomicRedeemCollectedEmberShares(vaultId: string, sourceReceiptType: string, sourceVaultId: string, amount: NumStr, options?: IVaultOptionalParams & SuiTransactionBlockResponseOptions): Promise<OnChainCallResponse>;
|
|
257
257
|
/**
|
|
258
|
-
* Release users' holdback for an ember share source vault
|
|
258
|
+
* Release users' holdback for an ember share source vault. On a strategy vault,
|
|
259
|
+
* pulls any shortfall from the strategy first, in the same transaction.
|
|
259
260
|
* @param vaultId The id of the atomic vault
|
|
260
261
|
* @param shareVaultId Id of the source ember share vault
|
|
261
262
|
* @param unwindingPercentage The unwinding percentage
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { OnChainCalls } from "./onchain-calls.js";
|
|
2
2
|
import { TransactionBlock } from "@firefly-exchange/library-sui";
|
|
3
3
|
import { CoinUtils } from "@firefly-exchange/library-sui";
|
|
4
|
+
import { SuiAtomicLiquidityReader } from "./atomic-liquidity.js";
|
|
5
|
+
import { holdbackPayout, isSuiCoinType } from "../utils/common.js";
|
|
4
6
|
import { SuiProtocolConfigReader, usesCollectPlatformFeeV2 } from "./protocol-config-reader.js";
|
|
5
7
|
export class OperatorCalls extends OnChainCalls {
|
|
6
8
|
constructor(_network, _suiClient, _deployment, _signer, _walletAddress) {
|
|
@@ -418,7 +420,8 @@ export class OperatorCalls extends OnChainCalls {
|
|
|
418
420
|
return this.execCall(txb, options);
|
|
419
421
|
}
|
|
420
422
|
/**
|
|
421
|
-
* Release users' holdback for an ember share source vault
|
|
423
|
+
* Release users' holdback for an ember share source vault. On a strategy vault,
|
|
424
|
+
* pulls any shortfall from the strategy first, in the same transaction.
|
|
422
425
|
* @param vaultId The id of the atomic vault
|
|
423
426
|
* @param shareVaultId Id of the source ember share vault
|
|
424
427
|
* @param unwindingPercentage The unwinding percentage
|
|
@@ -428,6 +431,26 @@ export class OperatorCalls extends OnChainCalls {
|
|
|
428
431
|
*/
|
|
429
432
|
async atomicReleaseUsersHoldback(vaultId, shareVaultId, unwindingPercentage, maxEntries, options) {
|
|
430
433
|
const txb = options?.txBlock || new TransactionBlock();
|
|
434
|
+
// Unlike EVM's releaseUsersHoldback, the Sui release pays only from idle
|
|
435
|
+
// balance and aborts when it is short, so the strategy has to be drawn on
|
|
436
|
+
// first — sized to what the release will actually pay, since withdrawing more
|
|
437
|
+
// than the strategy holds aborts the whole transaction.
|
|
438
|
+
const pct = BigInt(unwindingPercentage);
|
|
439
|
+
if (pct > 0n) {
|
|
440
|
+
const reader = new SuiAtomicLiquidityReader(this.suiClient);
|
|
441
|
+
const target = await reader.getStrategyTarget(vaultId);
|
|
442
|
+
if (target.strategy === "suilend") {
|
|
443
|
+
const { entries } = await reader.getHoldbackQueue(vaultId, shareVaultId, Number(maxEntries));
|
|
444
|
+
const payout = entries.reduce((sum, e) => sum + holdbackPayout(e.amount, pct), 0n);
|
|
445
|
+
const shortfall = payout - target.idleCollateral;
|
|
446
|
+
if (shortfall > 0n) {
|
|
447
|
+
const withdraw = isSuiCoinType(target.collateralType)
|
|
448
|
+
? this.txBuilder.atomicWithdrawFromSuilendStrategySui.bind(this.txBuilder)
|
|
449
|
+
: this.txBuilder.atomicWithdrawFromSuilendStrategy.bind(this.txBuilder);
|
|
450
|
+
withdraw(vaultId, target.suilendPoolType, target.suilendMarketId, shortfall.toString(), { ...options, txBlock: txb });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
431
454
|
this.txBuilder.atomicReleaseUsersHoldback(vaultId, shareVaultId, unwindingPercentage, maxEntries, { ...options, txBlock: txb });
|
|
432
455
|
return this.execCall(txb, options);
|
|
433
456
|
}
|
|
@@ -2,10 +2,7 @@ import { OnChainCalls } from "./onchain-calls.js";
|
|
|
2
2
|
import { TransactionBlock, bigNumber, CoinUtils, SUI_NATIVE_BASE } from "@firefly-exchange/library-sui";
|
|
3
3
|
import { Vault } from "./vault.js";
|
|
4
4
|
import { SuiAtomicLiquidityReader } from "./atomic-liquidity.js";
|
|
5
|
-
|
|
6
|
-
const [address, module, name] = coinType.split("::");
|
|
7
|
-
return module === "sui" && name === "SUI" && /^0x0*2$/.test(address);
|
|
8
|
-
};
|
|
5
|
+
import { isSuiCoinType } from "../utils/common.js";
|
|
9
6
|
export class UserCalls extends OnChainCalls {
|
|
10
7
|
constructor(_network, _suiClient, _deployment, _signer, _walletAddress, _isUIWallet) {
|
|
11
8
|
super(_network, _suiClient, _deployment, _signer, _walletAddress, _isUIWallet);
|
|
@@ -21,3 +21,11 @@ export declare const unwrapMoveFields: (value: any) => any;
|
|
|
21
21
|
* Silent, and wrong in the direction that looks like ordinary empty state.
|
|
22
22
|
*/
|
|
23
23
|
export declare const tableIdOf: (field: any) => string | undefined;
|
|
24
|
+
/** True for the SUI coin type, in its short (`0x2`) or padded form. */
|
|
25
|
+
export declare const isSuiCoinType: (coinType: string) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* What `release_users_holdback` pays one holdback entry at `unwindingPercentage`
|
|
28
|
+
* (1e9 = 100%): floor(amount * pct / 1e9), except that an entry of at most 100 base
|
|
29
|
+
* units that rounds to zero is paid in full. Zero unwinding pays nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare const holdbackPayout: (amount: bigint, unwindingPercentage: bigint) => bigint;
|
|
@@ -26,3 +26,19 @@ export const tableIdOf = (field) => {
|
|
|
26
26
|
const id = unwrapMoveFields(table?.id);
|
|
27
27
|
return (typeof id === "string" ? id : id?.id) ?? undefined;
|
|
28
28
|
};
|
|
29
|
+
/** True for the SUI coin type, in its short (`0x2`) or padded form. */
|
|
30
|
+
export const isSuiCoinType = (coinType) => {
|
|
31
|
+
const [address, module, name] = coinType.split("::");
|
|
32
|
+
return module === "sui" && name === "SUI" && /^0x0*2$/.test(address);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* What `release_users_holdback` pays one holdback entry at `unwindingPercentage`
|
|
36
|
+
* (1e9 = 100%): floor(amount * pct / 1e9), except that an entry of at most 100 base
|
|
37
|
+
* units that rounds to zero is paid in full. Zero unwinding pays nothing.
|
|
38
|
+
*/
|
|
39
|
+
export const holdbackPayout = (amount, unwindingPercentage) => {
|
|
40
|
+
if (unwindingPercentage === 0n)
|
|
41
|
+
return 0n;
|
|
42
|
+
const payout = (amount * unwindingPercentage) / 1000000000n;
|
|
43
|
+
return payout === 0n && amount > 0n && amount <= 100n ? amount : payout;
|
|
44
|
+
};
|