@suilend/sdk 9.0.0-next.ge1ea2dc → 9.0.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/package.json +1 -1
- package/strategies.js +142 -22
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@suilend/sdk","version":"9.0.0
|
|
1
|
+
{"name":"@suilend/sdk","version":"9.0.0","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./client":"./client.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./api/events":"./api/events.js","./api":"./api/index.js","./lib/constants":"./lib/constants.js","./lib":"./lib/index.js","./lib/initialize":"./lib/initialize.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/proCompatible":"./lib/proCompatible.js","./lib/pyth":"./lib/pyth.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/transactions":"./lib/transactions.js","./lib/types":"./lib/types.js","./margin":"./margin/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers":"./parsers/index.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./parsers/obligation":"./parsers/obligation.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers/reserve":"./parsers/reserve.js","./swap":"./swap/index.js","./swap/quote":"./swap/quote.js","./swap/transaction":"./swap/transaction.js","./utils/events":"./utils/events.js","./utils/feedId":"./utils/feedId.js","./utils":"./utils/index.js","./utils/obligation":"./utils/obligation.js","./utils/simulate":"./utils/simulate.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend":"./_generated/suilend/index.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/permissions":"./margin/margin/permissions.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/version":"./margin/margin/version.js","./margin/utils":"./margin/utils/index.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/_dependencies/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js"},"types":"./index.d.ts","scripts":{"build":"rm -rf ./dist && tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json","test":"vitest run","lint:ci":"yarn run typecheck","prettier":"prettier --write src/ tests/","release":"yarn run build && node ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"^7.5.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^11.1.5","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"7.3.1","uuid":"^14.0.1"},"devDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.23.2","@suilend/sui-core":"^1.0.0","@tsconfig/recommended":"^1.0.8","@types/bn.js":"^5.2.0","@types/lodash":"^4.17.25","@types/node":"^26.1.2","fast-check":"^4.9.0","prettier":"^3.3.3","ts-node":"^10.9.2","typescript":"^6.0.3","vitest":"4.1.10"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":">=2.22.1 <3","@suilend/sui-core":"^1.0.0"},"type":"module"}
|
package/strategies.js
CHANGED
|
@@ -1145,6 +1145,11 @@ transaction, dryRunTransaction) => {
|
|
|
1145
1145
|
const fullyRepayBorrowsUsingLst = async (maxWithdrawRemainingLstAndRedepositAsBase) => {
|
|
1146
1146
|
if (depositReserves.lst === undefined)
|
|
1147
1147
|
throw new Error("LST reserve not found");
|
|
1148
|
+
// Everything below is appended to `transaction` while only the repay is dry-run,
|
|
1149
|
+
// so a failure has to undo the whole handler — otherwise an aborting command stays
|
|
1150
|
+
// in the transaction the user is asked to sign.
|
|
1151
|
+
const transactionBeforeFullRepay = Transaction.from(transaction);
|
|
1152
|
+
const depositsBeforeFullRepay = cloneDeep(deposits);
|
|
1148
1153
|
const borrowedAmountUsd = borrowedAmount.times(borrowReserve.price);
|
|
1149
1154
|
const fullRepaymentAmount = (borrowedAmountUsd.lt(0.1)
|
|
1150
1155
|
? new BigNumber(0.1).div(borrowReserve.price) // $0.1 in borrow coinType
|
|
@@ -1203,14 +1208,18 @@ transaction, dryRunTransaction) => {
|
|
|
1203
1208
|
txCopy.transferObjects([fullRepaymentCoin], _address); // Transfer remaining SUI to user
|
|
1204
1209
|
await dryRunTransaction(txCopy); // Throws error if fails
|
|
1205
1210
|
transaction = txCopy;
|
|
1211
|
+
// 2.3) Update state
|
|
1212
|
+
borrowedAmount = BigNumber.max(borrowedAmount.minus(repaidAmount), new BigNumber(0));
|
|
1206
1213
|
}
|
|
1207
1214
|
catch (err) {
|
|
1208
|
-
//
|
|
1215
|
+
// Undo the handler rather than carrying a command that aborts, and leave
|
|
1216
|
+
// `borrowedAmount` alone — nothing was repaid, so the caller must still see the
|
|
1217
|
+
// debt as outstanding.
|
|
1209
1218
|
console.error(err);
|
|
1210
|
-
transaction
|
|
1219
|
+
transaction = transactionBeforeFullRepay;
|
|
1220
|
+
deposits = depositsBeforeFullRepay;
|
|
1221
|
+
return;
|
|
1211
1222
|
}
|
|
1212
|
-
// 2.3) Update state
|
|
1213
|
-
borrowedAmount = BigNumber.max(borrowedAmount.minus(repaidAmount), new BigNumber(0));
|
|
1214
1223
|
console.log(`[unloopStrategyToExposure.fullyRepayBorrowsUsingLst] repay_borrows.update_state |`, JSON.stringify({
|
|
1215
1224
|
deposits: deposits.map((d) => ({
|
|
1216
1225
|
coinType: d.coinType,
|
|
@@ -1288,6 +1297,11 @@ transaction, dryRunTransaction) => {
|
|
|
1288
1297
|
const fullyRepayBorrowsUsingBase = async () => {
|
|
1289
1298
|
if (depositReserves.base === undefined)
|
|
1290
1299
|
throw new Error("Base reserve not found");
|
|
1300
|
+
// Everything below is appended to `transaction` while only the repay is dry-run,
|
|
1301
|
+
// so a failure has to undo the whole handler — otherwise an aborting command stays
|
|
1302
|
+
// in the transaction the user is asked to sign.
|
|
1303
|
+
const transactionBeforeFullRepay = Transaction.from(transaction);
|
|
1304
|
+
const depositsBeforeFullRepay = cloneDeep(deposits);
|
|
1291
1305
|
const borrowedAmountUsd = borrowedAmount.times(borrowReserve.price);
|
|
1292
1306
|
const fullRepaymentAmount = (borrowedAmountUsd.lt(0.1)
|
|
1293
1307
|
? new BigNumber(0.1).div(borrowReserve.price) // $0.1 in borrow coinType
|
|
@@ -1401,14 +1415,18 @@ transaction, dryRunTransaction) => {
|
|
|
1401
1415
|
txCopy.transferObjects([swapCoin], _address); // Transfer remaining to user
|
|
1402
1416
|
await dryRunTransaction(txCopy); // Throws error if fails
|
|
1403
1417
|
transaction = txCopy;
|
|
1418
|
+
// 4.2) Update state
|
|
1419
|
+
borrowedAmount = BigNumber.max(borrowedAmount.minus(repaidAmount), new BigNumber(0));
|
|
1404
1420
|
}
|
|
1405
1421
|
catch (err) {
|
|
1406
|
-
//
|
|
1422
|
+
// Undo the handler rather than carrying a command that aborts, and leave
|
|
1423
|
+
// `borrowedAmount` alone — nothing was repaid, so the caller must still see the
|
|
1424
|
+
// debt as outstanding.
|
|
1407
1425
|
console.error(err);
|
|
1408
|
-
transaction
|
|
1426
|
+
transaction = transactionBeforeFullRepay;
|
|
1427
|
+
deposits = depositsBeforeFullRepay;
|
|
1428
|
+
return;
|
|
1409
1429
|
}
|
|
1410
|
-
// 4.2) Update state
|
|
1411
|
-
borrowedAmount = BigNumber.max(borrowedAmount.minus(repaidAmount), new BigNumber(0));
|
|
1412
1430
|
console.log(`[unloopStrategyToExposure.fullyRepayBorrowsUsingBase] repay_borrows.update_state |`, JSON.stringify({
|
|
1413
1431
|
deposits: deposits.map((d) => ({
|
|
1414
1432
|
coinType: d.coinType,
|
|
@@ -1490,6 +1508,15 @@ transaction, dryRunTransaction) => {
|
|
|
1490
1508
|
stepMaxRepaidAmount: stepMaxRepaidAmount.toFixed(20),
|
|
1491
1509
|
}, null, 2));
|
|
1492
1510
|
// 1.2) Withdraw
|
|
1511
|
+
// Once this step's ceiling can cover everything still owed, it is the last one
|
|
1512
|
+
// — see the base branch for why looping again aborts with `EBorrowNotFound`.
|
|
1513
|
+
// Unlike that branch this needs no margin on top of `pendingBorrowedAmount`:
|
|
1514
|
+
// there is no swap, the redeem rate is deterministic, and every estimate here
|
|
1515
|
+
// errs the same way. `stepWithdrawnAmount` and `stepRepaidAmount` both round
|
|
1516
|
+
// down, and `lstToSuiExchangeRate` is a cached floor that only grows as staking
|
|
1517
|
+
// rewards accrue, so the SUI actually redeemed is at or above the estimate and
|
|
1518
|
+
// the tracked debt already sits at or above the real one.
|
|
1519
|
+
const isFinalStep = pendingBorrowedAmount.lte(stepMaxRepaidAmount);
|
|
1493
1520
|
const stepWithdrawnAmount = BigNumber.min(pendingBorrowedAmount, stepMaxRepaidAmount)
|
|
1494
1521
|
.times(1 - +(lst?.redeemFeePercent ?? 0) / 100) // Potential rounding issue (max 1 MIST)
|
|
1495
1522
|
.div(lst?.lstToSuiExchangeRate ?? 1)
|
|
@@ -1499,6 +1526,13 @@ transaction, dryRunTransaction) => {
|
|
|
1499
1526
|
stepWithdrawnAmount: stepWithdrawnAmount.toFixed(20),
|
|
1500
1527
|
isMaxWithdraw,
|
|
1501
1528
|
}, null, 2));
|
|
1529
|
+
// The withdraw, redeem and swap below go straight onto `transaction`, but only
|
|
1530
|
+
// the repay is dry-run. When that dry run fails, the step's commands are already
|
|
1531
|
+
// in the transaction, so every later dry run fails on that same command and the
|
|
1532
|
+
// user ends up signing a transaction already known to fail. Snapshot the
|
|
1533
|
+
// pre-step state so a failed step can be rolled back whole.
|
|
1534
|
+
const transactionBeforeStep = Transaction.from(transaction);
|
|
1535
|
+
const depositsBeforeStep = cloneDeep(deposits);
|
|
1502
1536
|
const [stepWithdrawnCoin] = strategyWithdraw(strategyType, loopingDepositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(loopingDepositReserve.coinType), BigInt(new BigNumber(stepWithdrawnAmount
|
|
1503
1537
|
.times(10 ** loopingDepositReserve.token.decimals)
|
|
1504
1538
|
.integerValue(BigNumber.ROUND_DOWN)
|
|
@@ -1528,27 +1562,46 @@ transaction, dryRunTransaction) => {
|
|
|
1528
1562
|
stepRepaidAmount: stepRepaidAmount.toFixed(20),
|
|
1529
1563
|
isMaxRepay,
|
|
1530
1564
|
}, null, 2));
|
|
1565
|
+
let stepClearedDebt = false;
|
|
1531
1566
|
try {
|
|
1532
1567
|
const txCopy = Transaction.from(transaction);
|
|
1533
1568
|
suilendClient.repay(obligationId, borrowReserve.coinType, stepSuiCoin, txCopy);
|
|
1534
1569
|
txCopy.transferObjects([stepSuiCoin], _address);
|
|
1535
1570
|
await dryRunTransaction(txCopy); // Throws error if fails
|
|
1536
1571
|
transaction = txCopy;
|
|
1572
|
+
// 3.2) Update state
|
|
1573
|
+
// A final step covers everything owed and `obligation::repay` clamps to the
|
|
1574
|
+
// real debt, so the target is reached exactly rather than by subtracting a
|
|
1575
|
+
// figure the rounding has already made fictional.
|
|
1576
|
+
borrowedAmount = isFinalStep
|
|
1577
|
+
? targetBorrowedAmount
|
|
1578
|
+
: borrowedAmount.minus(stepRepaidAmount);
|
|
1579
|
+
stepClearedDebt = isFinalStep;
|
|
1537
1580
|
}
|
|
1538
1581
|
catch (err) {
|
|
1539
|
-
//
|
|
1582
|
+
// The step cannot land — the redeem aborted, or the borrow is already gone
|
|
1583
|
+
// (`EBorrowNotFound`). Roll the whole step back either way: keeping it would
|
|
1584
|
+
// leave an aborting command in the transaction, so every later dry run would
|
|
1585
|
+
// fail on that same command and the user would be asked to sign a transaction
|
|
1586
|
+
// known to fail.
|
|
1540
1587
|
console.error(err);
|
|
1541
|
-
transaction
|
|
1588
|
+
transaction = transactionBeforeStep;
|
|
1589
|
+
deposits = depositsBeforeStep;
|
|
1590
|
+
break;
|
|
1542
1591
|
}
|
|
1543
|
-
// 3.2) Update state
|
|
1544
|
-
borrowedAmount = borrowedAmount.minus(stepRepaidAmount);
|
|
1545
1592
|
console.log(`[unloopStrategyToExposure] ${i} repay_sui.update_state |`, JSON.stringify({
|
|
1546
1593
|
deposits: deposits.map((d) => ({
|
|
1547
1594
|
coinType: d.coinType,
|
|
1548
1595
|
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1549
1596
|
})),
|
|
1550
1597
|
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1598
|
+
stepClearedDebt,
|
|
1551
1599
|
}, null, 2));
|
|
1600
|
+
// Nothing is owed above target any more. Returning here rather than looping
|
|
1601
|
+
// again also skips `fullyRepayBorrowsUsingLst`, which would repay a borrow that
|
|
1602
|
+
// no longer exists.
|
|
1603
|
+
if (stepClearedDebt)
|
|
1604
|
+
break;
|
|
1552
1605
|
}
|
|
1553
1606
|
// Base only
|
|
1554
1607
|
else if (loopingDepositReserve.coinType === depositReserves.base?.coinType) {
|
|
@@ -1591,8 +1644,24 @@ transaction, dryRunTransaction) => {
|
|
|
1591
1644
|
stepMaxWithdrawnAmount: stepMaxWithdrawnAmount.toFixed(20),
|
|
1592
1645
|
stepMaxRepaidAmount: stepMaxRepaidAmount.toFixed(20),
|
|
1593
1646
|
}, null, 2));
|
|
1647
|
+
const slippagePercent = 1;
|
|
1594
1648
|
// 1.2) Withdraw
|
|
1595
|
-
|
|
1649
|
+
// Once this step's ceiling can cover everything still owed, it is the last
|
|
1650
|
+
// one — so size it to clear the debt outright rather than to the exact tracked
|
|
1651
|
+
// figure. The swap may fill up to `slippagePercent`% below its quote, so scale
|
|
1652
|
+
// by that to stay covered at the worst fill. Overshooting is free:
|
|
1653
|
+
// `obligation::repay` clamps to what is actually owed
|
|
1654
|
+
// (`min(max_repay_amount, borrowed_amount)`) and the surplus is transferred
|
|
1655
|
+
// back to the user below.
|
|
1656
|
+
//
|
|
1657
|
+
// Sizing to the tracked figure instead leaves the loop believing a residual is
|
|
1658
|
+
// still owed, because that figure is deliberately held above the real debt (see
|
|
1659
|
+
// the repay step). The next iteration then aborts with `EBorrowNotFound` — a
|
|
1660
|
+
// fully repaid borrow is removed from the obligation, so there is nothing left
|
|
1661
|
+
// to repay against.
|
|
1662
|
+
const stepTargetRepaidAmount = pendingBorrowedAmount.div(1 - slippagePercent / 100);
|
|
1663
|
+
const isFinalStep = stepTargetRepaidAmount.lte(stepMaxRepaidAmount);
|
|
1664
|
+
const stepWithdrawnAmount = BigNumber.min(stepTargetRepaidAmount, stepMaxRepaidAmount)
|
|
1596
1665
|
.div(baseToBorrowExchangeRate)
|
|
1597
1666
|
.decimalPlaces(loopingDepositReserve.token.decimals, BigNumber.ROUND_DOWN);
|
|
1598
1667
|
const isMaxWithdraw = stepWithdrawnAmount.eq(stepMaxWithdrawnAmount);
|
|
@@ -1606,6 +1675,14 @@ transaction, dryRunTransaction) => {
|
|
|
1606
1675
|
// zero-amount quote — there's nothing meaningful left to unloop, so stop.
|
|
1607
1676
|
if (stepWithdrawnAmount.lte(0))
|
|
1608
1677
|
break;
|
|
1678
|
+
// The withdraw and the swap below go straight onto `transaction`, but only the
|
|
1679
|
+
// repay is dry-run. When that dry run fails, the step's commands are already in
|
|
1680
|
+
// the transaction — and the swap is usually what aborts — so every later dry run
|
|
1681
|
+
// fails on that same command and the user ends up signing a transaction already
|
|
1682
|
+
// known to fail. Snapshot the pre-step state so a failed step can be rolled back
|
|
1683
|
+
// whole.
|
|
1684
|
+
const transactionBeforeStep = Transaction.from(transaction);
|
|
1685
|
+
const depositsBeforeStep = cloneDeep(deposits);
|
|
1609
1686
|
const [stepWithdrawnCoin] = strategyWithdraw(strategyType, loopingDepositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(loopingDepositReserve.coinType), BigInt(new BigNumber(stepWithdrawnAmount
|
|
1610
1687
|
.times(10 ** loopingDepositReserve.token.decimals)
|
|
1611
1688
|
.integerValue(BigNumber.ROUND_DOWN)
|
|
@@ -1638,7 +1715,6 @@ transaction, dryRunTransaction) => {
|
|
|
1638
1715
|
});
|
|
1639
1716
|
if (!routers)
|
|
1640
1717
|
throw new Error("No swap quote found");
|
|
1641
|
-
const slippagePercent = 1;
|
|
1642
1718
|
let stepBorrowCoin;
|
|
1643
1719
|
try {
|
|
1644
1720
|
stepBorrowCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -1676,27 +1752,46 @@ transaction, dryRunTransaction) => {
|
|
|
1676
1752
|
stepRepaidAmount: stepRepaidAmount.toFixed(20),
|
|
1677
1753
|
isMaxRepay,
|
|
1678
1754
|
}, null, 2));
|
|
1755
|
+
let stepClearedDebt = false;
|
|
1679
1756
|
try {
|
|
1680
1757
|
const txCopy = Transaction.from(transaction);
|
|
1681
1758
|
suilendClient.repay(obligationId, borrowReserve.coinType, stepBorrowCoin, txCopy);
|
|
1682
1759
|
txCopy.transferObjects([stepBorrowCoin], _address);
|
|
1683
1760
|
await dryRunTransaction(txCopy); // Throws error if fails
|
|
1684
1761
|
transaction = txCopy;
|
|
1762
|
+
// 3.2) Update state
|
|
1763
|
+
// A final step was sized to clear the debt even at the worst fill, and the
|
|
1764
|
+
// repay clamps, so the target is reached exactly rather than by subtracting a
|
|
1765
|
+
// figure the haircut has already made fictional.
|
|
1766
|
+
borrowedAmount = isFinalStep
|
|
1767
|
+
? targetBorrowedAmount
|
|
1768
|
+
: borrowedAmount.minus(stepRepaidAmount);
|
|
1769
|
+
stepClearedDebt = isFinalStep;
|
|
1685
1770
|
}
|
|
1686
1771
|
catch (err) {
|
|
1687
|
-
//
|
|
1772
|
+
// The step cannot land — the swap's slippage check aborted, or the borrow is
|
|
1773
|
+
// already gone (`EBorrowNotFound`). Roll the whole step back either way:
|
|
1774
|
+
// keeping it would leave an aborting command in the transaction, so every
|
|
1775
|
+
// later dry run would fail on that same command and the user would be asked
|
|
1776
|
+
// to sign a transaction known to fail.
|
|
1688
1777
|
console.error(err);
|
|
1689
|
-
transaction
|
|
1778
|
+
transaction = transactionBeforeStep;
|
|
1779
|
+
deposits = depositsBeforeStep;
|
|
1780
|
+
break;
|
|
1690
1781
|
}
|
|
1691
|
-
// 3.2) Update state
|
|
1692
|
-
borrowedAmount = borrowedAmount.minus(stepRepaidAmount);
|
|
1693
1782
|
console.log(`[unloopStrategyToExposure] ${i} repay_borrows.update_state |`, JSON.stringify({
|
|
1694
1783
|
deposits: deposits.map((d) => ({
|
|
1695
1784
|
coinType: d.coinType,
|
|
1696
1785
|
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1697
1786
|
})),
|
|
1698
1787
|
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1788
|
+
stepClearedDebt,
|
|
1699
1789
|
}, null, 2));
|
|
1790
|
+
// Nothing is owed above target any more. Returning here rather than looping
|
|
1791
|
+
// again also skips `fullyRepayBorrowsUsingBase`, which would repay a borrow
|
|
1792
|
+
// that no longer exists.
|
|
1793
|
+
if (stepClearedDebt)
|
|
1794
|
+
break;
|
|
1700
1795
|
}
|
|
1701
1796
|
else {
|
|
1702
1797
|
throw new Error("No LST or base reserve found"); // Should not happen
|
|
@@ -1963,7 +2058,22 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
1963
2058
|
}
|
|
1964
2059
|
// 2) Withdraw base or LST
|
|
1965
2060
|
// 2.1) Withdraw
|
|
1966
|
-
|
|
2061
|
+
// `withdrawnAmount` is priced off the pre-unloop position, but the unloop spends
|
|
2062
|
+
// collateral to repay borrows and each step only counts `slippagePercent`% less
|
|
2063
|
+
// than its quote towards the debt. The tracked debt therefore lags the real one,
|
|
2064
|
+
// the loop unwinds further than it needs to, and what is left can be less than
|
|
2065
|
+
// the amount the user asked for. Withdrawing the unclamped amount underflows
|
|
2066
|
+
// `obligation::withdraw_unchecked` on-chain and fails the whole transaction, so
|
|
2067
|
+
// clamp to what the position still holds.
|
|
2068
|
+
const remainingDepositedAmount = deposits.find((d) => d.coinType === depositReserve.coinType)
|
|
2069
|
+
?.depositedAmount ?? new BigNumber(0);
|
|
2070
|
+
const finalWithdrawnAmount = BigNumber.max(new BigNumber(0), BigNumber.min(withdrawnAmount, remainingDepositedAmount)).decimalPlaces(depositReserve.token.decimals, BigNumber.ROUND_DOWN);
|
|
2071
|
+
console.log(`[withdraw] withdraw.clamp |`, JSON.stringify({
|
|
2072
|
+
withdrawnAmount: withdrawnAmount.toFixed(20),
|
|
2073
|
+
remainingDepositedAmount: remainingDepositedAmount.toFixed(20),
|
|
2074
|
+
finalWithdrawnAmount: finalWithdrawnAmount.toFixed(20),
|
|
2075
|
+
}, null, 2));
|
|
2076
|
+
const [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), BigInt(new BigNumber(finalWithdrawnAmount
|
|
1967
2077
|
.times(10 ** depositReserve.token.decimals)
|
|
1968
2078
|
.integerValue(BigNumber.ROUND_DOWN)
|
|
1969
2079
|
.toString())
|
|
@@ -1973,7 +2083,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
1973
2083
|
// 2.2) Update state
|
|
1974
2084
|
deposits = addOrInsertStrategyDeposit(deposits, {
|
|
1975
2085
|
coinType: depositReserve.coinType,
|
|
1976
|
-
depositedAmount:
|
|
2086
|
+
depositedAmount: finalWithdrawnAmount.times(-1),
|
|
1977
2087
|
});
|
|
1978
2088
|
const newExposure = getStrategyExposure(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, deposits, borrowedAmount));
|
|
1979
2089
|
const newTvlAmountUsd = getStrategyTvlAmount(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, deposits, borrowedAmount)).times(defaultCurrencyReserve.price);
|
|
@@ -1998,7 +2108,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
1998
2108
|
const routers = await cetusSdk.findRouters({
|
|
1999
2109
|
from: depositReserve.coinType,
|
|
2000
2110
|
target: withdraw.coinType,
|
|
2001
|
-
amount: new BN(
|
|
2111
|
+
amount: new BN(finalWithdrawnAmount
|
|
2002
2112
|
.times(10 ** depositReserve.token.decimals)
|
|
2003
2113
|
.integerValue(BigNumber.ROUND_DOWN)
|
|
2004
2114
|
.toString()),
|
|
@@ -2089,6 +2199,12 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2089
2199
|
}, null, 2));
|
|
2090
2200
|
}
|
|
2091
2201
|
// 2) MAX withdraw base or LST
|
|
2202
|
+
// The withdraw below takes every deposit, which the obligation only permits once
|
|
2203
|
+
// the borrows are gone. The unloop stops early if a step cannot land, so check
|
|
2204
|
+
// rather than build a transaction that is certain to abort on the health check —
|
|
2205
|
+
// the caller can surface this instead of asking the user to sign it.
|
|
2206
|
+
if (borrowedAmount.gt(STRATEGY_E))
|
|
2207
|
+
throw new Error("Failed to repay borrows before withdrawing");
|
|
2092
2208
|
const estimatedWithdrawnAmount = deposits.find((d) => d.coinType === depositReserve.coinType)
|
|
2093
2209
|
?.depositedAmount ?? new BigNumber(0);
|
|
2094
2210
|
const [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), BigInt(MAX_U64.toString()), transaction);
|
|
@@ -2150,6 +2266,10 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2150
2266
|
}
|
|
2151
2267
|
// 4) Claim rewards and transfer to user
|
|
2152
2268
|
if (hasClaimableRewards) {
|
|
2269
|
+
// Claiming is a bonus, so a failure must not block the withdraw — but the calls
|
|
2270
|
+
// are appended as they are built, so a throw part-way leaves half a claim behind
|
|
2271
|
+
// that can abort the whole transaction. Roll back to before the claim instead.
|
|
2272
|
+
const transactionBeforeClaim = Transaction.from(transaction);
|
|
2153
2273
|
try {
|
|
2154
2274
|
const mergedCoinsMap = strategyClaimRewardsAndMergeCoins(strategyType, rewardsMap, strategyOwnerCapId, transaction);
|
|
2155
2275
|
for (const coin of Object.values(mergedCoinsMap)) {
|
|
@@ -2157,8 +2277,8 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2157
2277
|
}
|
|
2158
2278
|
}
|
|
2159
2279
|
catch (err) {
|
|
2160
|
-
// Don't block user if fails
|
|
2161
2280
|
console.error(err);
|
|
2281
|
+
transaction = transactionBeforeClaim;
|
|
2162
2282
|
}
|
|
2163
2283
|
}
|
|
2164
2284
|
return { deposits, borrowedAmount, transaction };
|