@upshiftfinance/sdk 0.4.0 → 0.5.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/README.md +5 -1
- package/esm/index.d.mts +15 -0
- package/esm/index.mjs +18 -0
- package/lib/adapters/sui/actions.d.ts +260 -14
- package/lib/adapters/sui/actions.js +332 -13
- package/lib/adapters/sui/client.d.ts +3 -3
- package/lib/adapters/sui/client.js +27 -9
- package/lib/adapters/sui/getters.d.ts +104 -2
- package/lib/adapters/sui/getters.js +283 -5
- package/lib/adapters/sui/index.d.ts +154 -7
- package/lib/adapters/sui/index.js +166 -9
- package/lib/adapters/sui/targets.d.ts +7 -1
- package/lib/adapters/sui/targets.js +7 -1
- package/lib/core/analytics/method-taxonomy.js +2 -0
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/errors/index.d.ts +15 -2
- package/lib/evm/methods/crossChainVault.js +25 -43
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/main.d.ts +4 -2
- package/lib/modules/vaults/getters.js +34 -6
- package/lib/modules/vaults/main.d.ts +4 -2
- package/lib/modules/vaults/main.js +3 -1
- package/lib/sdk.d.ts +16359 -15349
- package/package.json +21 -2
|
@@ -33,9 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_SUI_SLIPPAGE_BPS = void 0;
|
|
37
|
+
exports.suiMinOut = suiMinOut;
|
|
36
38
|
exports.suiExecutionErrorToSdkError = suiExecutionErrorToSdkError;
|
|
37
39
|
exports.buildSuiDepositTransaction = buildSuiDepositTransaction;
|
|
38
40
|
exports.buildSuiRedeemTransaction = buildSuiRedeemTransaction;
|
|
41
|
+
exports.buildSuiRequestRedeemTransaction = buildSuiRequestRedeemTransaction;
|
|
42
|
+
exports.buildSuiCancelRedeemRequestTransaction = buildSuiCancelRedeemRequestTransaction;
|
|
43
|
+
exports.buildSuiClaimWithdrawalTransaction = buildSuiClaimWithdrawalTransaction;
|
|
39
44
|
exports.submitSuiTransaction = submitSuiTransaction;
|
|
40
45
|
const errors_1 = require("../../core/errors");
|
|
41
46
|
const chain_address_1 = require("../../core/helpers/chain-address");
|
|
@@ -45,6 +50,31 @@ const constants_1 = require("./constants");
|
|
|
45
50
|
const getters_1 = require("./getters");
|
|
46
51
|
const targets_1 = require("./targets");
|
|
47
52
|
const U64_MAX = 2n ** 64n - 1n;
|
|
53
|
+
/**
|
|
54
|
+
* Default tolerance below the previewed output: 10 basis points (0.1%).
|
|
55
|
+
*
|
|
56
|
+
* Between the dry run and execution the price moves only by rounding (a unit
|
|
57
|
+
* as other deposits and redemptions land) and by management fee accruing for
|
|
58
|
+
* a few seconds; 0.1% absorbs both. An operator AUM report can move it by up
|
|
59
|
+
* to the vault's report bound (percent-level), which this still rejects with
|
|
60
|
+
* `slippage` rather than fill at a price the user did not see.
|
|
61
|
+
*/
|
|
62
|
+
exports.DEFAULT_SUI_SLIPPAGE_BPS = 10;
|
|
63
|
+
/**
|
|
64
|
+
* The minimum output for a previewed amount: `expected` less `slippageBps`,
|
|
65
|
+
* rounded down.
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
function suiMinOut(expected, slippageBps) {
|
|
69
|
+
return (expected * BigInt(10_000 - slippageBps)) / 10000n;
|
|
70
|
+
}
|
|
71
|
+
function assertSlippage(bps, method) {
|
|
72
|
+
if (bps === undefined)
|
|
73
|
+
return;
|
|
74
|
+
if (!Number.isInteger(bps) || bps < 0 || bps > 10_000) {
|
|
75
|
+
throw new errors_1.UpshiftValidationError('INVALID_INPUT', `${method}: slippageBps must be an integer from 0 to 10000.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
48
78
|
/** Contract errors with a stable SDK reason, keyed `module::constant`. */
|
|
49
79
|
const ABORT_REASONS = {
|
|
50
80
|
'vault::EPaused': 'paused',
|
|
@@ -58,16 +88,37 @@ const ABORT_REASONS = {
|
|
|
58
88
|
'vault::EZeroShares': 'zero_output',
|
|
59
89
|
'vault::EZeroAssets': 'zero_output',
|
|
60
90
|
'versioning::EVersionRetired': 'version_retired',
|
|
91
|
+
'vault::EQueueNotAttached': 'queue_not_attached',
|
|
92
|
+
'vault::EInvalidPayout': 'invalid_payout',
|
|
93
|
+
'vault::ENotRequestOwner': 'not_request_owner',
|
|
94
|
+
'vault::ENotFinalizer': 'not_finalizer',
|
|
95
|
+
'vault::ENotEligibleYet': 'not_eligible_yet',
|
|
96
|
+
'vault::ERequestExpired': 'request_expired',
|
|
97
|
+
// The request ID was used before: its derived object ID is already claimed.
|
|
98
|
+
'derived_object::EObjectAlreadyExists': 'request_id_in_use',
|
|
61
99
|
};
|
|
62
100
|
/**
|
|
63
101
|
* Map a failed Sui execution to an SDK error: a contract abort becomes an
|
|
64
102
|
* {@link UpshiftVaultAbortError} with a stable reason, anything else an
|
|
65
103
|
* {@link UpshiftSDKError} (`UNKNOWN`).
|
|
66
104
|
*
|
|
105
|
+
* The SDK applies this itself to its dry runs and to `executeTransaction`.
|
|
106
|
+
* Use it when the wallet builds, signs or executes a transaction and fails,
|
|
107
|
+
* e.g. because the vault changed after the SDK built it.
|
|
108
|
+
*
|
|
67
109
|
* @param executionError - `effects.status.error` of a failed transaction, or
|
|
68
110
|
* the `executionError` of a `SimulationError`.
|
|
69
111
|
* @param context - Structured context for the error (e.g. network, digest).
|
|
70
|
-
* @
|
|
112
|
+
* @returns The typed error; throw it or branch on its `reason`.
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* try {
|
|
116
|
+
* await dAppKit.signTransaction({ transaction });
|
|
117
|
+
* } catch (err) {
|
|
118
|
+
* if (err?.executionError) throw Sui.actions.suiExecutionErrorToSdkError(err.executionError);
|
|
119
|
+
* throw err;
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
71
122
|
*/
|
|
72
123
|
function suiExecutionErrorToSdkError(executionError, context = {}) {
|
|
73
124
|
const abort = executionError?.MoveAbort;
|
|
@@ -146,7 +197,11 @@ async function dryRun(config, build, method, onAbort) {
|
|
|
146
197
|
*
|
|
147
198
|
* RPC cost: one dry run (plus the coin lookup it needs).
|
|
148
199
|
*
|
|
149
|
-
*
|
|
200
|
+
* The minimum is the dry run's preview less `slippageBps` (default 0.1%),
|
|
201
|
+
* unless `minSharesOut` is given.
|
|
202
|
+
*
|
|
203
|
+
* @param params - Vault, amount, sender, and optional `minSharesOut` or
|
|
204
|
+
* `slippageBps`.
|
|
150
205
|
* @param sui - The SDK's `sui` config; applied only for `params.vault.network`.
|
|
151
206
|
* @returns The unsigned transaction, the shares the dry run minted and the
|
|
152
207
|
* minimum it enforces.
|
|
@@ -163,18 +218,23 @@ async function dryRun(config, build, method, onAbort) {
|
|
|
163
218
|
* { vault: ref, amount: 1_000_000n, sender: account.address },
|
|
164
219
|
* sdkSuiConfig,
|
|
165
220
|
* );
|
|
166
|
-
* const
|
|
221
|
+
* const digest = await submitSuiTransaction(
|
|
222
|
+
* await dAppKit.signTransaction({ transaction }),
|
|
223
|
+
* 'testnet',
|
|
224
|
+
* sdkSuiConfig,
|
|
225
|
+
* );
|
|
167
226
|
* ```
|
|
168
227
|
*/
|
|
169
228
|
async function buildSuiDepositTransaction(params, sui) {
|
|
170
229
|
const method = 'buildSuiDepositTransaction';
|
|
171
|
-
const { vault: ref, amount, sender, minSharesOut } = params;
|
|
230
|
+
const { vault: ref, amount, sender, minSharesOut, slippageBps } = params;
|
|
172
231
|
assertSender(sender, method);
|
|
173
232
|
if (!(0, chain_address_1.isSuiAddress)(ref?.vaultId)) {
|
|
174
233
|
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: vault.vaultId must be a Sui object ID.`);
|
|
175
234
|
}
|
|
176
235
|
assertAmount(amount, method, 'amount', true);
|
|
177
236
|
assertAmount(minSharesOut, method, 'minSharesOut', false);
|
|
237
|
+
assertSlippage(slippageBps, method);
|
|
178
238
|
const config = (0, getters_1.suiConfigForNetwork)(ref.network, sui);
|
|
179
239
|
const contracts = (0, targets_1.resolveSuiVaultContracts)(config);
|
|
180
240
|
const { Transaction: SuiTransaction, coinWithBalance } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
@@ -193,7 +253,8 @@ async function buildSuiDepositTransaction(params, sui) {
|
|
|
193
253
|
// The preview runs after the fee charge, so it matches the deposit exactly.
|
|
194
254
|
const results = await dryRun(config, async () => compose(0n, true), method);
|
|
195
255
|
const expectedShares = (0, getters_1.decodeSuiUint)(results[1][0], 8);
|
|
196
|
-
const min = minSharesOut ??
|
|
256
|
+
const min = minSharesOut ??
|
|
257
|
+
suiMinOut(expectedShares, slippageBps ?? exports.DEFAULT_SUI_SLIPPAGE_BPS);
|
|
197
258
|
return {
|
|
198
259
|
transaction: compose(min, false),
|
|
199
260
|
expectedShares,
|
|
@@ -201,8 +262,9 @@ async function buildSuiDepositTransaction(params, sui) {
|
|
|
201
262
|
};
|
|
202
263
|
}
|
|
203
264
|
/**
|
|
204
|
-
* Build an unsigned instant redemption:
|
|
205
|
-
* asset amount and send the assets to the sender —
|
|
265
|
+
* Build an unsigned instant redemption: charge the management fee, redeem
|
|
266
|
+
* shares with a minimum net asset amount and send the assets to the sender —
|
|
267
|
+
* one transaction.
|
|
206
268
|
*
|
|
207
269
|
* The shares are `coinWithBalance(Share)`. A vault with a withdrawal queue
|
|
208
270
|
* attached has instant redemption off; that fails here with `queue_attached`.
|
|
@@ -213,7 +275,11 @@ async function buildSuiDepositTransaction(params, sui) {
|
|
|
213
275
|
*
|
|
214
276
|
* RPC cost: one dry run (plus the coin lookup it needs).
|
|
215
277
|
*
|
|
216
|
-
*
|
|
278
|
+
* The minimum is the dry run's net preview less `slippageBps` (default 0.1%),
|
|
279
|
+
* unless `minAssetsOut` is given.
|
|
280
|
+
*
|
|
281
|
+
* @param params - Vault, shares, sender, and optional `minAssetsOut` or
|
|
282
|
+
* `slippageBps`.
|
|
217
283
|
* @param sui - The SDK's `sui` config; applied only for `params.vault.network`.
|
|
218
284
|
* @returns The unsigned transaction, the dry run's gross and net assets and
|
|
219
285
|
* the minimum it enforces.
|
|
@@ -227,13 +293,14 @@ async function buildSuiDepositTransaction(params, sui) {
|
|
|
227
293
|
*/
|
|
228
294
|
async function buildSuiRedeemTransaction(params, sui) {
|
|
229
295
|
const method = 'buildSuiRedeemTransaction';
|
|
230
|
-
const { vault: ref, shares, sender, minAssetsOut } = params;
|
|
296
|
+
const { vault: ref, shares, sender, minAssetsOut, slippageBps } = params;
|
|
231
297
|
assertSender(sender, method);
|
|
232
298
|
if (!(0, chain_address_1.isSuiAddress)(ref?.vaultId)) {
|
|
233
299
|
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: vault.vaultId must be a Sui object ID.`);
|
|
234
300
|
}
|
|
235
301
|
assertAmount(shares, method, 'shares', true);
|
|
236
302
|
assertAmount(minAssetsOut, method, 'minAssetsOut', false);
|
|
303
|
+
assertSlippage(slippageBps, method);
|
|
237
304
|
const config = (0, getters_1.suiConfigForNetwork)(ref.network, sui);
|
|
238
305
|
const contracts = (0, targets_1.resolveSuiVaultContracts)(config);
|
|
239
306
|
const { Transaction: SuiTransaction, coinWithBalance } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
@@ -241,6 +308,9 @@ async function buildSuiRedeemTransaction(params, sui) {
|
|
|
241
308
|
const tx = new SuiTransaction();
|
|
242
309
|
tx.setSender(sender);
|
|
243
310
|
const call = vaultCalls(tx, ref, contracts);
|
|
311
|
+
// As on deposit: charge the accrued management fee first, so the redeemer
|
|
312
|
+
// pays their share of it instead of leaving it to the remaining holders.
|
|
313
|
+
call('chargeManagementFee', [tx.object(constants_1.SUI_CLOCK_ID)]);
|
|
244
314
|
if (withPreview) {
|
|
245
315
|
call('previewRedemption', [tx.pure.u64(shares), tx.pure.bool(true)]);
|
|
246
316
|
}
|
|
@@ -258,9 +328,11 @@ async function buildSuiRedeemTransaction(params, sui) {
|
|
|
258
328
|
sharesRaw: shares.toString(),
|
|
259
329
|
error: err.message,
|
|
260
330
|
}));
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
331
|
+
// results[0] is the fee charge; the preview runs after it.
|
|
332
|
+
const expectedGrossAssets = (0, getters_1.decodeSuiUint)(results[1][0], 8);
|
|
333
|
+
const expectedAssets = (0, getters_1.decodeSuiUint)(results[1][1], 8);
|
|
334
|
+
const min = minAssetsOut ??
|
|
335
|
+
suiMinOut(expectedAssets, slippageBps ?? exports.DEFAULT_SUI_SLIPPAGE_BPS);
|
|
264
336
|
return {
|
|
265
337
|
transaction: compose(min, false),
|
|
266
338
|
expectedGrossAssets,
|
|
@@ -268,6 +340,247 @@ async function buildSuiRedeemTransaction(params, sui) {
|
|
|
268
340
|
minAssetsOut: min,
|
|
269
341
|
};
|
|
270
342
|
}
|
|
343
|
+
function assertVaultId(ref, method) {
|
|
344
|
+
if (!(0, chain_address_1.isSuiAddress)(ref?.vaultId)) {
|
|
345
|
+
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: vault.vaultId must be a Sui object ID.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function assertAddress(value, method, name) {
|
|
349
|
+
if (value !== undefined && value !== null && !(0, chain_address_1.isSuiAddress)(value)) {
|
|
350
|
+
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: ${name} must be a Sui address (0x + 64 hex chars).`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build an unsigned withdrawal request for a vault with a withdrawal queue:
|
|
355
|
+
* escrow the shares in a new request object and share it — one transaction.
|
|
356
|
+
*
|
|
357
|
+
* The request is priced when it is claimed, not now. Once the vault's cooldown
|
|
358
|
+
* has run, claim it with {@link buildSuiClaimWithdrawalTransaction}; it can be
|
|
359
|
+
* cancelled at any time with {@link buildSuiCancelRedeemRequestTransaction}.
|
|
360
|
+
*
|
|
361
|
+
* The request ID is the lowest the wallet has never used on this vault
|
|
362
|
+
* (`0, 1, 2, …`), which is how `getSuiWithdrawalRequests` finds requests
|
|
363
|
+
* without an event index. Two requests built before either lands get the same
|
|
364
|
+
* ID; the second then fails (the node refuses its stale coins, or it aborts
|
|
365
|
+
* with `request_id_in_use`) — build it again.
|
|
366
|
+
*
|
|
367
|
+
* The finalizer defaults to the owner: only the owner can claim during the
|
|
368
|
+
* first day the request is claimable, then anyone can (e.g. a keeper).
|
|
369
|
+
*
|
|
370
|
+
* RPC cost: one object read for the next ID (more after 23 earlier requests),
|
|
371
|
+
* then one dry run (plus the coin lookup it needs).
|
|
372
|
+
*
|
|
373
|
+
* @param params - Vault, shares, sender, and optional `payout` and `finalizer`.
|
|
374
|
+
* @param sui - The SDK's `sui` config; applied only for `params.vault.network`.
|
|
375
|
+
* @returns The unsigned transaction, the request ID and the request's object ID.
|
|
376
|
+
* @throws {@link UpshiftValidationError} for a malformed address, an
|
|
377
|
+
* out-of-range amount, unknown contract IDs, or a transaction that cannot be
|
|
378
|
+
* built (e.g. the sender lacks the shares).
|
|
379
|
+
* @throws {@link UpshiftVaultAbortError} when the vault would reject it
|
|
380
|
+
* (`queue_not_attached`, `paused`, `above_max_withdrawal`, `invalid_payout`,
|
|
381
|
+
* `request_id_in_use`, `version_retired`), or the shares would pay nothing
|
|
382
|
+
* (`zero_output`).
|
|
383
|
+
* @throws {@link UpshiftNetworkError} when every Sui endpoint fails.
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* const { transaction, request } = await buildSuiRequestRedeemTransaction(
|
|
387
|
+
* { vault: ref, shares: 1_000_000n, sender: account.address },
|
|
388
|
+
* sdkSuiConfig,
|
|
389
|
+
* );
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
async function buildSuiRequestRedeemTransaction(params, sui) {
|
|
393
|
+
const method = 'buildSuiRequestRedeemTransaction';
|
|
394
|
+
const { vault: ref, shares, sender, payout, finalizer } = params;
|
|
395
|
+
assertSender(sender, method);
|
|
396
|
+
assertVaultId(ref, method);
|
|
397
|
+
assertAmount(shares, method, 'shares', true);
|
|
398
|
+
assertAddress(payout, method, 'payout');
|
|
399
|
+
assertAddress(finalizer, method, 'finalizer');
|
|
400
|
+
const config = (0, getters_1.suiConfigForNetwork)(ref.network, sui);
|
|
401
|
+
const contracts = (0, targets_1.resolveSuiVaultContracts)(config);
|
|
402
|
+
const { Transaction: SuiTransaction, coinWithBalance } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
403
|
+
const requestId = await (0, getters_1.nextSuiWithdrawalRequestId)(ref, sender, sui);
|
|
404
|
+
const compose = (withPreview) => {
|
|
405
|
+
const tx = new SuiTransaction();
|
|
406
|
+
tx.setSender(sender);
|
|
407
|
+
const call = vaultCalls(tx, ref, contracts);
|
|
408
|
+
// Priced on a current NAV (fee charged first, in the dry run only).
|
|
409
|
+
// Queued withdrawals pay no instant fee, so gross is what a claim pays.
|
|
410
|
+
if (withPreview) {
|
|
411
|
+
call('chargeManagementFee', [tx.object(constants_1.SUI_CLOCK_ID)]);
|
|
412
|
+
call('previewRedemption', [tx.pure.u64(shares), tx.pure.bool(false)]);
|
|
413
|
+
}
|
|
414
|
+
const escrow = tx.add(coinWithBalance({ type: ref.shareCoinType, balance: shares }));
|
|
415
|
+
const request = call('requestWithdrawal', [
|
|
416
|
+
escrow,
|
|
417
|
+
tx.pure.u64(requestId),
|
|
418
|
+
tx.pure.address(payout ?? sender),
|
|
419
|
+
tx.pure.option('address', finalizer === undefined ? sender : finalizer),
|
|
420
|
+
tx.object(constants_1.SUI_CLOCK_ID),
|
|
421
|
+
]);
|
|
422
|
+
// The contract returns the request unshared; it must be shared in the
|
|
423
|
+
// same transaction.
|
|
424
|
+
tx.moveCall({
|
|
425
|
+
target: (0, targets_1.suiMoveCallTarget)(contracts, 'shareWithdrawalRequest'),
|
|
426
|
+
typeArguments: [ref.shareCoinType],
|
|
427
|
+
arguments: [request],
|
|
428
|
+
});
|
|
429
|
+
return tx;
|
|
430
|
+
};
|
|
431
|
+
const results = await dryRun(config, async () => compose(true), method);
|
|
432
|
+
// results[0] is the fee charge.
|
|
433
|
+
const expectedAssets = (0, getters_1.decodeSuiUint)(results[1][0], 8);
|
|
434
|
+
// The contract accepts a request whose claim would pay nothing (shares worth
|
|
435
|
+
// less than one asset unit); it could then only be cancelled.
|
|
436
|
+
if (expectedAssets === 0n) {
|
|
437
|
+
throw new errors_1.UpshiftVaultAbortError('zero_output', `${method}: ${shares} shares pay no assets at the current price; request more.`, {}, { context: { network: ref.network, method } });
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
transaction: compose(false),
|
|
441
|
+
requestId,
|
|
442
|
+
expectedAssets,
|
|
443
|
+
request: await (0, getters_1.deriveSuiWithdrawalRequestId)(ref.vaultId, sender, requestId, contracts.originalPackageId),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Build an unsigned cancellation of a pending withdrawal request: the escrowed
|
|
448
|
+
* shares go back to the sender — one transaction.
|
|
449
|
+
*
|
|
450
|
+
* Allowed at any time while the request exists, also while the vault is
|
|
451
|
+
* paused or after the request expired. Only the wallet that made it may.
|
|
452
|
+
*
|
|
453
|
+
* RPC cost: one dry run.
|
|
454
|
+
*
|
|
455
|
+
* @param params - Vault, request object ID and sender.
|
|
456
|
+
* @param sui - The SDK's `sui` config; applied only for `params.vault.network`.
|
|
457
|
+
* @returns The unsigned transaction and the shares it returns.
|
|
458
|
+
* @throws {@link UpshiftValidationError} for a malformed address, unknown
|
|
459
|
+
* contract IDs, or a transaction that cannot be built (e.g. the request no
|
|
460
|
+
* longer exists).
|
|
461
|
+
* @throws {@link UpshiftVaultAbortError} when the vault would reject it
|
|
462
|
+
* (`not_request_owner`, `version_retired`).
|
|
463
|
+
* @throws {@link UpshiftNetworkError} when every Sui endpoint fails.
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* const { transaction } = await buildSuiCancelRedeemRequestTransaction(
|
|
467
|
+
* { vault: ref, request, sender: account.address },
|
|
468
|
+
* sdkSuiConfig,
|
|
469
|
+
* );
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
async function buildSuiCancelRedeemRequestTransaction(params, sui) {
|
|
473
|
+
const method = 'buildSuiCancelRedeemRequestTransaction';
|
|
474
|
+
const { vault: ref, request, sender } = params;
|
|
475
|
+
assertSender(sender, method);
|
|
476
|
+
assertVaultId(ref, method);
|
|
477
|
+
if (!(0, chain_address_1.isSuiAddress)(request)) {
|
|
478
|
+
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: request must be a Sui object ID.`);
|
|
479
|
+
}
|
|
480
|
+
const config = (0, getters_1.suiConfigForNetwork)(ref.network, sui);
|
|
481
|
+
const contracts = (0, targets_1.resolveSuiVaultContracts)(config);
|
|
482
|
+
const { Transaction: SuiTransaction } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
483
|
+
const compose = () => {
|
|
484
|
+
const tx = new SuiTransaction();
|
|
485
|
+
tx.setSender(sender);
|
|
486
|
+
const call = vaultCalls(tx, ref, contracts);
|
|
487
|
+
const shares = call('cancelWithdrawal', [tx.object(request)]);
|
|
488
|
+
tx.transferObjects([shares], sender);
|
|
489
|
+
return tx;
|
|
490
|
+
};
|
|
491
|
+
const results = await dryRun(config, async () => compose(), method);
|
|
492
|
+
// The returned Coin<Share> is a 32-byte UID followed by its u64 balance.
|
|
493
|
+
const coin = results[0][0];
|
|
494
|
+
return {
|
|
495
|
+
transaction: compose(),
|
|
496
|
+
shares: (0, getters_1.decodeSuiUint)(coin.subarray(32), 8),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Build an unsigned claim of a withdrawal request: charge the management fee,
|
|
501
|
+
* then burn the escrowed shares and pay their current value to the request's
|
|
502
|
+
* payout, with a minimum — one transaction.
|
|
503
|
+
*
|
|
504
|
+
* The payout lands in the payout address's **address balance**, not as a
|
|
505
|
+
* coin object. Balance reads (`getBalance`) include it; wallets that only
|
|
506
|
+
* list coin objects may not show it.
|
|
507
|
+
*
|
|
508
|
+
* The request is priced now, at the vault's current share price, without the
|
|
509
|
+
* instant-redemption fee. It can be claimed from `eligibleAtMs` until
|
|
510
|
+
* `expiresAtMs`, by its owner, its finalizer (anyone when it names none), and
|
|
511
|
+
* by anyone from one day after it became eligible.
|
|
512
|
+
*
|
|
513
|
+
* RPC cost: one dry run.
|
|
514
|
+
*
|
|
515
|
+
* @param params - Vault, request object ID, sender, and optional
|
|
516
|
+
* `minAssetsOut` or `slippageBps`.
|
|
517
|
+
* @param sui - The SDK's `sui` config; applied only for `params.vault.network`.
|
|
518
|
+
* @returns The unsigned transaction, the shares it burns, the dry run's
|
|
519
|
+
* payout and the minimum it enforces.
|
|
520
|
+
* @throws {@link UpshiftValidationError} for a malformed address, an
|
|
521
|
+
* out-of-range minimum, unknown contract IDs, or a transaction that cannot
|
|
522
|
+
* be built (e.g. the request no longer exists).
|
|
523
|
+
* @throws {@link UpshiftVaultAbortError} when the vault would reject it
|
|
524
|
+
* (`not_eligible_yet`, `request_expired`, `not_finalizer`, `paused`,
|
|
525
|
+
* `above_max_withdrawal`, `insufficient_liquidity`, `zero_output`,
|
|
526
|
+
* `slippage`, `version_retired`).
|
|
527
|
+
* @throws {@link UpshiftNetworkError} when every Sui endpoint fails.
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* const { transaction, expectedAssets } = await buildSuiClaimWithdrawalTransaction(
|
|
531
|
+
* { vault: ref, request, sender: account.address },
|
|
532
|
+
* sdkSuiConfig,
|
|
533
|
+
* );
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
async function buildSuiClaimWithdrawalTransaction(params, sui) {
|
|
537
|
+
const method = 'buildSuiClaimWithdrawalTransaction';
|
|
538
|
+
const { vault: ref, request, sender, minAssetsOut, slippageBps } = params;
|
|
539
|
+
assertSender(sender, method);
|
|
540
|
+
assertVaultId(ref, method);
|
|
541
|
+
if (!(0, chain_address_1.isSuiAddress)(request)) {
|
|
542
|
+
throw new errors_1.UpshiftValidationError('INVALID_ADDRESS', `${method}: request must be a Sui object ID.`);
|
|
543
|
+
}
|
|
544
|
+
assertAmount(minAssetsOut, method, 'minAssetsOut', false);
|
|
545
|
+
assertSlippage(slippageBps, method);
|
|
546
|
+
const config = (0, getters_1.suiConfigForNetwork)(ref.network, sui);
|
|
547
|
+
const contracts = (0, targets_1.resolveSuiVaultContracts)(config);
|
|
548
|
+
const { Transaction: SuiTransaction } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
549
|
+
const compose = (min, withPreview) => {
|
|
550
|
+
const tx = new SuiTransaction();
|
|
551
|
+
tx.setSender(sender);
|
|
552
|
+
const call = vaultCalls(tx, ref, contracts);
|
|
553
|
+
// Charge the accrued management fee first, as on deposit and redeem, so
|
|
554
|
+
// the claim is priced on the current NAV.
|
|
555
|
+
call('chargeManagementFee', [tx.object(constants_1.SUI_CLOCK_ID)]);
|
|
556
|
+
if (withPreview) {
|
|
557
|
+
const shares = tx.moveCall({
|
|
558
|
+
target: (0, targets_1.suiMoveCallTarget)(contracts, 'withdrawalRequestShares'),
|
|
559
|
+
typeArguments: [ref.shareCoinType],
|
|
560
|
+
arguments: [tx.object(request)],
|
|
561
|
+
});
|
|
562
|
+
call('previewRedemption', [shares, tx.pure.bool(false)]);
|
|
563
|
+
}
|
|
564
|
+
call('finalizeWithdrawalChecked', [
|
|
565
|
+
tx.object(request),
|
|
566
|
+
tx.pure.u64(min),
|
|
567
|
+
tx.object(constants_1.SUI_CLOCK_ID),
|
|
568
|
+
]);
|
|
569
|
+
return tx;
|
|
570
|
+
};
|
|
571
|
+
// results[0] is the fee charge, [1] the escrowed shares, [2] the preview.
|
|
572
|
+
const results = await dryRun(config, async () => compose(0n, true), method);
|
|
573
|
+
const shares = (0, getters_1.decodeSuiUint)(results[1][0], 8);
|
|
574
|
+
const expectedAssets = (0, getters_1.decodeSuiUint)(results[2][1], 8);
|
|
575
|
+
const min = minAssetsOut ??
|
|
576
|
+
suiMinOut(expectedAssets, slippageBps ?? exports.DEFAULT_SUI_SLIPPAGE_BPS);
|
|
577
|
+
return {
|
|
578
|
+
transaction: compose(min, false),
|
|
579
|
+
shares,
|
|
580
|
+
expectedAssets,
|
|
581
|
+
minAssetsOut: min,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
271
584
|
/**
|
|
272
585
|
* The vault, sender and share amount of an instant redemption in a built
|
|
273
586
|
* transaction, for the curator alert; `null` when it is not one.
|
|
@@ -331,7 +644,13 @@ async function submitSuiTransaction(signed, network, sui) {
|
|
|
331
644
|
const networkConfig = (0, client_1.resolveSuiNetworkConfig)(config);
|
|
332
645
|
const { Transaction: SuiTransaction } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/transactions')));
|
|
333
646
|
const { fromBase64 } = await Promise.resolve().then(() => __importStar(require('@mysten/sui/utils')));
|
|
334
|
-
|
|
647
|
+
let bytes;
|
|
648
|
+
try {
|
|
649
|
+
bytes = fromBase64(signed.bytes);
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
throw new errors_1.UpshiftValidationError('INVALID_INPUT', 'submitSuiTransaction: bytes is not valid base64.', { cause: err });
|
|
653
|
+
}
|
|
335
654
|
// Executing the same signed bytes twice is idempotent, so endpoint failover is safe.
|
|
336
655
|
const result = await (0, client_1.withSuiClient)(networkConfig, (client) => client.core.executeTransaction({
|
|
337
656
|
transaction: bytes,
|
|
@@ -43,7 +43,7 @@ export declare function suiEndpointHost(url: string): string;
|
|
|
43
43
|
*
|
|
44
44
|
* @param err - What the call rejected with.
|
|
45
45
|
* @returns `true` for transport failures, gRPC statuses in
|
|
46
|
-
*
|
|
46
|
+
* `RETRYABLE_GRPC_CODES`, a bare `NOT_FOUND` (the endpoint has no Sui
|
|
47
47
|
* gRPC service at that path) and the SDK's own call timeout.
|
|
48
48
|
* @internal
|
|
49
49
|
*/
|
|
@@ -58,8 +58,8 @@ export declare function setSuiClientFactoryForTests(factory: SuiClientFactory |
|
|
|
58
58
|
* Run a Sui call against the first endpoint that answers.
|
|
59
59
|
*
|
|
60
60
|
* Endpoints are tried in order, starting with the one that last worked (for
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* `PREFERRED_ENDPOINT_TTL_MS`). An endpoint failure — see
|
|
62
|
+
* `isRetryableSuiError` — moves on to the next endpoint; any other error
|
|
63
63
|
* is thrown at once. Each attempt is bounded by a 15 s timeout.
|
|
64
64
|
*
|
|
65
65
|
* Worst case: one attempt per endpoint (two with a consumer `rpcUrl`).
|
|
@@ -99,7 +99,7 @@ function suiEndpointHost(url) {
|
|
|
99
99
|
*
|
|
100
100
|
* @param err - What the call rejected with.
|
|
101
101
|
* @returns `true` for transport failures, gRPC statuses in
|
|
102
|
-
*
|
|
102
|
+
* `RETRYABLE_GRPC_CODES`, a bare `NOT_FOUND` (the endpoint has no Sui
|
|
103
103
|
* gRPC service at that path) and the SDK's own call timeout.
|
|
104
104
|
* @internal
|
|
105
105
|
*/
|
|
@@ -115,14 +115,29 @@ function isRetryableSuiError(err) {
|
|
|
115
115
|
return false;
|
|
116
116
|
if (RETRYABLE_GRPC_CODES.has(code))
|
|
117
117
|
return true;
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
118
|
+
// An endpoint with no Sui gRPC service at that path answers a bare HTTP 404
|
|
119
|
+
// or 400. The transport reports those as NOT_FOUND / INVALID_ARGUMENT whose
|
|
120
|
+
// message is the HTTP status text ('' over HTTP/2, the reason phrase over
|
|
121
|
+
// HTTP/1.1). Errors from the Sui service itself carry their own message.
|
|
122
|
+
return ((code === 'NOT_FOUND' &&
|
|
123
|
+
(err.message === '' || err.message === 'Not Found')) ||
|
|
124
|
+
(code === 'INVALID_ARGUMENT' && err.message === 'Bad Request'));
|
|
121
125
|
}
|
|
122
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Default factory: loads `@mysten/sui/grpc` on first use. A failure to load
|
|
128
|
+
* the module is about the runtime, not the endpoint, so it is thrown as an
|
|
129
|
+
* SDK error that {@link withSuiClient} does not retry: in Node.js, requiring
|
|
130
|
+
* the ESM-only package needs `^20.19 || >=22.12`.
|
|
131
|
+
*/
|
|
123
132
|
const defaultClientFactory = async (baseUrl, network) => {
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
let grpc;
|
|
134
|
+
try {
|
|
135
|
+
grpc = await Promise.resolve().then(() => __importStar(require('@mysten/sui/grpc')));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw new errors_1.UpshiftSDKError('UNKNOWN', `Sui support could not load @mysten/sui — in Node.js it needs version ^20.19 or >=22.12: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
139
|
+
}
|
|
140
|
+
return new grpc.SuiGrpcClient({ baseUrl, network });
|
|
126
141
|
};
|
|
127
142
|
let clientFactory = defaultClientFactory;
|
|
128
143
|
const clientCache = new Map();
|
|
@@ -151,8 +166,8 @@ function clientFor(url, network) {
|
|
|
151
166
|
* Run a Sui call against the first endpoint that answers.
|
|
152
167
|
*
|
|
153
168
|
* Endpoints are tried in order, starting with the one that last worked (for
|
|
154
|
-
*
|
|
155
|
-
*
|
|
169
|
+
* `PREFERRED_ENDPOINT_TTL_MS`). An endpoint failure — see
|
|
170
|
+
* `isRetryableSuiError` — moves on to the next endpoint; any other error
|
|
156
171
|
* is thrown at once. Each attempt is bounded by a 15 s timeout.
|
|
157
172
|
*
|
|
158
173
|
* Worst case: one attempt per endpoint (two with a consumer `rpcUrl`).
|
|
@@ -185,6 +200,9 @@ async function withSuiClient(config, call) {
|
|
|
185
200
|
client = await clientFor(url, config.network);
|
|
186
201
|
}
|
|
187
202
|
catch (err) {
|
|
203
|
+
// The SDK could not load its Sui client at all: no endpoint can help.
|
|
204
|
+
if (err instanceof errors_1.UpshiftSDKError)
|
|
205
|
+
throw err;
|
|
188
206
|
// A malformed endpoint (e.g. a typo'd consumer rpcUrl) must not disable
|
|
189
207
|
// the endpoints behind it.
|
|
190
208
|
lastError = err;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ISuiConfig, type ISuiNetwork, type ITokenizedVault, type IVault, type IVaultAvailableRedemption } from '../../types';
|
|
2
2
|
import type { IVaultBaseOptions } from '../../modules/vaults/types';
|
|
3
3
|
/** The vault a read is for: its object ID, coin types and network. */
|
|
4
4
|
export interface ISuiVaultRef {
|
|
@@ -134,6 +134,108 @@ export declare function previewSuiRedemption(ref: ISuiVaultRef, shares: bigint,
|
|
|
134
134
|
* @throws {@link UpshiftNetworkError} when every Sui endpoint fails.
|
|
135
135
|
*/
|
|
136
136
|
export declare function getSuiShareBalance(owner: string, shareCoinType: string, network: ISuiNetwork, sui?: ISuiConfig): Promise<bigint>;
|
|
137
|
+
/**
|
|
138
|
+
* A pending withdrawal request: shares escrowed in a queued vault, waiting to
|
|
139
|
+
* be claimed or cancelled. Times are Unix milliseconds from the chain's clock.
|
|
140
|
+
*/
|
|
141
|
+
export interface ISuiWithdrawalRequest {
|
|
142
|
+
/** Request object ID; pass it as `request` to claim or cancel. */
|
|
143
|
+
request: string;
|
|
144
|
+
/** The wallet's numeric ID for the request on this vault (`0, 1, 2, …`). */
|
|
145
|
+
requestId: bigint;
|
|
146
|
+
/** Wallet that made the request; the only one that may cancel it. */
|
|
147
|
+
owner: string;
|
|
148
|
+
/** Escrowed shares, in share base units. Priced when the request is claimed. */
|
|
149
|
+
shares: bigint;
|
|
150
|
+
/** Address the claim pays, into its address balance. */
|
|
151
|
+
payout: string;
|
|
152
|
+
/**
|
|
153
|
+
* The one address besides the owner that may claim, or `null` when anyone
|
|
154
|
+
* may. From one day after `eligibleAtMs` anyone may claim either way.
|
|
155
|
+
*/
|
|
156
|
+
finalizer: string | null;
|
|
157
|
+
/** When the request was made. */
|
|
158
|
+
requestedAtMs: number;
|
|
159
|
+
/** When the cooldown ends and the request can be claimed. */
|
|
160
|
+
eligibleAtMs: number;
|
|
161
|
+
/**
|
|
162
|
+
* When the request stops being claimable (it can still be cancelled), or
|
|
163
|
+
* `null` when it never expires.
|
|
164
|
+
*/
|
|
165
|
+
expiresAtMs: number | null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A wallet's pending withdrawal requests on a vault, oldest first.
|
|
169
|
+
*
|
|
170
|
+
* Finds the requests the SDK made (see `buildSuiRequestRedeemTransaction`),
|
|
171
|
+
* however old: requests made outside the SDK with non-sequential IDs are not
|
|
172
|
+
* listed. Claimed and cancelled requests no longer exist and are left out.
|
|
173
|
+
*
|
|
174
|
+
* RPC cost: one object read for a wallet that has made up to 23 requests on
|
|
175
|
+
* the vault (most wallets), one more per further 25.
|
|
176
|
+
*
|
|
177
|
+
* @param ref - The vault and its coin types and network.
|
|
178
|
+
* @param owner - Wallet that made the requests (`0x` + 64 hex chars).
|
|
179
|
+
* @param sui - The SDK's `sui` config; applied only for `ref.network`.
|
|
180
|
+
* @returns The pending requests; empty when there are none.
|
|
181
|
+
* @throws {@link UpshiftValidationError} (`INVALID_ADDRESS`) for a malformed
|
|
182
|
+
* vault ID or owner, (`INVALID_INPUT`) for unknown contract IDs, or a vault
|
|
183
|
+
* ID that is not a vault of the configured `sui.originalPackageId`.
|
|
184
|
+
* @throws {@link UpshiftNetworkError} when every Sui endpoint fails, or the
|
|
185
|
+
* node cannot answer for one of the objects.
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* const pending = await getSuiWithdrawalRequests(ref, account.address, sdkSuiConfig);
|
|
189
|
+
* const now = Date.now();
|
|
190
|
+
* const claimable = pending.filter(
|
|
191
|
+
* (r) => r.eligibleAtMs <= now && (r.expiresAtMs === null || now < r.expiresAtMs),
|
|
192
|
+
* );
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export declare function getSuiWithdrawalRequests(ref: ISuiVaultRef, owner: string, sui?: ISuiConfig): Promise<ISuiWithdrawalRequest[]>;
|
|
196
|
+
/**
|
|
197
|
+
* Split a wallet's withdrawal requests into position redemptions, the way
|
|
198
|
+
* the EVM and Solana paths do: `availableRedemptions` holds requests still in
|
|
199
|
+
* their cooldown or past their window (cancel-only), `pendingRedemptions` the
|
|
200
|
+
* ones claimable now. Each entry's `amount` is the escrowed shares; `date` is
|
|
201
|
+
* when it becomes claimable, `timestamp` when it was made (Unix seconds).
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
export declare function suiPositionRedemptions(requests: readonly ISuiWithdrawalRequest[], vault: string, decimals: number, nowMs: number): {
|
|
205
|
+
availableRedemptions: IVaultAvailableRedemption[];
|
|
206
|
+
pendingRedemptions: IVaultAvailableRedemption[];
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* A wallet's withdrawal requests on a vault as position redemptions (see
|
|
210
|
+
* {@link suiPositionRedemptions}). Empty when the SDK has no contract IDs for
|
|
211
|
+
* the network: the share balance can still be read without them, so a
|
|
212
|
+
* position should not fail for that.
|
|
213
|
+
*
|
|
214
|
+
* RPC cost: as {@link getSuiWithdrawalRequests}; none without contract IDs.
|
|
215
|
+
* @internal
|
|
216
|
+
*/
|
|
217
|
+
export declare function getSuiPositionRedemptions(ref: ISuiVaultRef, owner: string, decimals: number, sui?: ISuiConfig): Promise<ReturnType<typeof suiPositionRedemptions>>;
|
|
218
|
+
/**
|
|
219
|
+
* The request ID a wallet's next withdrawal request on a vault takes: the
|
|
220
|
+
* lowest it has never used.
|
|
221
|
+
*
|
|
222
|
+
* RPC cost: as {@link getSuiWithdrawalRequests}.
|
|
223
|
+
* @internal
|
|
224
|
+
*/
|
|
225
|
+
export declare function nextSuiWithdrawalRequestId(ref: ISuiVaultRef, owner: string, sui?: ISuiConfig): Promise<bigint>;
|
|
226
|
+
/**
|
|
227
|
+
* The object ID of a wallet's withdrawal request: derived from the vault, the
|
|
228
|
+
* owner and the request ID, as the contract derives it.
|
|
229
|
+
*
|
|
230
|
+
* @param vaultId - Vault object ID.
|
|
231
|
+
* @param owner - Wallet that made the request.
|
|
232
|
+
* @param requestId - The wallet's numeric request ID on the vault.
|
|
233
|
+
* @param originalPackageId - The original `upshift_vault` package, which
|
|
234
|
+
* names the derivation key type.
|
|
235
|
+
* @returns The request object ID.
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
export declare function deriveSuiWithdrawalRequestId(vaultId: string, owner: string, requestId: bigint, originalPackageId: string): Promise<string>;
|
|
137
239
|
/**
|
|
138
240
|
* The vault reference for a backend row, or `null` when the row has no usable
|
|
139
241
|
* `sui_vault_metadata`.
|
|
@@ -153,7 +255,7 @@ export declare function buildSuiVault(row: ITokenizedVault, state: ISuiVaultStat
|
|
|
153
255
|
*
|
|
154
256
|
* A failed read (unknown contract IDs, every endpoint down, an aborted
|
|
155
257
|
* simulation) does not fail the call: the vault is built from the backend row
|
|
156
|
-
* alone (see
|
|
258
|
+
* alone (see `buildSuiVault`) and a warning is logged.
|
|
157
259
|
*
|
|
158
260
|
* RPC cost: as {@link readSuiVaultState}.
|
|
159
261
|
*
|