@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.
@@ -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
- const isSuiCoinType = (coinType) => {
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
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ember-finance/sdk",
3
3
  "description": "Ember Protocol SDK",
4
- "version": "2.27.0",
4
+ "version": "2.29.0",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",