@gearbox-protocol/sdk 16.4.0-next.5 → 16.4.0-next.7
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/onchain/accounts/intents/borrow.js +139 -0
- package/dist/cjs/onchain/accounts/intents/{collateral-money.js → collateral-valuation.js} +7 -7
- package/dist/cjs/onchain/accounts/intents/index.js +82 -1
- package/dist/cjs/onchain/accounts/intents/leverage-band.js +2 -12
- package/dist/cjs/onchain/accounts/intents/maxBorrow.js +89 -0
- package/dist/cjs/onchain/accounts/intents/maxWithdrawCollateral.js +14 -14
- package/dist/cjs/onchain/accounts/intents/open-strategy.js +6 -45
- package/dist/cjs/onchain/accounts/intents/testing/sdk-mock.js +1 -0
- package/dist/cjs/onchain/accounts/intents/utils/common.js +19 -0
- package/dist/cjs/onchain/accounts/intents/utils/credit-account-slice.js +21 -0
- package/dist/cjs/onchain/accounts/intents/utils/index.js +2 -0
- package/dist/cjs/onchain/accounts/intents/withdraw-limits.js +8 -8
- package/dist/cjs/onchain/positions/calcHealthFactor.js +3 -3
- package/dist/cjs/onchain/preview/preview/previewOpenStrategyPosition.js +2 -1
- package/dist/cjs/sdk/execute/ExecuteApi.js +65 -5
- package/dist/cjs/sdk/prepare/PrepareApi.js +86 -12
- package/dist/esm/onchain/accounts/intents/borrow.js +137 -0
- package/dist/esm/onchain/accounts/intents/{collateral-money.js → collateral-valuation.js} +7 -7
- package/dist/esm/onchain/accounts/intents/index.js +82 -1
- package/dist/esm/onchain/accounts/intents/leverage-band.js +2 -12
- package/dist/esm/onchain/accounts/intents/maxBorrow.js +87 -0
- package/dist/esm/onchain/accounts/intents/maxWithdrawCollateral.js +14 -14
- package/dist/esm/onchain/accounts/intents/open-strategy.js +6 -45
- package/dist/esm/onchain/accounts/intents/testing/sdk-mock.js +1 -0
- package/dist/esm/onchain/accounts/intents/utils/common.js +19 -1
- package/dist/esm/onchain/accounts/intents/utils/credit-account-slice.js +21 -1
- package/dist/esm/onchain/accounts/intents/utils/index.js +3 -3
- package/dist/esm/onchain/accounts/intents/withdraw-limits.js +8 -8
- package/dist/esm/onchain/positions/calcHealthFactor.js +3 -3
- package/dist/esm/onchain/preview/preview/previewOpenStrategyPosition.js +2 -1
- package/dist/esm/sdk/execute/ExecuteApi.js +65 -5
- package/dist/esm/sdk/prepare/PrepareApi.js +86 -12
- package/dist/types/model/previews.d.ts +11 -2
- package/dist/types/onchain/accounts/index.d.ts +4 -3
- package/dist/types/onchain/accounts/intents/borrow.d.ts +141 -0
- package/dist/types/onchain/accounts/intents/{collateral-money.d.ts → collateral-valuation.d.ts} +8 -8
- package/dist/types/onchain/accounts/intents/index.d.ts +81 -2
- package/dist/types/onchain/accounts/intents/maxBorrow.d.ts +54 -0
- package/dist/types/onchain/accounts/intents/maxWithdrawCollateral.d.ts +3 -3
- package/dist/types/onchain/accounts/intents/open-strategy.d.ts +3 -17
- package/dist/types/onchain/accounts/intents/testing/sdk-mock.d.ts +2 -0
- package/dist/types/onchain/accounts/intents/tests/open-strategy.fixtures.d.ts +2 -2
- package/dist/types/onchain/accounts/intents/types.d.ts +6 -1
- package/dist/types/onchain/accounts/intents/utils/common.d.ts +15 -1
- package/dist/types/onchain/accounts/intents/utils/credit-account-slice.d.ts +14 -1
- package/dist/types/onchain/accounts/intents/utils/index.d.ts +3 -3
- package/dist/types/onchain/accounts/intents/withdraw-limits.d.ts +1 -1
- package/dist/types/onchain/index.d.ts +4 -3
- package/dist/types/sdk/execute/index.d.ts +2 -2
- package/dist/types/sdk/execute/types.d.ts +48 -6
- package/dist/types/sdk/index.d.ts +5 -4
- package/dist/types/sdk/prepare/PrepareApi.d.ts +15 -3
- package/dist/types/sdk/prepare/index.d.ts +4 -3
- package/dist/types/sdk/prepare/types.d.ts +176 -49
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BigIntMath } from "../../utils/bigint-math.js";
|
|
2
|
+
import "../../constants/math.js";
|
|
3
|
+
import "../../utils/index.js";
|
|
4
|
+
import { eq, resolveCreditManager, toTargetDecimals } from "./utils/common.js";
|
|
5
|
+
import { unopenedAccountSlice } from "./utils/credit-account-slice.js";
|
|
6
|
+
import "./utils/index.js";
|
|
7
|
+
import { borrowCollateralQuota } from "./borrow.js";
|
|
8
|
+
import { collateralValuation } from "./collateral-valuation.js";
|
|
9
|
+
//#region src/onchain/accounts/intents/maxBorrow.ts
|
|
10
|
+
/**
|
|
11
|
+
* Largest loan this collateral supports at `targetHF` — the ceiling a borrow
|
|
12
|
+
* form should offer, in the payout token's units.
|
|
13
|
+
*
|
|
14
|
+
* The inverse of a borrow rather than a search for one: the loan leaves the
|
|
15
|
+
* account entirely, so the collateral is the whole of what backs the debt, and
|
|
16
|
+
* the health factor is one division away from the amount. Solving it the other
|
|
17
|
+
* way round costs a division too, and no iteration.
|
|
18
|
+
*
|
|
19
|
+
* Collateral is valued the way the transaction will be judged — at safe
|
|
20
|
+
* prices, under its liquidation threshold, capped by the quota the borrow
|
|
21
|
+
* buys for it, all of which is {@link collateralValuation}'s business. The ceiling
|
|
22
|
+
* is then held to what the market will actually lend: the pool's free
|
|
23
|
+
* liquidity, the manager's own allowance and the facade's `maxDebt`, whichever
|
|
24
|
+
* binds first.
|
|
25
|
+
*
|
|
26
|
+
* The facade's `minDebt` is deliberately not applied. It is a floor, and a
|
|
27
|
+
* ceiling answered as `0n` because the collateral is too small for this market
|
|
28
|
+
* would tell a form nothing about what it is holding — the number a user needs
|
|
29
|
+
* to see is the one they are short of. Collateral that carries something
|
|
30
|
+
* therefore answers with it, whether or not the market would lend that little;
|
|
31
|
+
* a loan under the floor is refused by `borrow` itself, with `debtOutOfRange`
|
|
32
|
+
* naming both ends.
|
|
33
|
+
*
|
|
34
|
+
* Nothing is fetched or simulated — the account does not exist yet and every
|
|
35
|
+
* input is loaded market state, so a form can call this on each keystroke.
|
|
36
|
+
*
|
|
37
|
+
* @param props - {@link MaxBorrowProps}
|
|
38
|
+
* @returns Amount in the payout token's units; `0n` where no loan of this
|
|
39
|
+
* shape exists at any size — a collateral that backs nothing at safe prices, a
|
|
40
|
+
* market with nothing left to lend, and a manager the SDK does not hold yet
|
|
41
|
+
**/
|
|
42
|
+
function maxBorrow(props) {
|
|
43
|
+
const { sdk, creditManager, collateralAmount, targetHF, quotaReserve } = props;
|
|
44
|
+
const found = resolveCreditManager(sdk, creditManager);
|
|
45
|
+
if (!found) return 0n;
|
|
46
|
+
const { suite, market } = found;
|
|
47
|
+
const { priceOracle } = market;
|
|
48
|
+
const underlying = market.pool.underlying.toLowerCase();
|
|
49
|
+
const collateralToken = props.collateralToken.toLowerCase();
|
|
50
|
+
const borrowToken = props.borrowToken.toLowerCase();
|
|
51
|
+
const rwaAsset = sdk.tokensMeta.rwaUnderlyings.get(underlying)?.asset?.toLowerCase();
|
|
52
|
+
if (eq(collateralToken, borrowToken)) return 0n;
|
|
53
|
+
if (rwaAsset && eq(borrowToken, underlying)) return 0n;
|
|
54
|
+
if (collateralAmount <= 10n || targetHF <= 0n) return 0n;
|
|
55
|
+
const quotas = borrowCollateralQuota({
|
|
56
|
+
sdk,
|
|
57
|
+
creditManager,
|
|
58
|
+
assets: [{
|
|
59
|
+
token: collateralToken,
|
|
60
|
+
balance: collateralAmount
|
|
61
|
+
}],
|
|
62
|
+
quotaReserve
|
|
63
|
+
});
|
|
64
|
+
const holding = {
|
|
65
|
+
token: collateralToken,
|
|
66
|
+
balance: collateralAmount,
|
|
67
|
+
quota: quotas.find((q) => eq(q.token, collateralToken))?.balance ?? 0n,
|
|
68
|
+
mask: 0n,
|
|
69
|
+
success: true
|
|
70
|
+
};
|
|
71
|
+
const valuation = collateralValuation({
|
|
72
|
+
...unopenedAccountSlice({
|
|
73
|
+
creditManager,
|
|
74
|
+
creditFacade: suite.creditFacade.address,
|
|
75
|
+
underlying
|
|
76
|
+
}),
|
|
77
|
+
tokens: [holding]
|
|
78
|
+
}, sdk);
|
|
79
|
+
const weighted = valuation.checkedUsd(holding) * valuation.lt(collateralToken);
|
|
80
|
+
const backed = quotas.some((q) => eq(q.token, collateralToken)) ? BigIntMath.min(valuation.quotaValue(holding), weighted) : weighted;
|
|
81
|
+
if (backed <= 0n) return 0n;
|
|
82
|
+
const ceiling = BigIntMath.min(priceOracle.safeConvertFromUSD(underlying, backed / targetHF).value, suite.maxBorrowAmount().amount.value);
|
|
83
|
+
const unwrapsPayout = !!rwaAsset && eq(borrowToken, rwaAsset);
|
|
84
|
+
return eq(borrowToken, underlying) ? ceiling : unwrapsPayout ? toTargetDecimals(ceiling, underlying, borrowToken, sdk) : priceOracle.safeConvert(underlying, borrowToken, ceiling).value;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { maxBorrow };
|
|
@@ -2,16 +2,16 @@ import { BigIntMath } from "../../utils/bigint-math.js";
|
|
|
2
2
|
import "../../constants/math.js";
|
|
3
3
|
import "../../utils/index.js";
|
|
4
4
|
import { eq } from "./utils/common.js";
|
|
5
|
-
import {
|
|
5
|
+
import { collateralValuation } from "./collateral-valuation.js";
|
|
6
6
|
//#region src/onchain/accounts/intents/maxWithdrawCollateral.ts
|
|
7
7
|
/**
|
|
8
8
|
* Largest amount of one collateral the account can withdraw while its health
|
|
9
9
|
* factor stays at or above `targetHF`.
|
|
10
10
|
*
|
|
11
11
|
* This is the collateral check solved for one balance, and it counts what that
|
|
12
|
-
* check counts — see {@link
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* check counts — see {@link collateralValuation} for it, safe prices included.
|
|
13
|
+
* The debt is valued at the main feed, as the check does. Zero debt frees the
|
|
14
|
+
* whole balance.
|
|
15
15
|
*
|
|
16
16
|
* Rounding always favours the account, so the answer clears the check rather
|
|
17
17
|
* than landing a wei short of it.
|
|
@@ -25,20 +25,20 @@ function maxWithdrawCollateral(props) {
|
|
|
25
25
|
const target = creditAccount.tokens.find((t) => eq(t.token, token));
|
|
26
26
|
if (!target || target.balance <= 10n) return 0n;
|
|
27
27
|
if (creditAccount.totalDebt === 0n) return target.balance;
|
|
28
|
-
const
|
|
29
|
-
let
|
|
28
|
+
const valuation = collateralValuation(creditAccount, sdk);
|
|
29
|
+
let otherValue = 0n;
|
|
30
30
|
for (const t of creditAccount.tokens) {
|
|
31
|
-
if (eq(t.token, token) || !
|
|
32
|
-
|
|
31
|
+
if (eq(t.token, token) || !valuation.counts(t)) continue;
|
|
32
|
+
otherValue += valuation.weigh(t);
|
|
33
33
|
}
|
|
34
|
-
const borrowed =
|
|
34
|
+
const borrowed = valuation.mainUsd(valuation.underlying, creditAccount.totalDebt);
|
|
35
35
|
if (borrowed === void 0 || borrowed <= 0n) return 0n;
|
|
36
36
|
const required = borrowed * targetHF;
|
|
37
|
-
if (required <=
|
|
38
|
-
const shortfall = required -
|
|
39
|
-
if (target.quota > 0n &&
|
|
40
|
-
const targetLt =
|
|
41
|
-
const targetUsd =
|
|
37
|
+
if (required <= otherValue) return target.balance;
|
|
38
|
+
const shortfall = required - otherValue;
|
|
39
|
+
if (target.quota > 0n && valuation.quotaValue(target) < shortfall) return 0n;
|
|
40
|
+
const targetLt = valuation.lt(target.token);
|
|
41
|
+
const targetUsd = valuation.checkedUsd(target);
|
|
42
42
|
if (targetLt === 0n || targetUsd === 0n) return 0n;
|
|
43
43
|
const keptUsd = BigIntMath.ceilDiv(shortfall, targetLt);
|
|
44
44
|
const kept = BigIntMath.ceilDiv(target.balance * keptUsd, targetUsd);
|
|
@@ -3,17 +3,13 @@ import "../../../model/index.js";
|
|
|
3
3
|
import { IntentPreviewError } from "../../validation/raise.js";
|
|
4
4
|
import { assertCanBorrow, assertCollateralised, assertGrowthAllowed, assertMarketOperable, assertQuotaAvailable } from "./guards.js";
|
|
5
5
|
import { assertDebtLimits, assertLeverageAtLeastOne, debtForLeverage } from "./math.js";
|
|
6
|
+
import { unopenedAccountSlice } from "./utils/credit-account-slice.js";
|
|
6
7
|
import { collectPriceImpact } from "./utils/price-impact.js";
|
|
7
8
|
import { getQuotasForUpdate } from "./utils/quotas-for-update.js";
|
|
8
9
|
import { createRouterPaths } from "./utils/router-path.js";
|
|
9
10
|
import "./utils/index.js";
|
|
10
11
|
//#region src/onchain/accounts/intents/open-strategy.ts
|
|
11
12
|
/**
|
|
12
|
-
* Stand-in account address, used when the opening creates its own account:
|
|
13
|
-
* nothing exists on chain until the tx lands.
|
|
14
|
-
*/
|
|
15
|
-
const NO_ACCOUNT = "0x0000000000000000000000000000000000000000";
|
|
16
|
-
/**
|
|
17
13
|
* Builds the state opening a leveraged position out of wallet collateral would
|
|
18
14
|
* reach.
|
|
19
15
|
*
|
|
@@ -26,7 +22,6 @@ const NO_ACCOUNT = "0x0000000000000000000000000000000000000000";
|
|
|
26
22
|
* `sdk.accounts.openCA`.
|
|
27
23
|
*/
|
|
28
24
|
async function buildOpenStrategyState(props) {
|
|
29
|
-
if (props.empty) return emptyOpenState(props);
|
|
30
25
|
const { sdk, creditManager, collateral, targetToken, leverage, slippage = 0, quotaReserve, leftoverBalances = [], creditAccount: existing } = props;
|
|
31
26
|
assertLeverageAtLeastOne(leverage);
|
|
32
27
|
const suite = sdk.marketRegister.findCreditManager(creditManager);
|
|
@@ -37,16 +32,11 @@ async function buildOpenStrategyState(props) {
|
|
|
37
32
|
const margin = collateral.reduce((acc, a) => acc + convert(a.token, underlying, a.balance), 0n);
|
|
38
33
|
if (margin <= 0n) throw new IntentPreviewError(insufficientBalance(), "openStrategy: collateral is worth nothing in underlying");
|
|
39
34
|
const debt = debtForLeverage(margin, leverage);
|
|
40
|
-
const account = existing ?? {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
enabledTokensMask: 0n,
|
|
46
|
-
totalDebtUSD: 0n,
|
|
47
|
-
totalDebt: 0n,
|
|
48
|
-
tokens: []
|
|
49
|
-
};
|
|
35
|
+
const account = existing ?? unopenedAccountSlice({
|
|
36
|
+
creditManager,
|
|
37
|
+
creditFacade: suite.creditFacade.address,
|
|
38
|
+
underlying
|
|
39
|
+
});
|
|
50
40
|
assertDebtLimits(sdk, debt, suite.creditFacade, underlying);
|
|
51
41
|
assertCanBorrow(sdk, suite, debt);
|
|
52
42
|
const paths = createRouterPaths({
|
|
@@ -110,35 +100,6 @@ async function buildOpenStrategyState(props) {
|
|
|
110
100
|
creditAccount: existing?.creditAccount
|
|
111
101
|
};
|
|
112
102
|
}
|
|
113
|
-
/**
|
|
114
|
-
* The opening that holds an account and nothing else.
|
|
115
|
-
*
|
|
116
|
-
* Taken before the walk rather than threaded through it: the router has no
|
|
117
|
-
* guard for an empty basket and would still make its `eth_call`, and every
|
|
118
|
-
* assertion below reads amounts that are not there.
|
|
119
|
-
*/
|
|
120
|
-
async function emptyOpenState(props) {
|
|
121
|
-
const { sdk, creditManager } = props;
|
|
122
|
-
assertMarketOperable(sdk.marketRegister.findCreditManager(creditManager));
|
|
123
|
-
const snapshot = {
|
|
124
|
-
creditManager,
|
|
125
|
-
assets: [],
|
|
126
|
-
quotas: [],
|
|
127
|
-
totalDebt: 0n,
|
|
128
|
-
totalValue: 0n
|
|
129
|
-
};
|
|
130
|
-
const { assets: _assets, quotas: _quotas, ...projection } = sdk.positions.projection(snapshot, { availableLiquidityChange: 0n });
|
|
131
|
-
return {
|
|
132
|
-
...projection,
|
|
133
|
-
currentPrice: sdk.positions.currentPrice(snapshot),
|
|
134
|
-
priceImpact: void 0,
|
|
135
|
-
averageAssets: [],
|
|
136
|
-
minAssets: [],
|
|
137
|
-
averageQuota: [],
|
|
138
|
-
minQuota: [],
|
|
139
|
-
calls: []
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
103
|
/** Collateral plus the borrowed underlying, folded into one balance per token. */
|
|
143
104
|
function mergeExpectedBalances(collateral, underlying, debt) {
|
|
144
105
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -203,6 +203,7 @@ function buildMockSdk(args) {
|
|
|
203
203
|
liquidationThresholds,
|
|
204
204
|
collateralTokens,
|
|
205
205
|
feeInterest: args.feeInterest ?? 0,
|
|
206
|
+
maxEnabledTokens: args.maxEnabledTokens ?? 4,
|
|
206
207
|
maxLeverage: (collateral, targetHF) => calcMaxLeverage(liquidationThresholds.get(collateral) ?? 0, targetHF)
|
|
207
208
|
},
|
|
208
209
|
creditFacade: {
|
|
@@ -2,6 +2,24 @@ import { AddressMap } from "../../../utils/AddressMap.js";
|
|
|
2
2
|
//#region src/onchain/accounts/intents/utils/common.ts
|
|
3
3
|
/** Case-insensitive address equality. */
|
|
4
4
|
const eq = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
5
|
+
/**
|
|
6
|
+
* The suite and market behind a credit manager, or nothing where the register
|
|
7
|
+
* has no entry for it.
|
|
8
|
+
*
|
|
9
|
+
* For the reads a form calls on every keystroke, including before the SDK has
|
|
10
|
+
* finished attaching: a question the register cannot answer yet is not an
|
|
11
|
+
* error. Everything that prepares a transaction wants the throw instead.
|
|
12
|
+
*/
|
|
13
|
+
function resolveCreditManager(sdk, creditManager) {
|
|
14
|
+
try {
|
|
15
|
+
return {
|
|
16
|
+
suite: sdk.marketRegister.findCreditManager(creditManager),
|
|
17
|
+
market: sdk.marketRegister.findByCreditManager(creditManager)
|
|
18
|
+
};
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
5
23
|
function toTargetDecimals(fromAmount, fromToken, toToken, sdk) {
|
|
6
24
|
const fromDecimals = sdk.tokensMeta.get(fromToken)?.decimals ?? 18;
|
|
7
25
|
const toDecimals = sdk.tokensMeta.get(toToken)?.decimals ?? 18;
|
|
@@ -37,4 +55,4 @@ function toRouterCaSlice(creditAccount, expectedBalances = []) {
|
|
|
37
55
|
};
|
|
38
56
|
}
|
|
39
57
|
//#endregion
|
|
40
|
-
export { eq, toRouterCaSlice, toTargetDecimals };
|
|
58
|
+
export { eq, resolveCreditManager, toRouterCaSlice, toTargetDecimals };
|
|
@@ -26,6 +26,26 @@ function toCreditAccountSlice(ca) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
+
* The slice a flow that has no account yet quotes against.
|
|
30
|
+
*
|
|
31
|
+
* Nothing of it exists on chain until the transaction lands, and nothing has
|
|
32
|
+
* to: the pathfinder is asked about the credit manager, and every balance the
|
|
33
|
+
* flow reasons about is one the transaction itself puts there. The zero
|
|
34
|
+
* address stands in for the account so the shape is complete.
|
|
35
|
+
*/
|
|
36
|
+
function unopenedAccountSlice(args) {
|
|
37
|
+
return {
|
|
38
|
+
creditAccount: "0x0000000000000000000000000000000000000000",
|
|
39
|
+
creditManager: args.creditManager.toLowerCase(),
|
|
40
|
+
creditFacade: args.creditFacade.toLowerCase(),
|
|
41
|
+
underlying: args.underlying.toLowerCase(),
|
|
42
|
+
enabledTokensMask: 0n,
|
|
43
|
+
totalDebtUSD: 0n,
|
|
44
|
+
totalDebt: 0n,
|
|
45
|
+
tokens: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
29
49
|
* Reads an account by address and narrows it to {@link CreditAccountSlice}.
|
|
30
50
|
*
|
|
31
51
|
* The shared read model's `StrategyPosition` carries neither `tokens` nor
|
|
@@ -40,4 +60,4 @@ async function fetchCreditAccountSlice(sdk, creditAccount) {
|
|
|
40
60
|
return toCreditAccountSlice(data);
|
|
41
61
|
}
|
|
42
62
|
//#endregion
|
|
43
|
-
export { fetchCreditAccountSlice, toCreditAccountSlice };
|
|
63
|
+
export { fetchCreditAccountSlice, toCreditAccountSlice, unopenedAccountSlice };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { eq, toRouterCaSlice, toTargetDecimals } from "./common.js";
|
|
1
|
+
import { eq, resolveCreditManager, toRouterCaSlice, toTargetDecimals } from "./common.js";
|
|
2
2
|
import { isPhantomToken, isRedemptionPhantomToken, pickFattestNonPhantomToken, rankAccountTokens } from "./pick-token.js";
|
|
3
3
|
import { adjustStateToSnapshot } from "./adjust-state-to-snapshot.js";
|
|
4
4
|
import { assembleOperationCalls } from "./assemble-operation-calls.js";
|
|
5
5
|
import { calcBorrowedAmountPlusInterestAndFees } from "./borrowed-amount-plus-interest-and-fees.js";
|
|
6
|
-
import { fetchCreditAccountSlice, toCreditAccountSlice } from "./credit-account-slice.js";
|
|
6
|
+
import { fetchCreditAccountSlice, toCreditAccountSlice, unopenedAccountSlice } from "./credit-account-slice.js";
|
|
7
7
|
import { OperationLedger } from "./ledger.js";
|
|
8
8
|
import { collectPriceImpact, lossRate, startProbe } from "./price-impact.js";
|
|
9
9
|
import { clearedQuotas, getQuotasForUpdate, quotasAfterUpdate } from "./quotas-for-update.js";
|
|
10
10
|
import { createOraclePaths, createRouterPaths } from "./router-path.js";
|
|
11
|
-
export { OperationLedger, adjustStateToSnapshot, assembleOperationCalls, calcBorrowedAmountPlusInterestAndFees, clearedQuotas, collectPriceImpact, createOraclePaths, createRouterPaths, eq, fetchCreditAccountSlice, getQuotasForUpdate, isPhantomToken, isRedemptionPhantomToken, lossRate, pickFattestNonPhantomToken, quotasAfterUpdate, rankAccountTokens, startProbe, toCreditAccountSlice, toRouterCaSlice, toTargetDecimals };
|
|
11
|
+
export { OperationLedger, adjustStateToSnapshot, assembleOperationCalls, calcBorrowedAmountPlusInterestAndFees, clearedQuotas, collectPriceImpact, createOraclePaths, createRouterPaths, eq, fetchCreditAccountSlice, getQuotasForUpdate, isPhantomToken, isRedemptionPhantomToken, lossRate, pickFattestNonPhantomToken, quotasAfterUpdate, rankAccountTokens, resolveCreditManager, startProbe, toCreditAccountSlice, toRouterCaSlice, toTargetDecimals, unopenedAccountSlice };
|
|
@@ -3,8 +3,8 @@ import "../../utils/index.js";
|
|
|
3
3
|
import { MIN_HEALTH_FACTOR_FACADE } from "../../validation/helpers/health-factor-limits.js";
|
|
4
4
|
import "../../validation/index.js";
|
|
5
5
|
import { eq } from "./utils/common.js";
|
|
6
|
-
import { collateralMoney } from "./collateral-money.js";
|
|
7
6
|
import { maxProportionalWithdrawal } from "./math.js";
|
|
7
|
+
import { collateralValuation } from "./collateral-valuation.js";
|
|
8
8
|
import { accountView } from "./view.js";
|
|
9
9
|
//#region src/onchain/accounts/intents/withdraw-limits.ts
|
|
10
10
|
/**
|
|
@@ -38,7 +38,7 @@ function withdrawLimits(props) {
|
|
|
38
38
|
* in underlying units.
|
|
39
39
|
*
|
|
40
40
|
* A withdrawal hands funds over, so the facade weighs the account it leaves
|
|
41
|
-
* behind at safe prices rather than main ones — see {@link
|
|
41
|
+
* behind at safe prices rather than main ones — see {@link collateralValuation}.
|
|
42
42
|
* That is a second limit on top of the facade's `debtLimits`, and the two are
|
|
43
43
|
* independent: a caller wanting the amount a form may actually offer takes the
|
|
44
44
|
* lesser of this and `maxProportionalWithdrawal`.
|
|
@@ -71,20 +71,20 @@ function maxSafeWithdrawal(props) {
|
|
|
71
71
|
const view = accountView(creditAccount, sdk);
|
|
72
72
|
if (view.collateral <= 0n) return 0n;
|
|
73
73
|
if (view.debt === 0n) return view.collateral;
|
|
74
|
-
const
|
|
74
|
+
const valuation = collateralValuation(creditAccount, sdk);
|
|
75
75
|
const source = props.sourceToken ?? view.fattest();
|
|
76
76
|
const holding = source === void 0 ? void 0 : creditAccount.tokens.find((t) => eq(t.token, source));
|
|
77
77
|
if (!holding) return view.collateral;
|
|
78
78
|
let total = 0n;
|
|
79
|
-
for (const t of creditAccount.tokens) if (
|
|
80
|
-
const debtUsd =
|
|
79
|
+
for (const t of creditAccount.tokens) if (valuation.counts(t)) total += valuation.weigh(t);
|
|
80
|
+
const debtUsd = valuation.mainUsd(valuation.underlying, view.debt);
|
|
81
81
|
if (debtUsd === void 0 || debtUsd <= 0n) return view.collateral;
|
|
82
82
|
const slack = total - debtUsd * targetHF;
|
|
83
83
|
if (slack < 0n) return 0n;
|
|
84
|
-
const sourceMainUsd =
|
|
84
|
+
const sourceMainUsd = valuation.mainUsd(holding.token, holding.balance);
|
|
85
85
|
if (sourceMainUsd === void 0 || sourceMainUsd <= 0n) return view.collateral;
|
|
86
|
-
const sourceRate =
|
|
87
|
-
const tvlUsd =
|
|
86
|
+
const sourceRate = valuation.lt(holding.token) * valuation.checkedUsd(holding);
|
|
87
|
+
const tvlUsd = valuation.mainUsd(valuation.underlying, view.collateral + view.debt);
|
|
88
88
|
if (tvlUsd === void 0) return view.collateral;
|
|
89
89
|
const drain = sourceRate * tvlUsd - targetHF * debtUsd * sourceMainUsd;
|
|
90
90
|
if (drain <= 0n) return view.collateral;
|
|
@@ -35,7 +35,7 @@ function calcHealthFactor(props) {
|
|
|
35
35
|
const scale = 10n ** BigInt(decimalsByToken.get(token) ?? 18);
|
|
36
36
|
return amount * price / scale;
|
|
37
37
|
};
|
|
38
|
-
const
|
|
38
|
+
const assetValue = snapshot.assets.reduce((acc, { token, balance }) => {
|
|
39
39
|
if (balance <= 10n) return acc;
|
|
40
40
|
const lt = BigInt(lts.get(token) ?? 0);
|
|
41
41
|
const tokenLtWeighted = (convertToUSD(token, balance, true) ?? 0n) * lt;
|
|
@@ -44,8 +44,8 @@ function calcHealthFactor(props) {
|
|
|
44
44
|
const quotaWeighted = (convertToUSD(underlying, quotaBalance) ?? 0n) * PERCENTAGE_FACTOR;
|
|
45
45
|
return acc + (quota ? BigIntMath.min(quotaWeighted, tokenLtWeighted) : tokenLtWeighted);
|
|
46
46
|
}, 0n);
|
|
47
|
-
const
|
|
48
|
-
const hf =
|
|
47
|
+
const borrowedValue = convertToUSD(underlying, snapshot.totalDebt) ?? 0n;
|
|
48
|
+
const hf = borrowedValue > 0n ? assetValue / borrowedValue : 0n;
|
|
49
49
|
return Number(hf);
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
@@ -17,7 +17,7 @@ function previewOpenStrategyPosition(sdk, input, operation, replay) {
|
|
|
17
17
|
warning ??= priced.error;
|
|
18
18
|
return priced.value;
|
|
19
19
|
};
|
|
20
|
-
const netValue = before.balances.sum(price) + after.collateralAdded.sum(price);
|
|
20
|
+
const netValue = before.balances.sum(price) + after.collateralAdded.sum(price) - after.collateralWithdrawn.sum(price);
|
|
21
21
|
const unwrapped = unwrapNativeCollateral(after.collateralAdded.toAssets(), value, sdk.addressProvider.getAddress(AP_WETH_TOKEN, 0));
|
|
22
22
|
if (!unwrapped.ok) return unwrapped;
|
|
23
23
|
const collateral = unwrapped.data;
|
|
@@ -27,6 +27,7 @@ function previewOpenStrategyPosition(sdk, input, operation, replay) {
|
|
|
27
27
|
...asEstimated(sdk.positions.projection(snap, { availableLiquidityChange: -account.totalDebt })),
|
|
28
28
|
targetCollateral: targetAsset ? oracle.toTokenAmount(targetAsset.token, targetAsset.balance) : void 0,
|
|
29
29
|
collateralAdded: collateral.map((a) => oracle.toTokenAmount(a.token, a.balance)),
|
|
30
|
+
collateralWithdrawn: after.collateralWithdrawn.toAssets().map((a) => oracle.toTokenAmount(a.token, a.balance)),
|
|
30
31
|
warning
|
|
31
32
|
};
|
|
32
33
|
return sdkOk({
|
|
@@ -16,6 +16,8 @@ var ExecuteApi = class {
|
|
|
16
16
|
switch (request.kind) {
|
|
17
17
|
case "pool": return poolTx(sdk, request);
|
|
18
18
|
case "open": return openTx(sdk, request);
|
|
19
|
+
case "openEmpty": return openEmptyTx(sdk, request);
|
|
20
|
+
case "borrow": return borrowTx(sdk, request);
|
|
19
21
|
case "account": return accountTx(sdk, request);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -62,17 +64,75 @@ async function openTx(sdk, request) {
|
|
|
62
64
|
reopenCreditAccount: state.creditAccount,
|
|
63
65
|
permits: {},
|
|
64
66
|
referralCode: 0n,
|
|
65
|
-
rwaOptions: await openRwaOptions(sdk, request)
|
|
67
|
+
rwaOptions: await openRwaOptions(sdk, request, request.targetToken)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The opening with nothing in it: no debt to draw, no collateral to take, no
|
|
72
|
+
* path to run and no quota to buy, so `openCA` is left with the price updates
|
|
73
|
+
* the market demands and the facade's own `openCreditAccount`.
|
|
74
|
+
*
|
|
75
|
+
* Nothing is read off the preparation because there is nothing on it. The
|
|
76
|
+
* account holds no token, so an RWA market has none to gate on either.
|
|
77
|
+
**/
|
|
78
|
+
function openEmptyTx(sdk, request) {
|
|
79
|
+
return sdk.accounts.openCA({
|
|
80
|
+
creditManager: request.creditManager,
|
|
81
|
+
to: request.wallet,
|
|
82
|
+
collateral: [],
|
|
83
|
+
ethAmount: 0n,
|
|
84
|
+
debt: 0n,
|
|
85
|
+
calls: [],
|
|
86
|
+
averageQuota: [],
|
|
87
|
+
minQuota: [],
|
|
88
|
+
permits: {},
|
|
89
|
+
referralCode: 0n
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A loan is an opening whose debt leaves again, so it is the same `openCA`
|
|
94
|
+
* with `withdrawToken` set: the facade draws the debt, takes the collateral,
|
|
95
|
+
* runs whatever path buys the payout, and sweeps that payout to the wallet.
|
|
96
|
+
*
|
|
97
|
+
* Both quota branches are the one the state carries. An opening has two
|
|
98
|
+
* because the balances it lands on are a router quote; here the account is
|
|
99
|
+
* left holding the collateral the caller named, and a named amount has no
|
|
100
|
+
* floor to differ from.
|
|
101
|
+
**/
|
|
102
|
+
async function borrowTx(sdk, request) {
|
|
103
|
+
const { creditManager, wallet, ethAmount, sim } = request;
|
|
104
|
+
const { state } = sim.data;
|
|
105
|
+
const collateral = {
|
|
106
|
+
token: state.collateral.token.address,
|
|
107
|
+
balance: state.collateral.value
|
|
108
|
+
};
|
|
109
|
+
return sdk.accounts.openCA({
|
|
110
|
+
creditManager,
|
|
111
|
+
to: wallet,
|
|
112
|
+
collateral: [collateral],
|
|
113
|
+
ethAmount,
|
|
114
|
+
debt: state.totalDebt.value,
|
|
115
|
+
calls: state.calls,
|
|
116
|
+
withdrawToken: state.borrowed.token.address,
|
|
117
|
+
averageQuota: state.quotaIncrease,
|
|
118
|
+
minQuota: state.quotaIncrease,
|
|
119
|
+
reopenCreditAccount: state.creditAccount,
|
|
120
|
+
permits: {},
|
|
121
|
+
referralCode: 0n,
|
|
122
|
+
rwaOptions: await openRwaOptions(sdk, request, collateral.token)
|
|
66
123
|
});
|
|
67
124
|
}
|
|
68
125
|
/**
|
|
69
126
|
* The documented `openCA` contract: ask the market for its open requirements
|
|
70
127
|
* and hand them back as operation args, with the caller's cached signatures
|
|
71
|
-
* attached. `undefined` on non-RWA markets and when no
|
|
128
|
+
* attached. `undefined` on non-RWA markets and when no token is named.
|
|
129
|
+
*
|
|
130
|
+
* `token` is the one the account ends up holding, which is what an RWA market
|
|
131
|
+
* gates on: the opening's target, and the borrow's collateral.
|
|
72
132
|
**/
|
|
73
|
-
async function openRwaOptions(sdk, request) {
|
|
74
|
-
if (!
|
|
75
|
-
const requirements = await sdk.accounts.getOpenAccountRequirements(request.wallet, request.creditManager, { tokenOutAddress:
|
|
133
|
+
async function openRwaOptions(sdk, request, token) {
|
|
134
|
+
if (!token) return;
|
|
135
|
+
const requirements = await sdk.accounts.getOpenAccountRequirements(request.wallet, request.creditManager, { tokenOutAddress: token });
|
|
76
136
|
if (requirements?.protocol !== "securitize") return;
|
|
77
137
|
return {
|
|
78
138
|
protocol: "securitize",
|
|
@@ -195,26 +195,33 @@ var PrepareApi = class extends MultichainConstruct {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
/**
|
|
198
|
+
* {@inheritDoc IOpportunitiesPrepare.openEmptyCreditAccount}
|
|
199
|
+
**/
|
|
200
|
+
async openEmptyCreditAccount(strategy) {
|
|
201
|
+
try {
|
|
202
|
+
const sdk = await this.#chain(strategy.chainId);
|
|
203
|
+
const at = stateBlock(sdk);
|
|
204
|
+
const result = await service(sdk).openEmptyAccountIntent({
|
|
205
|
+
sdk,
|
|
206
|
+
creditManager: strategy.creditManager
|
|
207
|
+
});
|
|
208
|
+
return result.ok ? sdkOk(at) : methodError(result);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return sdkErr(unexpectedFailure(e));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
198
214
|
* {@inheritDoc IOpportunitiesPrepare.openNewStrategy}
|
|
199
215
|
**/
|
|
200
216
|
async openNewStrategy(strategy, params) {
|
|
201
217
|
try {
|
|
202
218
|
const sdk = await this.#chain(strategy.chainId);
|
|
203
219
|
const at = stateBlock(sdk);
|
|
204
|
-
if (params.empty) return opened(await service(sdk).openStrategyIntent({
|
|
205
|
-
sdk,
|
|
206
|
-
creditManager: strategy.creditManager,
|
|
207
|
-
empty: true
|
|
208
|
-
}), at);
|
|
209
220
|
const targetToken = params.targetToken ?? sdk.marketRegister.findCreditManager(strategy.creditManager).strategyTargetCollateral;
|
|
210
221
|
if (!targetToken) return sdkErr(noStrategyTargetCollateral(strategy.creditManager));
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
if (!reused || !isAddressEqual(reused.creditManager, strategy.creditManager)) return sdkErr(creditAccountNotFound(params.creditAccount));
|
|
215
|
-
if (reused.totalDebt > 0n || reused.tokens.some((t) => t.quota > 0n)) return sdkErr(creditAccountNotEmpty(params.creditAccount));
|
|
216
|
-
creditAccount = reused;
|
|
217
|
-
}
|
|
222
|
+
const reused = await reusable(sdk, strategy, params.creditAccount);
|
|
223
|
+
if (reused && "error" in reused) return reused;
|
|
224
|
+
const creditAccount = reused?.account;
|
|
218
225
|
return opened(await service(sdk).openStrategyIntent({
|
|
219
226
|
sdk,
|
|
220
227
|
creditManager: strategy.creditManager,
|
|
@@ -231,6 +238,30 @@ var PrepareApi = class extends MultichainConstruct {
|
|
|
231
238
|
}
|
|
232
239
|
}
|
|
233
240
|
/**
|
|
241
|
+
* {@inheritDoc IOpportunitiesPrepare.borrow}
|
|
242
|
+
**/
|
|
243
|
+
async borrow(strategy, params) {
|
|
244
|
+
try {
|
|
245
|
+
const sdk = await this.#chain(strategy.chainId);
|
|
246
|
+
const at = stateBlock(sdk);
|
|
247
|
+
const reused = await reusable(sdk, strategy, params.creditAccount);
|
|
248
|
+
if (reused && "error" in reused) return reused;
|
|
249
|
+
return borrowed(await service(sdk).borrowIntent({
|
|
250
|
+
sdk,
|
|
251
|
+
creditManager: strategy.creditManager,
|
|
252
|
+
collateralToken: params.collateralToken,
|
|
253
|
+
collateralAmount: params.collateralAmount,
|
|
254
|
+
borrowToken: params.borrowToken,
|
|
255
|
+
borrowAmount: params.borrowAmount,
|
|
256
|
+
slippage: params.slippage,
|
|
257
|
+
quotaReserve: params.quotaReserve,
|
|
258
|
+
creditAccount: reused?.account
|
|
259
|
+
}), at);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return sdkErr(unexpectedFailure(e));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
234
265
|
* {@inheritDoc IOpportunitiesPrepare.depositStrategy}
|
|
235
266
|
**/
|
|
236
267
|
async depositStrategy(position, params) {
|
|
@@ -353,6 +384,21 @@ var PrepareApi = class extends MultichainConstruct {
|
|
|
353
384
|
});
|
|
354
385
|
}
|
|
355
386
|
/**
|
|
387
|
+
* {@inheritDoc IOpportunitiesPrepare.maxBorrow}
|
|
388
|
+
**/
|
|
389
|
+
maxBorrow(strategy, params) {
|
|
390
|
+
const sdk = this.sdk.chain(strategy.chainId);
|
|
391
|
+
return service(sdk).maxBorrow({
|
|
392
|
+
sdk,
|
|
393
|
+
creditManager: strategy.creditManager,
|
|
394
|
+
collateralToken: params.collateralToken,
|
|
395
|
+
collateralAmount: params.collateralAmount,
|
|
396
|
+
borrowToken: params.borrowToken,
|
|
397
|
+
targetHF: params.targetHF,
|
|
398
|
+
quotaReserve: params.quotaReserve
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
356
402
|
* The account a bare `max*` read weighs. These reads answer a number, not
|
|
357
403
|
* an envelope, so an account the markets do not hold is thrown rather than
|
|
358
404
|
* described, see {@link IOpportunitiesPrepare.maxWithdraw}.
|
|
@@ -438,6 +484,25 @@ async function slice(sdk, creditAccount) {
|
|
|
438
484
|
return data && toCreditAccountSlice(data);
|
|
439
485
|
}
|
|
440
486
|
/**
|
|
487
|
+
* The pre-opened account a request asks to be run on, held to what "pre-opened"
|
|
488
|
+
* means: this manager's, owing nothing and holding no quota.
|
|
489
|
+
*
|
|
490
|
+
* Shared by the two flows that put something on a fresh account — an opening
|
|
491
|
+
* and a borrow — so both hold a reused one to the same terms. Whatever
|
|
492
|
+
* balances sit on it are left to the flow: an opening routes them, a borrow
|
|
493
|
+
* leaves them where they are.
|
|
494
|
+
*
|
|
495
|
+
* @returns Nothing when the request named no account, the refusal to answer
|
|
496
|
+
* with when it named one that does not qualify, and the slice otherwise
|
|
497
|
+
**/
|
|
498
|
+
async function reusable(sdk, strategy, creditAccount) {
|
|
499
|
+
if (!creditAccount) return;
|
|
500
|
+
const account = await slice(sdk, creditAccount);
|
|
501
|
+
if (!account || !isAddressEqual(account.creditManager, strategy.creditManager)) return sdkErr(creditAccountNotFound(creditAccount));
|
|
502
|
+
if (account.totalDebt > 0n || account.tokens.some((t) => t.quota > 0n)) return sdkErr(creditAccountNotEmpty(creditAccount));
|
|
503
|
+
return { account };
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
441
506
|
* The operation a claim resumes, or `undefined` when there is none to resume:
|
|
442
507
|
* a withdrawal requested without an intent, or one read through a compressor
|
|
443
508
|
* too old to report it. Every intent the engine records can be finished,
|
|
@@ -537,6 +602,15 @@ function opened(result, at) {
|
|
|
537
602
|
}
|
|
538
603
|
/**
|
|
539
604
|
* {@inheritDoc planned}
|
|
605
|
+
**/
|
|
606
|
+
function borrowed(result, at) {
|
|
607
|
+
return result.ok ? sdkOk({
|
|
608
|
+
state: result.state,
|
|
609
|
+
...at
|
|
610
|
+
}) : methodError(result);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* {@inheritDoc planned}
|
|
540
614
|
*
|
|
541
615
|
* Both routes are payload, error and all: `errors` says why a missing one is
|
|
542
616
|
* missing, and it stays on the error when neither route answered, since that is
|
|
@@ -369,6 +369,15 @@ interface OpenStrategyPositionProjection extends EstimatedProjection {
|
|
|
369
369
|
* accordingly (omitted entirely when it reaches zero).
|
|
370
370
|
*/
|
|
371
371
|
collateralAdded: TokenAmount[];
|
|
372
|
+
/**
|
|
373
|
+
* Tokens the same transaction hands back to the wallet
|
|
374
|
+
* (`withdrawCollateral` calls, with the MAX_UINT256 sentinel resolved
|
|
375
|
+
* against replayed balances).
|
|
376
|
+
*
|
|
377
|
+
* Empty for an opening that keeps everything it bought; a borrow pays its
|
|
378
|
+
* loan out here, and the values above are what is left once it has.
|
|
379
|
+
*/
|
|
380
|
+
collateralWithdrawn: TokenAmount[];
|
|
372
381
|
/**
|
|
373
382
|
* The oracle could not price a token; it contributes nothing to the values.
|
|
374
383
|
*/
|
|
@@ -376,8 +385,8 @@ interface OpenStrategyPositionProjection extends EstimatedProjection {
|
|
|
376
385
|
}
|
|
377
386
|
/**
|
|
378
387
|
* What an account-opening transaction that already exists would do — the
|
|
379
|
-
* counterpart of `prepare.openNewStrategy`, read off
|
|
380
|
-
* planned into it.
|
|
388
|
+
* counterpart of `prepare.openNewStrategy` and `prepare.borrow`, read off
|
|
389
|
+
* calldata rather than planned into it.
|
|
381
390
|
**/
|
|
382
391
|
interface OpenStrategyPositionPreview extends OpenStrategyPositionProjection {
|
|
383
392
|
operation: "OpenCreditAccount";
|