@gearbox-protocol/sdk 15.1.0-next.22 → 15.1.0-next.24
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/cjs/model/opportunities.schema.js +3 -5
- package/dist/cjs/new-sdk/prepare/PrepareApi.js +4 -4
- package/dist/cjs/sdk/accounts/intents/index.js +57 -28
- package/dist/cjs/sdk/accounts/intents/plan.js +40 -2
- package/dist/cjs/sdk/accounts/intents/realize.js +22 -12
- package/dist/cjs/sdk/accounts/intents/tail.js +120 -0
- package/dist/cjs/sdk/accounts/intents/utils/index.js +1 -0
- package/dist/cjs/sdk/accounts/intents/utils/router-path.js +32 -0
- package/dist/cjs/sdk/index.js +4 -1
- package/dist/cjs/sdk/market/credit/CreditSuite.js +10 -8
- package/dist/cjs/sdk/market/index.js +4 -1
- package/dist/cjs/sdk/market/math.js +71 -17
- package/dist/esm/dev/AccountOpener.js +1 -1
- package/dist/esm/dev/withdrawalUtils.js +1 -1
- package/dist/esm/model/opportunities.schema.js +3 -5
- package/dist/esm/new-sdk/prepare/PrepareApi.js +4 -4
- package/dist/esm/preview/simulate/simulatePoolOperation.js +1 -1
- package/dist/esm/preview/trace/extractTransfers.js +1 -1
- package/dist/esm/sdk/accounts/CreditAccountsServiceV310.js +2 -2
- package/dist/esm/sdk/accounts/intents/index.js +58 -29
- package/dist/esm/sdk/accounts/intents/plan.js +40 -3
- package/dist/esm/sdk/accounts/intents/realize.js +22 -12
- package/dist/esm/sdk/accounts/intents/tail.js +118 -0
- package/dist/esm/sdk/accounts/intents/utils/index.js +2 -2
- package/dist/esm/sdk/accounts/intents/utils/router-path.js +32 -1
- package/dist/esm/sdk/accounts/liquidations/LiquidationsService.js +1 -1
- package/dist/esm/sdk/accounts/withdrawal-compressor/RedemptionLoggerV310Contract.js +1 -1
- package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV310Contract.js +1 -1
- package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV311Contract.js +1 -1
- package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +1 -1
- package/dist/esm/sdk/base/TokensMeta.js +3 -3
- package/dist/esm/sdk/chain/detectNetwork.js +1 -1
- package/dist/esm/sdk/core/createAddressProvider.js +1 -1
- package/dist/esm/sdk/index.js +2 -2
- package/dist/esm/sdk/market/adapters/contracts/AccountMigratorAdapterContract.js +1 -1
- package/dist/esm/sdk/market/adapters/contracts/ERC4626AdapterContract.js +1 -1
- package/dist/esm/sdk/market/credit/CreditFacadeV310BaseContract.js +1 -1
- package/dist/esm/sdk/market/credit/CreditSuite.js +11 -9
- package/dist/esm/sdk/market/index.js +2 -2
- package/dist/esm/sdk/market/math.js +68 -17
- package/dist/esm/sdk/market/pool/PoolV310Contract.js +1 -1
- package/dist/esm/sdk/market/zapper/IETHZapperContract.js +1 -1
- package/dist/esm/sdk/market/zapper/ZapperContract.js +1 -1
- package/dist/esm/sdk/pools/PoolService.js +1 -1
- package/dist/esm/sdk/utils/viem/simulateWithPriceUpdates.js +1 -1
- package/dist/types/model/opportunities.d.ts +9 -25
- package/dist/types/model/opportunities.schema.d.ts +12 -204
- package/dist/types/new-sdk/prepare/types.d.ts +22 -8
- package/dist/types/sdk/accounts/intents/index.d.ts +13 -6
- package/dist/types/sdk/accounts/intents/plan.d.ts +18 -2
- package/dist/types/sdk/accounts/intents/realize.d.ts +7 -0
- package/dist/types/sdk/accounts/intents/tail.d.ts +52 -0
- package/dist/types/sdk/accounts/intents/types.d.ts +46 -10
- package/dist/types/sdk/accounts/intents/utils/index.d.ts +2 -2
- package/dist/types/sdk/accounts/intents/utils/router-path.d.ts +15 -1
- package/dist/types/sdk/index.d.ts +2 -2
- package/dist/types/sdk/market/credit/CreditSuite.d.ts +1 -5
- package/dist/types/sdk/market/index.d.ts +2 -2
- package/dist/types/sdk/market/math.d.ts +64 -13
- package/package.json +1 -1
|
@@ -75,6 +75,70 @@ function calcBorrowApy(baseInterestRate, feeInterest) {
|
|
|
75
75
|
return rayToBps(baseInterestRate * (require_sdk_constants_math.PERCENTAGE_FACTOR + BigInt(feeInterest)) / require_sdk_constants_math.PERCENTAGE_FACTOR);
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
+
* Annual quota cost of a collateral, in basis points:
|
|
79
|
+
* `quotaRate × (1 + feeInterest)` — the quoted rate plus the protocol's cut of
|
|
80
|
+
* the accrued quota interest, matching {@link calcBorrowApy}.
|
|
81
|
+
*
|
|
82
|
+
* @param quotaRate - Pool quota keeper rate in basis points, without the fee.
|
|
83
|
+
* @param feeInterest - Credit manager interest fee in basis points.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* // quotaRate: 200 bps = 2%, feeInterest: 2500 bps = 25%
|
|
88
|
+
* calcQuotaRate(200, 2500) // 2% × 1.25 = 250 bps = 2.5%
|
|
89
|
+
* ```
|
|
90
|
+
**/
|
|
91
|
+
function calcQuotaRate(quotaRate, feeInterest) {
|
|
92
|
+
return Math.round(quotaRate * (FULL + feeInterest) / FULL);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extra quota, as a fraction of equity, that an aggressive position quotes
|
|
96
|
+
* above the debt it actually owes. Matches {@link MAX_LEVERAGE_BUFFER_BPS}.
|
|
97
|
+
**/
|
|
98
|
+
const DEFAULT_QUOTA_BUFFER_BPS = 500;
|
|
99
|
+
/**
|
|
100
|
+
* Quoted amount per unit of equity at the given leverage and quota mode.
|
|
101
|
+
* Dimensionless: `1` means the quota equals the user's equity.
|
|
102
|
+
**/
|
|
103
|
+
function calcQuotaMultiplier(leverage, lt, quotaMode = "safe") {
|
|
104
|
+
switch (quotaMode) {
|
|
105
|
+
case "min": return leverage - 1;
|
|
106
|
+
case "safe": return leverage * lt / FULL;
|
|
107
|
+
case "aggressive": return (1 + 500 / FULL) * (leverage - 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Annual cost of credit on the user's equity, in basis points, at a given
|
|
112
|
+
* leverage and quota mode: base interest on the borrowed part plus quota
|
|
113
|
+
* interest on the quoted amount. Both rates already include the protocol's
|
|
114
|
+
* interest fee.
|
|
115
|
+
*
|
|
116
|
+
* @param opportunity - Borrow APY, quota rate, and liquidation threshold.
|
|
117
|
+
* @param leverage - Total-value leverage, same scale as {@link Leverage}.
|
|
118
|
+
* @param mode - How much quota the position quotes, see {@link QuotaMode}.
|
|
119
|
+
**/
|
|
120
|
+
function calcEffectiveBorrowApy(opportunity, leverage, mode = "safe") {
|
|
121
|
+
const { borrowApy, quotaRate, liquidationThreshold } = opportunity;
|
|
122
|
+
return Math.round(borrowApy * (leverage - 1) + quotaRate * calcQuotaMultiplier(leverage, liquidationThreshold, mode));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Net yield of a strategy on the user's equity, in basis points, at a given
|
|
126
|
+
* leverage and quota mode:
|
|
127
|
+
* `leverage × totalCollateralApy − effectiveBorrowApy`. Collateral yield is
|
|
128
|
+
* on the whole position; borrow and quota interest are those of
|
|
129
|
+
* {@link calcEffectiveBorrowApy}.
|
|
130
|
+
*
|
|
131
|
+
* @param opportunity - Borrow APY, quota rate, and liquidation threshold.
|
|
132
|
+
* @param totalCollateralApy - Collateral yield the caller chose, typically
|
|
133
|
+
* `totalApy` of {@link StrategyOpportunity.collateralApy} or
|
|
134
|
+
* {@link StrategyOpportunity.collateralApyAvg7D}.
|
|
135
|
+
* @param leverage - Total-value leverage, same scale as {@link Leverage}.
|
|
136
|
+
* @param mode - How much quota the position quotes, see {@link QuotaMode}.
|
|
137
|
+
**/
|
|
138
|
+
function calcNetStrategyApy(opportunity, totalCollateralApy, leverage, mode = "safe") {
|
|
139
|
+
return Math.round(leverage * totalCollateralApy - calcEffectiveBorrowApy(opportunity, leverage, mode));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
78
142
|
* 5% safety margin subtracted from 100% in {@link calcMaxLeverage}, so a
|
|
79
143
|
* maxed position opens with HF slightly above 1.
|
|
80
144
|
**/
|
|
@@ -91,9 +155,11 @@ const MAX_LEVERAGE_BUFFER_BPS = 500;
|
|
|
91
155
|
* // liquidationThreshold: 9000 bps = 90%
|
|
92
156
|
* calcMaxLeverage(9000) // (1 − 0.05) / (1 − 0.9) = 9.5x total exposure
|
|
93
157
|
* ```
|
|
158
|
+
* @throws If `liquidationThreshold` is 100% or more, which would make
|
|
159
|
+
* leverage unbounded.
|
|
94
160
|
**/
|
|
95
161
|
function calcMaxLeverage(liquidationThreshold) {
|
|
96
|
-
if (liquidationThreshold >= FULL)
|
|
162
|
+
if (liquidationThreshold >= FULL) throw new Error("cannot compute max leverage: liquidation threshold is 100% or more");
|
|
97
163
|
const leverage = (FULL - 500) / (FULL - liquidationThreshold);
|
|
98
164
|
return Math.max(leverage, 1);
|
|
99
165
|
}
|
|
@@ -135,21 +201,6 @@ function calcPositionLeverage(totalValue, totalDebt) {
|
|
|
135
201
|
return Number(totalValue) / Number(equity);
|
|
136
202
|
}
|
|
137
203
|
/**
|
|
138
|
-
* Annual quota cost on equity, in basis points:
|
|
139
|
-
* `quotaRate × (1 + feeInterest) × leverage`. Quota accrues on the whole
|
|
140
|
-
* quoted position, and the DAO takes `feeInterest` of it as with base interest.
|
|
141
|
-
*
|
|
142
|
-
* @example
|
|
143
|
-
* ```ts
|
|
144
|
-
* // quotaRate: 200 bps = 2%, feeInterest: 2500 bps = 25%, leverage: 9.5x
|
|
145
|
-
* calcAdditionalBorrowApy(200, 2500, 9.5) // 2% × 1.25 × 9.5 = 2375 bps = 23.75%
|
|
146
|
-
* ```
|
|
147
|
-
**/
|
|
148
|
-
function calcAdditionalBorrowApy(quotaRate, feeInterest, leverage) {
|
|
149
|
-
if (!Number.isFinite(leverage) || leverage <= 0) return 0;
|
|
150
|
-
return Math.round(quotaRate * (1 + feeInterest / FULL) * leverage);
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
204
|
* {@link PERCENTAGE_FACTOR} less a 0.1% safety buffer.
|
|
154
205
|
*
|
|
155
206
|
* Partial liquidation amounts are computed off prices that can drift between
|
|
@@ -205,12 +256,15 @@ function optimalHFForPartialLiquidation(borrowRate) {
|
|
|
205
256
|
return require_sdk_constants_math.PERCENTAGE_FACTOR + (borrowRate < 100n ? borrowRate : 100n);
|
|
206
257
|
}
|
|
207
258
|
//#endregion
|
|
259
|
+
exports.DEFAULT_QUOTA_BUFFER_BPS = DEFAULT_QUOTA_BUFFER_BPS;
|
|
208
260
|
exports.MAX_LEVERAGE_BUFFER_BPS = MAX_LEVERAGE_BUFFER_BPS;
|
|
209
261
|
exports.PARTIAL_LIQUIDATION_BUFFER_BPS = PARTIAL_LIQUIDATION_BUFFER_BPS;
|
|
210
|
-
exports.calcAdditionalBorrowApy = calcAdditionalBorrowApy;
|
|
211
262
|
exports.calcBorrowApy = calcBorrowApy;
|
|
263
|
+
exports.calcEffectiveBorrowApy = calcEffectiveBorrowApy;
|
|
212
264
|
exports.calcMaxLeverage = calcMaxLeverage;
|
|
265
|
+
exports.calcNetStrategyApy = calcNetStrategyApy;
|
|
213
266
|
exports.calcPositionLeverage = calcPositionLeverage;
|
|
267
|
+
exports.calcQuotaRate = calcQuotaRate;
|
|
214
268
|
exports.calcUtilization = calcUtilization;
|
|
215
269
|
exports.healthFactorBps = healthFactorBps;
|
|
216
270
|
exports.minSeizedAmount = minSeizedAmount;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ierc20Abi } from "../abi/iERC20.js";
|
|
1
2
|
import { iCreditFacadeV310Abi } from "../abi/310/generated.js";
|
|
2
3
|
import { AddressMap } from "../sdk/utils/AddressMap.js";
|
|
3
4
|
import { AddressSet } from "../sdk/utils/AddressSet.js";
|
|
4
5
|
import { AssetsMap } from "../sdk/utils/AssetsMap.js";
|
|
5
6
|
import { childLogger } from "../sdk/utils/childLogger.js";
|
|
6
|
-
import { ierc20Abi } from "../abi/iERC20.js";
|
|
7
7
|
import "../sdk/constants/addresses.js";
|
|
8
8
|
import { MAX_UINT256, PERCENTAGE_FACTOR } from "../sdk/constants/math.js";
|
|
9
9
|
import { SDKConstruct } from "../sdk/base/SDKConstruct.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { iWithdrawalCompressorV313Abi } from "../abi/IWithdrawalCompressorV313.js";
|
|
1
2
|
import { getNetworkType } from "../sdk/chain/chains.js";
|
|
2
3
|
import { getWithdrawalCompressorAddress } from "../sdk/accounts/withdrawal-compressor/addresses.js";
|
|
3
|
-
import { iWithdrawalCompressorV313Abi } from "../abi/IWithdrawalCompressorV313.js";
|
|
4
4
|
import "../sdk/index.js";
|
|
5
5
|
import { iMidasDataFeedAbi, iMidasRedemptionVaultAbi, midasGatewayAbi, midasRedeemerAbi, midasRedemptionVaultPhantomTokenAbi, securitizeRedeemerAbi, securitizeRedemptionGatewayAbi, securitizeRedemptionPhantomTokenAbi } from "./withdrawalAbi.js";
|
|
6
6
|
import { erc20Abi, hexToString, parseAbi, parseEther } from "viem";
|
|
@@ -91,12 +91,10 @@ const strategyOpportunitySchema = z.object({
|
|
|
91
91
|
expirationDate: timestampSchema.nullable(),
|
|
92
92
|
collateralApy: offchainOnly(apyBreakdownSchema).optional(),
|
|
93
93
|
collateralApyAvg7D: offchainOnly(apyBreakdownSchema).optional(),
|
|
94
|
-
|
|
95
|
-
maxLeverageApyAvg7D: offchainOnly(apyBreakdownSchema).optional(),
|
|
96
|
-
borrowApy: tolerance(bpsSchema, "bps").optional(),
|
|
94
|
+
borrowApy: tolerance(bpsSchema, "bps"),
|
|
97
95
|
borrowApyAvg7D: offchainOnly(bpsSchema).optional(),
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
quotaRate: tolerance(bpsSchema, "bps"),
|
|
97
|
+
quotaRateAvg7D: offchainOnly(bpsSchema).optional(),
|
|
100
98
|
totalValue: offchainOnly(amountSchema).optional(),
|
|
101
99
|
utilization: offchainOnly(bpsSchema).optional(),
|
|
102
100
|
availableLiquidity: tolerance(amountSchema, "amount"),
|
|
@@ -306,12 +306,12 @@ function slice(sdk, creditAccount) {
|
|
|
306
306
|
}
|
|
307
307
|
/**
|
|
308
308
|
* The operation a claim resumes, or `undefined` when there is none to resume:
|
|
309
|
-
* a withdrawal requested without an intent,
|
|
310
|
-
*
|
|
309
|
+
* a withdrawal requested without an intent, or one read through a compressor
|
|
310
|
+
* too old to report it. Every intent the engine records can be finished,
|
|
311
|
+
* `CLOSE_ACCOUNT` included.
|
|
311
312
|
**/
|
|
312
313
|
function resumable(intent) {
|
|
313
|
-
|
|
314
|
-
return intent;
|
|
314
|
+
return intent ?? void 0;
|
|
315
315
|
}
|
|
316
316
|
/**
|
|
317
317
|
* Picks the route the operation takes out of `tokenIn`, as a value rather than
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { iPoolV310Abi } from "../../abi/310/generated.js";
|
|
2
1
|
import { iZapperAbi } from "../../abi/iZapper.js";
|
|
2
|
+
import { iPoolV310Abi } from "../../abi/310/generated.js";
|
|
3
3
|
import { asPreviewSimulationError } from "./errors.js";
|
|
4
4
|
//#region src/preview/simulate/simulatePoolOperation.ts
|
|
5
5
|
function previewRead(operation) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { ierc20Abi } from "../../abi/iERC20.js";
|
|
1
2
|
import { iCreditFacadeV310Abi } from "../../abi/310/generated.js";
|
|
2
3
|
import { AddressMap } from "../../sdk/utils/AddressMap.js";
|
|
3
|
-
import { ierc20Abi } from "../../abi/iERC20.js";
|
|
4
4
|
import "../../sdk/index.js";
|
|
5
5
|
import { UnexpectedFacadeEventOrderError } from "./errors.js";
|
|
6
6
|
import { getAddress, isAddressEqual, parseEventLogs } from "viem";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { iBaseRewardPoolAbi } from "../../abi/iBaseRewardPool.js";
|
|
2
|
+
import { ierc4626AdapterAbi } from "../../abi/ierc4626Adapter.js";
|
|
1
3
|
import { AP_REWARDS_COMPRESSOR } from "../constants/address-provider.js";
|
|
2
4
|
import { ADDRESS_0X0 } from "../constants/addresses.js";
|
|
3
5
|
import { MAX_UINT256 } from "../constants/math.js";
|
|
@@ -8,8 +10,6 @@ import "../base/index.js";
|
|
|
8
10
|
import { AccountBotsService } from "./bots/AccountBotsService.js";
|
|
9
11
|
import "./bots/index.js";
|
|
10
12
|
import { rewardsCompressorAbi } from "../../abi/compressors/rewardsCompressor.js";
|
|
11
|
-
import { iBaseRewardPoolAbi } from "../../abi/iBaseRewardPool.js";
|
|
12
|
-
import { ierc4626AdapterAbi } from "../../abi/ierc4626Adapter.js";
|
|
13
13
|
import { expectedBalanceDeltas } from "../market/credit/expectedBalanceDeltas.js";
|
|
14
14
|
import "../market/index.js";
|
|
15
15
|
import { CreditAccountCompressor } from "./credit-account-compressor/CreditAccountCompressor.js";
|
|
@@ -4,10 +4,10 @@ import { assertMarketOperable } from "./guards.js";
|
|
|
4
4
|
import { maxProportionalWithdrawal } from "./math.js";
|
|
5
5
|
import { fetchCreditAccountSlice, toCreditAccountSlice } from "./utils/credit-account-slice.js";
|
|
6
6
|
import { previewOpenStrategy } from "./open-strategy.js";
|
|
7
|
-
import {
|
|
8
|
-
import { planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planFinishClaimOnly, planFinishDecreaseLeverage, planFinishWithdraw, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed } from "./plan.js";
|
|
7
|
+
import { planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed } from "./plan.js";
|
|
9
8
|
import { realize } from "./realize.js";
|
|
10
9
|
import { accountView } from "./view.js";
|
|
10
|
+
import { planTail, projectTail } from "./tail.js";
|
|
11
11
|
//#region src/sdk/accounts/intents/index.ts
|
|
12
12
|
/**
|
|
13
13
|
* Previews of everything a wallet can do to an existing credit account.
|
|
@@ -80,10 +80,16 @@ var CreditAccountOperationsService = class extends SDKConstruct {
|
|
|
80
80
|
* {@link intentRoutes}; this is the one to call when the delayed route is the
|
|
81
81
|
* only one of interest.
|
|
82
82
|
*
|
|
83
|
+
* `preview` is where the intent ends — the account once the redemption has
|
|
84
|
+
* matured, been claimed and the tail has run — because that is what the
|
|
85
|
+
* caller asked for; the half-way state the request itself lands in is
|
|
86
|
+
* `delayed.afterRequest`. Both are validated, so a request whose tail could
|
|
87
|
+
* not be completed is refused instead of started.
|
|
88
|
+
*
|
|
83
89
|
* @param props - Intent plus account slice, quota reserve and slippage
|
|
84
|
-
* @returns The request transaction
|
|
85
|
-
* `{ ok: false, reason }` — `noDelayedRoute` when this route
|
|
86
|
-
* for the account at all
|
|
90
|
+
* @returns The request transaction, the state it ends in and what it recorded
|
|
91
|
+
* for the tail, or `{ ok: false, reason }` — `noDelayedRoute` when this route
|
|
92
|
+
* does not exist for the account at all
|
|
87
93
|
*/
|
|
88
94
|
async startDelayedIntent(props) {
|
|
89
95
|
const { intent } = props;
|
|
@@ -96,11 +102,30 @@ var CreditAccountOperationsService = class extends SDKConstruct {
|
|
|
96
102
|
}
|
|
97
103
|
});
|
|
98
104
|
if (!result.ok) return result;
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
const { delayed } = result;
|
|
106
|
+
if (!delayed) throw new Error("startDelayedIntent: plan started no withdrawal");
|
|
107
|
+
if (delayed.settlement === "instant") return {
|
|
101
108
|
...result,
|
|
102
|
-
delayed
|
|
109
|
+
delayed
|
|
103
110
|
};
|
|
111
|
+
const request = result.operations.find((op) => op.type === "startDelayedWithdrawal");
|
|
112
|
+
if (!request) throw new Error("startDelayedIntent: no request among the operations");
|
|
113
|
+
try {
|
|
114
|
+
const tail = await projectTail({
|
|
115
|
+
request,
|
|
116
|
+
delayed,
|
|
117
|
+
creditAccount: props.creditAccount,
|
|
118
|
+
sdk: props.sdk,
|
|
119
|
+
quotaReserve: props.quotaReserve
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
...result,
|
|
123
|
+
preview: tail.state,
|
|
124
|
+
delayed
|
|
125
|
+
};
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return asFailure(e);
|
|
128
|
+
}
|
|
104
129
|
}
|
|
105
130
|
/**
|
|
106
131
|
* Previews a delayable intent both ways at once: settled by the router now,
|
|
@@ -149,9 +174,10 @@ var CreditAccountOperationsService = class extends SDKConstruct {
|
|
|
149
174
|
* Previews the tail of a delayed intent, once the withdrawal it started has
|
|
150
175
|
* matured: the claim, then whatever the intent still owes.
|
|
151
176
|
*
|
|
152
|
-
* Serves the
|
|
153
|
-
* withdrawal and
|
|
154
|
-
* the tokens land on the account and only their quota has to
|
|
177
|
+
* Serves the three operations that can genuinely be interrupted by a delay —
|
|
178
|
+
* a withdrawal, a deleveraging and an exit. For the rest, the claim is the
|
|
179
|
+
* whole tail: the tokens land on the account and only their quota has to
|
|
180
|
+
* catch up.
|
|
155
181
|
*
|
|
156
182
|
* @param props - The recorded intent, the account slice as it stands now, and
|
|
157
183
|
* the matured claimable
|
|
@@ -159,24 +185,11 @@ var CreditAccountOperationsService = class extends SDKConstruct {
|
|
|
159
185
|
* an operation are consumed the same way
|
|
160
186
|
*/
|
|
161
187
|
async finishIntent(props) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!output) throw new IntentPreviewError("insufficientSourceBalance", "finishIntent: the claim credits nothing to spend");
|
|
168
|
-
return output;
|
|
169
|
-
};
|
|
170
|
-
switch (intent.type) {
|
|
171
|
-
case "WITHDRAW_COLLATERAL": return planFinishWithdraw(intent, claimable, claimed(), view);
|
|
172
|
-
case "DECREASE_LEVERAGE": return planFinishDecreaseLeverage(claimable, claimed(), view);
|
|
173
|
-
case "ADD_COLLATERAL":
|
|
174
|
-
case "INCREASE_LEVERAGE":
|
|
175
|
-
case "DEPOSIT":
|
|
176
|
-
case "DEPOSIT_AND_INCREASE_LEVERAGE": return planFinishClaimOnly(claimable);
|
|
177
|
-
default: throw new Error(`${intent.type} - not implemented`);
|
|
178
|
-
}
|
|
179
|
-
}));
|
|
188
|
+
return plain(await this.#preview(props, () => planTail({
|
|
189
|
+
intent: props.intent,
|
|
190
|
+
claimable: props.claimable,
|
|
191
|
+
view: accountView(props.creditAccount, props.sdk)
|
|
192
|
+
})));
|
|
180
193
|
}
|
|
181
194
|
/**
|
|
182
195
|
* Previews opening a brand-new leveraged position.
|
|
@@ -239,7 +252,23 @@ function asFailure(e) {
|
|
|
239
252
|
ok: false,
|
|
240
253
|
reason: e.reason
|
|
241
254
|
};
|
|
255
|
+
if (isUnroutable(e)) return {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: "unsupportedTokenPair"
|
|
258
|
+
};
|
|
242
259
|
throw e;
|
|
243
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* How the pathfinder says there is no route: it reverts instead of answering
|
|
263
|
+
* with an empty path, so viem raises a contract error where the rest of the
|
|
264
|
+
* engine raises an {@link IntentPreviewError}. Nothing is wrong — the trade
|
|
265
|
+
* asked for cannot be made, which is a refusal the caller can act on, and one
|
|
266
|
+
* `intentRoutes` in particular must keep as a value so the other route can
|
|
267
|
+
* still be offered.
|
|
268
|
+
*/
|
|
269
|
+
function isUnroutable(e) {
|
|
270
|
+
for (let cause = e; cause instanceof Error; cause = cause.cause) if (cause.message.includes("no optimal edge found")) return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
244
273
|
//#endregion
|
|
245
274
|
export { CreditAccountOperationsService, IntentPreviewError, fetchCreditAccountSlice, toCreditAccountSlice };
|
|
@@ -114,11 +114,25 @@ function planWithdraw(intent, view) {
|
|
|
114
114
|
*
|
|
115
115
|
* The tail is planned at claim time by {@link planFinishWithdraw}, from the
|
|
116
116
|
* intent this request records — only then is the claimed amount, and the token
|
|
117
|
-
* it arrived in, known.
|
|
117
|
+
* it arrived in, known. An exit records {@link planFinishCloseAccount}'s intent
|
|
118
|
+
* instead, and rebuilds itself from the account rather than from the request.
|
|
118
119
|
*/
|
|
119
120
|
function planWithdrawDelayed(intent, view) {
|
|
120
121
|
const { U, T, S, WU, dD, all } = withdrawShape(intent, view);
|
|
121
|
-
if (all)
|
|
122
|
+
if (all) {
|
|
123
|
+
const held = view.balanceOf(S);
|
|
124
|
+
if (held <= 0n) throw new IntentPreviewError("insufficientSourceBalance", `withdraw: account holds no ${S} to redeem`);
|
|
125
|
+
return [{
|
|
126
|
+
kind: "request",
|
|
127
|
+
token: S,
|
|
128
|
+
amount: held,
|
|
129
|
+
reserve: 0n,
|
|
130
|
+
record: {
|
|
131
|
+
type: "CLOSE_ACCOUNT",
|
|
132
|
+
to: intent.to
|
|
133
|
+
}
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
122
136
|
if (!eq(T, U) && !(view.rwaAsset && eq(T, view.rwaAsset))) throw new IntentPreviewError("noDelayedRoute", `withdraw: a delayed route cannot pay out in ${T}`);
|
|
123
137
|
const payoutIsSource = eq(T, S);
|
|
124
138
|
return [{
|
|
@@ -217,6 +231,29 @@ function planFinishWithdraw(intent, claimable, claimed, view) {
|
|
|
217
231
|
}, intent.to)
|
|
218
232
|
];
|
|
219
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* The tail of an exit: the claim lands, everything the account holds is sold
|
|
236
|
+
* into the underlying, the loan is settled out of the proceeds and the rest
|
|
237
|
+
* goes to the wallet. The account survives it, empty and owing nothing.
|
|
238
|
+
*
|
|
239
|
+
* Nothing is quoted from the request — the same shape {@link planWithdraw}
|
|
240
|
+
* builds for an instant exit is rebuilt here against the account as it stands
|
|
241
|
+
* now, which is the only state that can name these amounts.
|
|
242
|
+
*/
|
|
243
|
+
function planFinishCloseAccount(intent, claimable, claimed, view) {
|
|
244
|
+
const wrap = view.rwaAsset && eq(claimed.token, view.rwaAsset) ? [convert(claimed.token, view.underlying, view.balanceOf(claimed.token) + claimed.amount)] : [];
|
|
245
|
+
return [
|
|
246
|
+
claim(claimable),
|
|
247
|
+
...wrap,
|
|
248
|
+
clearQuotas(),
|
|
249
|
+
{ kind: "closeAll" },
|
|
250
|
+
...view.debt > 0n ? [repay(view.debt)] : [],
|
|
251
|
+
{
|
|
252
|
+
kind: "sweep",
|
|
253
|
+
to: intent.to
|
|
254
|
+
}
|
|
255
|
+
];
|
|
256
|
+
}
|
|
220
257
|
/** Nothing is owed beyond the claim: the tokens land and quotas catch up. */
|
|
221
258
|
function planFinishClaimOnly(claimable) {
|
|
222
259
|
return [claim(claimable)];
|
|
@@ -358,4 +395,4 @@ function assertPositive(amount, flow) {
|
|
|
358
395
|
if (amount <= 0n) throw new IntentPreviewError("insufficientSourceBalance", `${flow}: amount must be positive`);
|
|
359
396
|
}
|
|
360
397
|
//#endregion
|
|
361
|
-
export { RAISED, planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planFinishClaimOnly, planFinishDecreaseLeverage, planFinishWithdraw, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed };
|
|
398
|
+
export { RAISED, planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planFinishClaimOnly, planFinishCloseAccount, planFinishDecreaseLeverage, planFinishWithdraw, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed };
|
|
@@ -24,7 +24,7 @@ async function realize(steps, props) {
|
|
|
24
24
|
const { underlying } = creditAccount;
|
|
25
25
|
const rwaAsset = sdk.tokensMeta.rwaUnderlyings.get(underlying)?.asset;
|
|
26
26
|
const price = convertAmount(sdk, creditAccount.creditManager);
|
|
27
|
-
const paths = createRouterPaths({
|
|
27
|
+
const paths = props.paths ?? createRouterPaths({
|
|
28
28
|
sdk,
|
|
29
29
|
creditAccount,
|
|
30
30
|
slippage
|
|
@@ -44,6 +44,7 @@ async function realize(steps, props) {
|
|
|
44
44
|
};
|
|
45
45
|
/** Output of the last convert or claim, for `RAISED` amounts. */
|
|
46
46
|
let raised = 0n;
|
|
47
|
+
/** The request, before the walk's end state can be attached to it. */
|
|
47
48
|
let delayed;
|
|
48
49
|
/**
|
|
49
50
|
* Set by a `clearQuotas` step, which settles the quotas mid-walk instead of
|
|
@@ -145,7 +146,7 @@ async function realize(steps, props) {
|
|
|
145
146
|
if (pending) throw new IntentPreviewError("withdrawalInProgress", `closeAll: ${pending.token} is a pending withdrawal, claim it first`);
|
|
146
147
|
if (balances.length > 0) {
|
|
147
148
|
const leg = await paths.closeAll({ balances });
|
|
148
|
-
if (leg.calls.length > 0) push(buildCloseSwapOperation({
|
|
149
|
+
if (leg.calls.length > 0 || leg.minAmount > 0n) push(buildCloseSwapOperation({
|
|
149
150
|
from: balances,
|
|
150
151
|
tokenOut: underlying,
|
|
151
152
|
amountOut: leg.minAmount,
|
|
@@ -184,10 +185,15 @@ async function realize(steps, props) {
|
|
|
184
185
|
creditAccount,
|
|
185
186
|
sdk
|
|
186
187
|
}));
|
|
188
|
+
const queued = preview.outputs.find((o) => o.isDelayed);
|
|
187
189
|
delayed = {
|
|
188
190
|
record: step.record,
|
|
189
191
|
claimableAt: preview.claimableAt,
|
|
190
|
-
settlement:
|
|
192
|
+
settlement: queued ? "delayed" : "instant",
|
|
193
|
+
claim: queued ? {
|
|
194
|
+
token: asset.underlying.toLowerCase(),
|
|
195
|
+
amount: toTargetDecimals(queued.amount, queued.token, asset.underlying, sdk)
|
|
196
|
+
} : void 0
|
|
191
197
|
};
|
|
192
198
|
raised = instantOutput(preview.outputs)?.amount ?? 0n;
|
|
193
199
|
break;
|
|
@@ -265,18 +271,22 @@ async function realize(steps, props) {
|
|
|
265
271
|
liquidationPrice: sdk.positions.liquidationPrice(snapshot)
|
|
266
272
|
};
|
|
267
273
|
assertCollateralised(paysOut ? sdk.positions.healthFactor(snapshot, { safePrices: true }) : metrics.healthFactor);
|
|
274
|
+
const state = {
|
|
275
|
+
totalValue,
|
|
276
|
+
accountDebt: debt,
|
|
277
|
+
leverage: calcPositionLeverage(totalValue, debt),
|
|
278
|
+
assets,
|
|
279
|
+
quotas: quotasAfter,
|
|
280
|
+
...metrics
|
|
281
|
+
};
|
|
268
282
|
return {
|
|
269
283
|
operations,
|
|
270
|
-
state
|
|
271
|
-
totalValue,
|
|
272
|
-
accountDebt: debt,
|
|
273
|
-
leverage: calcPositionLeverage(totalValue, debt),
|
|
274
|
-
assets,
|
|
275
|
-
quotas: quotasAfter,
|
|
276
|
-
...metrics
|
|
277
|
-
},
|
|
284
|
+
state,
|
|
278
285
|
calls: callsOf(operations),
|
|
279
|
-
delayed
|
|
286
|
+
delayed: delayed && {
|
|
287
|
+
...delayed,
|
|
288
|
+
afterRequest: state
|
|
289
|
+
}
|
|
280
290
|
};
|
|
281
291
|
}
|
|
282
292
|
/**
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { IntentPreviewError } from "./types.js";
|
|
2
|
+
import { createOraclePaths } from "./utils/router-path.js";
|
|
3
|
+
import { planFinishClaimOnly, planFinishCloseAccount, planFinishDecreaseLeverage, planFinishWithdraw } from "./plan.js";
|
|
4
|
+
import { instantOutput } from "./operations.js";
|
|
5
|
+
import { realize } from "./realize.js";
|
|
6
|
+
import { accountView } from "./view.js";
|
|
7
|
+
//#region src/sdk/accounts/intents/tail.ts
|
|
8
|
+
/**
|
|
9
|
+
* The second half of a delayed intent: the claim, then whatever the intent
|
|
10
|
+
* still owes.
|
|
11
|
+
*
|
|
12
|
+
* Shared by the two callers that need it and must not disagree — the tail as
|
|
13
|
+
* it is previewed days later against the account that really exists, and the
|
|
14
|
+
* tail as it is projected the moment the request is made.
|
|
15
|
+
*/
|
|
16
|
+
function planTail(args) {
|
|
17
|
+
const { intent, claimable, view } = args;
|
|
18
|
+
const claimed = () => {
|
|
19
|
+
const output = instantOutput(claimable.outputs);
|
|
20
|
+
if (!output) throw new IntentPreviewError("insufficientSourceBalance", "finishIntent: the claim credits nothing to spend");
|
|
21
|
+
return output;
|
|
22
|
+
};
|
|
23
|
+
switch (intent.type) {
|
|
24
|
+
case "WITHDRAW_COLLATERAL": return planFinishWithdraw(intent, claimable, claimed(), view);
|
|
25
|
+
case "DECREASE_LEVERAGE": return planFinishDecreaseLeverage(claimable, claimed(), view);
|
|
26
|
+
case "CLOSE_ACCOUNT": return planFinishCloseAccount(intent, claimable, claimed(), view);
|
|
27
|
+
case "ADD_COLLATERAL":
|
|
28
|
+
case "INCREASE_LEVERAGE":
|
|
29
|
+
case "DEPOSIT":
|
|
30
|
+
case "DEPOSIT_AND_INCREASE_LEVERAGE": return planFinishClaimOnly(claimable);
|
|
31
|
+
default: throw new Error(`${intent.type} - not implemented`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Where a delayed intent ends up, worked out at the moment it is started.
|
|
36
|
+
*
|
|
37
|
+
* A request is only half a withdrawal, so the state it lands in is not the
|
|
38
|
+
* answer to "what does this do to my position": the debt is still there, the
|
|
39
|
+
* payout has not been made, and the position sits in a phantom token. What the
|
|
40
|
+
* caller means is the far side — and that side can be walked now, because the
|
|
41
|
+
* request already fixes the claim it will be finished from.
|
|
42
|
+
*
|
|
43
|
+
* So the same tail {@link planTail} builds at claim time is built here against
|
|
44
|
+
* the account as the request leaves it, with the claim it is expected to bring,
|
|
45
|
+
* and walked by the same realiser — with one substitution: routed legs are
|
|
46
|
+
* priced by the oracle rather than the pathfinder, since the funds they trade
|
|
47
|
+
* do not exist yet and no calldata is being produced. The result is an
|
|
48
|
+
* estimate that the engine's guards are nevertheless applied to, so a request
|
|
49
|
+
* that would strand the account is refused before it is sent rather than
|
|
50
|
+
* discovered days later.
|
|
51
|
+
*/
|
|
52
|
+
async function projectTail(args) {
|
|
53
|
+
const { request, delayed, creditAccount, sdk, quotaReserve } = args;
|
|
54
|
+
const { claim } = delayed;
|
|
55
|
+
const queued = request.outputs.find((o) => o.isDelayed);
|
|
56
|
+
if (!queued || !claim) throw new Error("projectTail: the request queued nothing to claim");
|
|
57
|
+
const next = sliceAfter(creditAccount, delayed.afterRequest);
|
|
58
|
+
const steps = planTail({
|
|
59
|
+
intent: delayed.record,
|
|
60
|
+
claimable: projectedClaimable(request, queued.token, queued.amount, claim),
|
|
61
|
+
view: accountView(next, sdk)
|
|
62
|
+
});
|
|
63
|
+
const { state, operations } = await realize(steps, {
|
|
64
|
+
creditAccount: next,
|
|
65
|
+
sdk,
|
|
66
|
+
slippage: 0,
|
|
67
|
+
quotaReserve,
|
|
68
|
+
paths: createOraclePaths({
|
|
69
|
+
sdk,
|
|
70
|
+
creditAccount: next
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
state,
|
|
75
|
+
operations
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The matured withdrawal the tail will be built from, as the request implies
|
|
80
|
+
* it: the phantom it created is burned and the venue's payout takes its place.
|
|
81
|
+
* It carries no claim calls — those are read from the chain when the claim is
|
|
82
|
+
* real, and nothing here is going to be sent.
|
|
83
|
+
*/
|
|
84
|
+
function projectedClaimable(request, phantom, phantomAmount, claim) {
|
|
85
|
+
return {
|
|
86
|
+
token: request.token,
|
|
87
|
+
withdrawalPhantomToken: phantom,
|
|
88
|
+
withdrawalTokenSpent: phantomAmount,
|
|
89
|
+
outputs: [{
|
|
90
|
+
token: claim.token,
|
|
91
|
+
amount: claim.amount,
|
|
92
|
+
isDelayed: false
|
|
93
|
+
}],
|
|
94
|
+
claimCalls: []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The account as one walk left it, shaped as the slice the next walk reads.
|
|
99
|
+
* Masks are carried over from the tokens the account already held; one it
|
|
100
|
+
* picked up along the way has none to carry, which only the router slice
|
|
101
|
+
* would have used.
|
|
102
|
+
*/
|
|
103
|
+
function sliceAfter(creditAccount, after) {
|
|
104
|
+
const masks = new Map(creditAccount.tokens.map((t) => [t.token.toLowerCase(), t.mask]));
|
|
105
|
+
return {
|
|
106
|
+
...creditAccount,
|
|
107
|
+
accountDebt: after.accountDebt,
|
|
108
|
+
tokens: after.assets.map((a) => ({
|
|
109
|
+
token: a.token,
|
|
110
|
+
balance: a.balance,
|
|
111
|
+
quota: after.quotas[a.token]?.balance ?? 0n,
|
|
112
|
+
mask: masks.get(a.token) ?? 0n,
|
|
113
|
+
success: true
|
|
114
|
+
}))
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { planTail, projectTail };
|
|
@@ -7,5 +7,5 @@ import { calcBorrowedAmountPlusInterestAndFees } from "./borrowed-amount-plus-in
|
|
|
7
7
|
import { fetchCreditAccountSlice, toCreditAccountSlice } from "./credit-account-slice.js";
|
|
8
8
|
import { OperationLedger } from "./ledger.js";
|
|
9
9
|
import { clearedQuotas, getQuotasForUpdate, quotasAfterUpdate } from "./quotas-for-update.js";
|
|
10
|
-
import { createRouterPaths } from "./router-path.js";
|
|
11
|
-
export { OperationLedger, adjustStateToSnapshot, assembleOperationCalls, calcBorrowedAmountPlusInterestAndFees, clearedQuotas, convertAmount, createRouterPaths, eq, fetchCreditAccountSlice, getQuotasForUpdate, isPhantomToken, isRedemptionPhantomToken, pickFattestNonPhantomToken, quotasAfterUpdate, rankAccountTokens, toCreditAccountSlice, toRouterCaSlice, toTargetDecimals };
|
|
10
|
+
import { createOraclePaths, createRouterPaths } from "./router-path.js";
|
|
11
|
+
export { OperationLedger, adjustStateToSnapshot, assembleOperationCalls, calcBorrowedAmountPlusInterestAndFees, clearedQuotas, convertAmount, createOraclePaths, createRouterPaths, eq, fetchCreditAccountSlice, getQuotasForUpdate, isPhantomToken, isRedemptionPhantomToken, pickFattestNonPhantomToken, quotasAfterUpdate, rankAccountTokens, toCreditAccountSlice, toRouterCaSlice, toTargetDecimals };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toRouterCaSlice } from "./common.js";
|
|
2
|
+
import { convertAmount } from "./convert-amount.js";
|
|
2
3
|
//#region src/sdk/accounts/intents/utils/router-path.ts
|
|
3
4
|
/**
|
|
4
5
|
* The engine's only door to the pathfinder.
|
|
@@ -81,5 +82,35 @@ function createRouterPaths(args) {
|
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The same door, priced by the oracle and opening onto no calldata.
|
|
87
|
+
*
|
|
88
|
+
* For a leg that cannot be quoted yet: the tail of a redemption trades funds
|
|
89
|
+
* that do not exist, along a route the pathfinder will only be able to build
|
|
90
|
+
* once they do. Asking it now would price a swap of nothing, so the amounts
|
|
91
|
+
* come from the oracle instead — an estimate with no slippage floor, which is
|
|
92
|
+
* all a projection days out can honestly be — and the walk yields a state
|
|
93
|
+
* rather than a transaction.
|
|
94
|
+
*/
|
|
95
|
+
function createOraclePaths(args) {
|
|
96
|
+
const { sdk, creditAccount } = args;
|
|
97
|
+
const price = convertAmount(sdk, creditAccount.creditManager);
|
|
98
|
+
const estimate = (amount) => ({
|
|
99
|
+
amount,
|
|
100
|
+
minAmount: amount,
|
|
101
|
+
calls: []
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
async swap({ tokenIn, tokenOut, amount }) {
|
|
105
|
+
return estimate(amount > 0n ? price(tokenIn, tokenOut, amount) : 0n);
|
|
106
|
+
},
|
|
107
|
+
async closeAll({ balances }) {
|
|
108
|
+
return estimate(balances.reduce((sum, b) => sum + price(b.token, creditAccount.underlying, b.balance), 0n));
|
|
109
|
+
},
|
|
110
|
+
async openStrategy() {
|
|
111
|
+
throw new Error("oracle paths: opening a position is never projected");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
84
115
|
//#endregion
|
|
85
|
-
export { createRouterPaths };
|
|
116
|
+
export { createOraclePaths, createRouterPaths };
|