@gearbox-protocol/sdk 16.4.0-next.4 → 16.4.0-next.6
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/dev/midasUtils.js +46 -11
- 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 +79 -1
- package/dist/cjs/onchain/accounts/intents/leverage-band.js +2 -12
- package/dist/cjs/onchain/accounts/intents/maxBorrow.js +85 -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/dev/midasUtils.js +47 -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 +79 -1
- package/dist/esm/onchain/accounts/intents/leverage-band.js +2 -12
- package/dist/esm/onchain/accounts/intents/maxBorrow.js +83 -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/dev/midasUtils.d.ts +4 -3
- 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 +78 -2
- package/dist/types/onchain/accounts/intents/maxBorrow.d.ts +47 -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 +169 -49
- package/package.json +1 -1
|
@@ -3,6 +3,24 @@ const require_onchain_utils_AddressMap = require("../../../utils/AddressMap.js")
|
|
|
3
3
|
//#region src/onchain/accounts/intents/utils/common.ts
|
|
4
4
|
/** Case-insensitive address equality. */
|
|
5
5
|
const eq = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
6
|
+
/**
|
|
7
|
+
* The suite and market behind a credit manager, or nothing where the register
|
|
8
|
+
* has no entry for it.
|
|
9
|
+
*
|
|
10
|
+
* For the reads a form calls on every keystroke, including before the SDK has
|
|
11
|
+
* finished attaching: a question the register cannot answer yet is not an
|
|
12
|
+
* error. Everything that prepares a transaction wants the throw instead.
|
|
13
|
+
*/
|
|
14
|
+
function resolveCreditManager(sdk, creditManager) {
|
|
15
|
+
try {
|
|
16
|
+
return {
|
|
17
|
+
suite: sdk.marketRegister.findCreditManager(creditManager),
|
|
18
|
+
market: sdk.marketRegister.findByCreditManager(creditManager)
|
|
19
|
+
};
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
6
24
|
function toTargetDecimals(fromAmount, fromToken, toToken, sdk) {
|
|
7
25
|
const fromDecimals = sdk.tokensMeta.get(fromToken)?.decimals ?? 18;
|
|
8
26
|
const toDecimals = sdk.tokensMeta.get(toToken)?.decimals ?? 18;
|
|
@@ -39,5 +57,6 @@ function toRouterCaSlice(creditAccount, expectedBalances = []) {
|
|
|
39
57
|
}
|
|
40
58
|
//#endregion
|
|
41
59
|
exports.eq = eq;
|
|
60
|
+
exports.resolveCreditManager = resolveCreditManager;
|
|
42
61
|
exports.toRouterCaSlice = toRouterCaSlice;
|
|
43
62
|
exports.toTargetDecimals = toTargetDecimals;
|
|
@@ -27,6 +27,26 @@ function toCreditAccountSlice(ca) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
+
* The slice a flow that has no account yet quotes against.
|
|
31
|
+
*
|
|
32
|
+
* Nothing of it exists on chain until the transaction lands, and nothing has
|
|
33
|
+
* to: the pathfinder is asked about the credit manager, and every balance the
|
|
34
|
+
* flow reasons about is one the transaction itself puts there. The zero
|
|
35
|
+
* address stands in for the account so the shape is complete.
|
|
36
|
+
*/
|
|
37
|
+
function unopenedAccountSlice(args) {
|
|
38
|
+
return {
|
|
39
|
+
creditAccount: "0x0000000000000000000000000000000000000000",
|
|
40
|
+
creditManager: args.creditManager.toLowerCase(),
|
|
41
|
+
creditFacade: args.creditFacade.toLowerCase(),
|
|
42
|
+
underlying: args.underlying.toLowerCase(),
|
|
43
|
+
enabledTokensMask: 0n,
|
|
44
|
+
totalDebtUSD: 0n,
|
|
45
|
+
totalDebt: 0n,
|
|
46
|
+
tokens: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
30
50
|
* Reads an account by address and narrows it to {@link CreditAccountSlice}.
|
|
31
51
|
*
|
|
32
52
|
* The shared read model's `StrategyPosition` carries neither `tokens` nor
|
|
@@ -43,3 +63,4 @@ async function fetchCreditAccountSlice(sdk, creditAccount) {
|
|
|
43
63
|
//#endregion
|
|
44
64
|
exports.fetchCreditAccountSlice = fetchCreditAccountSlice;
|
|
45
65
|
exports.toCreditAccountSlice = toCreditAccountSlice;
|
|
66
|
+
exports.unopenedAccountSlice = unopenedAccountSlice;
|
|
@@ -26,7 +26,9 @@ exports.lossRate = require_onchain_accounts_intents_utils_price_impact.lossRate;
|
|
|
26
26
|
exports.pickFattestNonPhantomToken = require_onchain_accounts_intents_utils_pick_token.pickFattestNonPhantomToken;
|
|
27
27
|
exports.quotasAfterUpdate = require_onchain_accounts_intents_utils_quotas_for_update.quotasAfterUpdate;
|
|
28
28
|
exports.rankAccountTokens = require_onchain_accounts_intents_utils_pick_token.rankAccountTokens;
|
|
29
|
+
exports.resolveCreditManager = require_onchain_accounts_intents_utils_common.resolveCreditManager;
|
|
29
30
|
exports.startProbe = require_onchain_accounts_intents_utils_price_impact.startProbe;
|
|
30
31
|
exports.toCreditAccountSlice = require_onchain_accounts_intents_utils_credit_account_slice.toCreditAccountSlice;
|
|
31
32
|
exports.toRouterCaSlice = require_onchain_accounts_intents_utils_common.toRouterCaSlice;
|
|
32
33
|
exports.toTargetDecimals = require_onchain_accounts_intents_utils_common.toTargetDecimals;
|
|
34
|
+
exports.unopenedAccountSlice = require_onchain_accounts_intents_utils_credit_account_slice.unopenedAccountSlice;
|
|
@@ -4,8 +4,8 @@ require("../../utils/index.js");
|
|
|
4
4
|
const require_onchain_validation_helpers_health_factor_limits = require("../../validation/helpers/health-factor-limits.js");
|
|
5
5
|
require("../../validation/index.js");
|
|
6
6
|
const require_onchain_accounts_intents_utils_common = require("./utils/common.js");
|
|
7
|
-
const require_onchain_accounts_intents_collateral_money = require("./collateral-money.js");
|
|
8
7
|
const require_onchain_accounts_intents_math = require("./math.js");
|
|
8
|
+
const require_onchain_accounts_intents_collateral_valuation = require("./collateral-valuation.js");
|
|
9
9
|
const require_onchain_accounts_intents_view = require("./view.js");
|
|
10
10
|
//#region src/onchain/accounts/intents/withdraw-limits.ts
|
|
11
11
|
/**
|
|
@@ -39,7 +39,7 @@ function withdrawLimits(props) {
|
|
|
39
39
|
* in underlying units.
|
|
40
40
|
*
|
|
41
41
|
* A withdrawal hands funds over, so the facade weighs the account it leaves
|
|
42
|
-
* behind at safe prices rather than main ones — see {@link
|
|
42
|
+
* behind at safe prices rather than main ones — see {@link collateralValuation}.
|
|
43
43
|
* That is a second limit on top of the facade's `debtLimits`, and the two are
|
|
44
44
|
* independent: a caller wanting the amount a form may actually offer takes the
|
|
45
45
|
* lesser of this and `maxProportionalWithdrawal`.
|
|
@@ -72,20 +72,20 @@ function maxSafeWithdrawal(props) {
|
|
|
72
72
|
const view = require_onchain_accounts_intents_view.accountView(creditAccount, sdk);
|
|
73
73
|
if (view.collateral <= 0n) return 0n;
|
|
74
74
|
if (view.debt === 0n) return view.collateral;
|
|
75
|
-
const
|
|
75
|
+
const valuation = require_onchain_accounts_intents_collateral_valuation.collateralValuation(creditAccount, sdk);
|
|
76
76
|
const source = props.sourceToken ?? view.fattest();
|
|
77
77
|
const holding = source === void 0 ? void 0 : creditAccount.tokens.find((t) => require_onchain_accounts_intents_utils_common.eq(t.token, source));
|
|
78
78
|
if (!holding) return view.collateral;
|
|
79
79
|
let total = 0n;
|
|
80
|
-
for (const t of creditAccount.tokens) if (
|
|
81
|
-
const debtUsd =
|
|
80
|
+
for (const t of creditAccount.tokens) if (valuation.counts(t)) total += valuation.weigh(t);
|
|
81
|
+
const debtUsd = valuation.mainUsd(valuation.underlying, view.debt);
|
|
82
82
|
if (debtUsd === void 0 || debtUsd <= 0n) return view.collateral;
|
|
83
83
|
const slack = total - debtUsd * targetHF;
|
|
84
84
|
if (slack < 0n) return 0n;
|
|
85
|
-
const sourceMainUsd =
|
|
85
|
+
const sourceMainUsd = valuation.mainUsd(holding.token, holding.balance);
|
|
86
86
|
if (sourceMainUsd === void 0 || sourceMainUsd <= 0n) return view.collateral;
|
|
87
|
-
const sourceRate =
|
|
88
|
-
const tvlUsd =
|
|
87
|
+
const sourceRate = valuation.lt(holding.token) * valuation.checkedUsd(holding);
|
|
88
|
+
const tvlUsd = valuation.mainUsd(valuation.underlying, view.collateral + view.debt);
|
|
89
89
|
if (tvlUsd === void 0) return view.collateral;
|
|
90
90
|
const drain = sourceRate * tvlUsd - targetHF * debtUsd * sourceMainUsd;
|
|
91
91
|
if (drain <= 0n) return view.collateral;
|
|
@@ -36,7 +36,7 @@ function calcHealthFactor(props) {
|
|
|
36
36
|
const scale = 10n ** BigInt(decimalsByToken.get(token) ?? 18);
|
|
37
37
|
return amount * price / scale;
|
|
38
38
|
};
|
|
39
|
-
const
|
|
39
|
+
const assetValue = snapshot.assets.reduce((acc, { token, balance }) => {
|
|
40
40
|
if (balance <= 10n) return acc;
|
|
41
41
|
const lt = BigInt(lts.get(token) ?? 0);
|
|
42
42
|
const tokenLtWeighted = (convertToUSD(token, balance, true) ?? 0n) * lt;
|
|
@@ -45,8 +45,8 @@ function calcHealthFactor(props) {
|
|
|
45
45
|
const quotaWeighted = (convertToUSD(underlying, quotaBalance) ?? 0n) * require_onchain_constants_math.PERCENTAGE_FACTOR;
|
|
46
46
|
return acc + (quota ? require_onchain_utils_bigint_math.BigIntMath.min(quotaWeighted, tokenLtWeighted) : tokenLtWeighted);
|
|
47
47
|
}, 0n);
|
|
48
|
-
const
|
|
49
|
-
const hf =
|
|
48
|
+
const borrowedValue = convertToUSD(underlying, snapshot.totalDebt) ?? 0n;
|
|
49
|
+
const hf = borrowedValue > 0n ? assetValue / borrowedValue : 0n;
|
|
50
50
|
return Number(hf);
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
@@ -18,7 +18,7 @@ function previewOpenStrategyPosition(sdk, input, operation, replay) {
|
|
|
18
18
|
warning ??= priced.error;
|
|
19
19
|
return priced.value;
|
|
20
20
|
};
|
|
21
|
-
const netValue = before.balances.sum(price) + after.collateralAdded.sum(price);
|
|
21
|
+
const netValue = before.balances.sum(price) + after.collateralAdded.sum(price) - after.collateralWithdrawn.sum(price);
|
|
22
22
|
const unwrapped = require_onchain_preview_preview_unwrapNativeCollateral.unwrapNativeCollateral(after.collateralAdded.toAssets(), value, sdk.addressProvider.getAddress(require_onchain_constants_address_provider.AP_WETH_TOKEN, 0));
|
|
23
23
|
if (!unwrapped.ok) return unwrapped;
|
|
24
24
|
const collateral = unwrapped.data;
|
|
@@ -28,6 +28,7 @@ function previewOpenStrategyPosition(sdk, input, operation, replay) {
|
|
|
28
28
|
...require_model_previews.asEstimated(sdk.positions.projection(snap, { availableLiquidityChange: -account.totalDebt })),
|
|
29
29
|
targetCollateral: targetAsset ? oracle.toTokenAmount(targetAsset.token, targetAsset.balance) : void 0,
|
|
30
30
|
collateralAdded: collateral.map((a) => oracle.toTokenAmount(a.token, a.balance)),
|
|
31
|
+
collateralWithdrawn: after.collateralWithdrawn.toAssets().map((a) => oracle.toTokenAmount(a.token, a.balance)),
|
|
31
32
|
warning
|
|
32
33
|
};
|
|
33
34
|
return require_model_result.sdkOk({
|
|
@@ -17,6 +17,8 @@ var ExecuteApi = class {
|
|
|
17
17
|
switch (request.kind) {
|
|
18
18
|
case "pool": return poolTx(sdk, request);
|
|
19
19
|
case "open": return openTx(sdk, request);
|
|
20
|
+
case "openEmpty": return openEmptyTx(sdk, request);
|
|
21
|
+
case "borrow": return borrowTx(sdk, request);
|
|
20
22
|
case "account": return accountTx(sdk, request);
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -63,17 +65,75 @@ async function openTx(sdk, request) {
|
|
|
63
65
|
reopenCreditAccount: state.creditAccount,
|
|
64
66
|
permits: {},
|
|
65
67
|
referralCode: 0n,
|
|
66
|
-
rwaOptions: await openRwaOptions(sdk, request)
|
|
68
|
+
rwaOptions: await openRwaOptions(sdk, request, request.targetToken)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The opening with nothing in it: no debt to draw, no collateral to take, no
|
|
73
|
+
* path to run and no quota to buy, so `openCA` is left with the price updates
|
|
74
|
+
* the market demands and the facade's own `openCreditAccount`.
|
|
75
|
+
*
|
|
76
|
+
* Nothing is read off the preparation because there is nothing on it. The
|
|
77
|
+
* account holds no token, so an RWA market has none to gate on either.
|
|
78
|
+
**/
|
|
79
|
+
function openEmptyTx(sdk, request) {
|
|
80
|
+
return sdk.accounts.openCA({
|
|
81
|
+
creditManager: request.creditManager,
|
|
82
|
+
to: request.wallet,
|
|
83
|
+
collateral: [],
|
|
84
|
+
ethAmount: 0n,
|
|
85
|
+
debt: 0n,
|
|
86
|
+
calls: [],
|
|
87
|
+
averageQuota: [],
|
|
88
|
+
minQuota: [],
|
|
89
|
+
permits: {},
|
|
90
|
+
referralCode: 0n
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A loan is an opening whose debt leaves again, so it is the same `openCA`
|
|
95
|
+
* with `withdrawToken` set: the facade draws the debt, takes the collateral,
|
|
96
|
+
* runs whatever path buys the payout, and sweeps that payout to the wallet.
|
|
97
|
+
*
|
|
98
|
+
* Both quota branches are the one the state carries. An opening has two
|
|
99
|
+
* because the balances it lands on are a router quote; here the account is
|
|
100
|
+
* left holding the collateral the caller named, and a named amount has no
|
|
101
|
+
* floor to differ from.
|
|
102
|
+
**/
|
|
103
|
+
async function borrowTx(sdk, request) {
|
|
104
|
+
const { creditManager, wallet, ethAmount, sim } = request;
|
|
105
|
+
const { state } = sim.data;
|
|
106
|
+
const collateral = {
|
|
107
|
+
token: state.collateral.token.address,
|
|
108
|
+
balance: state.collateral.value
|
|
109
|
+
};
|
|
110
|
+
return sdk.accounts.openCA({
|
|
111
|
+
creditManager,
|
|
112
|
+
to: wallet,
|
|
113
|
+
collateral: [collateral],
|
|
114
|
+
ethAmount,
|
|
115
|
+
debt: state.totalDebt.value,
|
|
116
|
+
calls: state.calls,
|
|
117
|
+
withdrawToken: state.borrowed.token.address,
|
|
118
|
+
averageQuota: state.quotaIncrease,
|
|
119
|
+
minQuota: state.quotaIncrease,
|
|
120
|
+
reopenCreditAccount: state.creditAccount,
|
|
121
|
+
permits: {},
|
|
122
|
+
referralCode: 0n,
|
|
123
|
+
rwaOptions: await openRwaOptions(sdk, request, collateral.token)
|
|
67
124
|
});
|
|
68
125
|
}
|
|
69
126
|
/**
|
|
70
127
|
* The documented `openCA` contract: ask the market for its open requirements
|
|
71
128
|
* and hand them back as operation args, with the caller's cached signatures
|
|
72
|
-
* attached. `undefined` on non-RWA markets and when no
|
|
129
|
+
* attached. `undefined` on non-RWA markets and when no token is named.
|
|
130
|
+
*
|
|
131
|
+
* `token` is the one the account ends up holding, which is what an RWA market
|
|
132
|
+
* gates on: the opening's target, and the borrow's collateral.
|
|
73
133
|
**/
|
|
74
|
-
async function openRwaOptions(sdk, request) {
|
|
75
|
-
if (!
|
|
76
|
-
const requirements = await sdk.accounts.getOpenAccountRequirements(request.wallet, request.creditManager, { tokenOutAddress:
|
|
134
|
+
async function openRwaOptions(sdk, request, token) {
|
|
135
|
+
if (!token) return;
|
|
136
|
+
const requirements = await sdk.accounts.getOpenAccountRequirements(request.wallet, request.creditManager, { tokenOutAddress: token });
|
|
77
137
|
if (requirements?.protocol !== "securitize") return;
|
|
78
138
|
return {
|
|
79
139
|
protocol: "securitize",
|
|
@@ -196,26 +196,33 @@ var PrepareApi = class extends require_onchain_base_MultichainConstruct.Multicha
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
+
* {@inheritDoc IOpportunitiesPrepare.openEmptyCreditAccount}
|
|
200
|
+
**/
|
|
201
|
+
async openEmptyCreditAccount(strategy) {
|
|
202
|
+
try {
|
|
203
|
+
const sdk = await this.#chain(strategy.chainId);
|
|
204
|
+
const at = stateBlock(sdk);
|
|
205
|
+
const result = await service(sdk).openEmptyAccountIntent({
|
|
206
|
+
sdk,
|
|
207
|
+
creditManager: strategy.creditManager
|
|
208
|
+
});
|
|
209
|
+
return result.ok ? require_model_result.sdkOk(at) : methodError(result);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return require_model_result.sdkErr(require_model_errors_prepare_errors.unexpectedFailure(e));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
199
215
|
* {@inheritDoc IOpportunitiesPrepare.openNewStrategy}
|
|
200
216
|
**/
|
|
201
217
|
async openNewStrategy(strategy, params) {
|
|
202
218
|
try {
|
|
203
219
|
const sdk = await this.#chain(strategy.chainId);
|
|
204
220
|
const at = stateBlock(sdk);
|
|
205
|
-
if (params.empty) return opened(await service(sdk).openStrategyIntent({
|
|
206
|
-
sdk,
|
|
207
|
-
creditManager: strategy.creditManager,
|
|
208
|
-
empty: true
|
|
209
|
-
}), at);
|
|
210
221
|
const targetToken = params.targetToken ?? sdk.marketRegister.findCreditManager(strategy.creditManager).strategyTargetCollateral;
|
|
211
222
|
if (!targetToken) return require_model_result.sdkErr(require_model_errors_prepare_errors.noStrategyTargetCollateral(strategy.creditManager));
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
if (!reused || !(0, viem.isAddressEqual)(reused.creditManager, strategy.creditManager)) return require_model_result.sdkErr(require_model_errors_prepare_errors.creditAccountNotFound(params.creditAccount));
|
|
216
|
-
if (reused.totalDebt > 0n || reused.tokens.some((t) => t.quota > 0n)) return require_model_result.sdkErr(require_model_errors_prepare_errors.creditAccountNotEmpty(params.creditAccount));
|
|
217
|
-
creditAccount = reused;
|
|
218
|
-
}
|
|
223
|
+
const reused = await reusable(sdk, strategy, params.creditAccount);
|
|
224
|
+
if (reused && "error" in reused) return reused;
|
|
225
|
+
const creditAccount = reused?.account;
|
|
219
226
|
return opened(await service(sdk).openStrategyIntent({
|
|
220
227
|
sdk,
|
|
221
228
|
creditManager: strategy.creditManager,
|
|
@@ -232,6 +239,30 @@ var PrepareApi = class extends require_onchain_base_MultichainConstruct.Multicha
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
/**
|
|
242
|
+
* {@inheritDoc IOpportunitiesPrepare.borrow}
|
|
243
|
+
**/
|
|
244
|
+
async borrow(strategy, params) {
|
|
245
|
+
try {
|
|
246
|
+
const sdk = await this.#chain(strategy.chainId);
|
|
247
|
+
const at = stateBlock(sdk);
|
|
248
|
+
const reused = await reusable(sdk, strategy, params.creditAccount);
|
|
249
|
+
if (reused && "error" in reused) return reused;
|
|
250
|
+
return borrowed(await service(sdk).borrowIntent({
|
|
251
|
+
sdk,
|
|
252
|
+
creditManager: strategy.creditManager,
|
|
253
|
+
collateralToken: params.collateralToken,
|
|
254
|
+
collateralAmount: params.collateralAmount,
|
|
255
|
+
borrowToken: params.borrowToken,
|
|
256
|
+
borrowAmount: params.borrowAmount,
|
|
257
|
+
slippage: params.slippage,
|
|
258
|
+
quotaReserve: params.quotaReserve,
|
|
259
|
+
creditAccount: reused?.account
|
|
260
|
+
}), at);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return require_model_result.sdkErr(require_model_errors_prepare_errors.unexpectedFailure(e));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
235
266
|
* {@inheritDoc IOpportunitiesPrepare.depositStrategy}
|
|
236
267
|
**/
|
|
237
268
|
async depositStrategy(position, params) {
|
|
@@ -354,6 +385,21 @@ var PrepareApi = class extends require_onchain_base_MultichainConstruct.Multicha
|
|
|
354
385
|
});
|
|
355
386
|
}
|
|
356
387
|
/**
|
|
388
|
+
* {@inheritDoc IOpportunitiesPrepare.maxBorrow}
|
|
389
|
+
**/
|
|
390
|
+
maxBorrow(strategy, params) {
|
|
391
|
+
const sdk = this.sdk.chain(strategy.chainId);
|
|
392
|
+
return service(sdk).maxBorrow({
|
|
393
|
+
sdk,
|
|
394
|
+
creditManager: strategy.creditManager,
|
|
395
|
+
collateralToken: params.collateralToken,
|
|
396
|
+
collateralAmount: params.collateralAmount,
|
|
397
|
+
borrowToken: params.borrowToken,
|
|
398
|
+
targetHF: params.targetHF,
|
|
399
|
+
quotaReserve: params.quotaReserve
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
357
403
|
* The account a bare `max*` read weighs. These reads answer a number, not
|
|
358
404
|
* an envelope, so an account the markets do not hold is thrown rather than
|
|
359
405
|
* described, see {@link IOpportunitiesPrepare.maxWithdraw}.
|
|
@@ -439,6 +485,25 @@ async function slice(sdk, creditAccount) {
|
|
|
439
485
|
return data && require_onchain_accounts_intents_utils_credit_account_slice.toCreditAccountSlice(data);
|
|
440
486
|
}
|
|
441
487
|
/**
|
|
488
|
+
* The pre-opened account a request asks to be run on, held to what "pre-opened"
|
|
489
|
+
* means: this manager's, owing nothing and holding no quota.
|
|
490
|
+
*
|
|
491
|
+
* Shared by the two flows that put something on a fresh account — an opening
|
|
492
|
+
* and a borrow — so both hold a reused one to the same terms. Whatever
|
|
493
|
+
* balances sit on it are left to the flow: an opening routes them, a borrow
|
|
494
|
+
* leaves them where they are.
|
|
495
|
+
*
|
|
496
|
+
* @returns Nothing when the request named no account, the refusal to answer
|
|
497
|
+
* with when it named one that does not qualify, and the slice otherwise
|
|
498
|
+
**/
|
|
499
|
+
async function reusable(sdk, strategy, creditAccount) {
|
|
500
|
+
if (!creditAccount) return;
|
|
501
|
+
const account = await slice(sdk, creditAccount);
|
|
502
|
+
if (!account || !(0, viem.isAddressEqual)(account.creditManager, strategy.creditManager)) return require_model_result.sdkErr(require_model_errors_prepare_errors.creditAccountNotFound(creditAccount));
|
|
503
|
+
if (account.totalDebt > 0n || account.tokens.some((t) => t.quota > 0n)) return require_model_result.sdkErr(require_model_errors_prepare_errors.creditAccountNotEmpty(creditAccount));
|
|
504
|
+
return { account };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
442
507
|
* The operation a claim resumes, or `undefined` when there is none to resume:
|
|
443
508
|
* a withdrawal requested without an intent, or one read through a compressor
|
|
444
509
|
* too old to report it. Every intent the engine records can be finished,
|
|
@@ -538,6 +603,15 @@ function opened(result, at) {
|
|
|
538
603
|
}
|
|
539
604
|
/**
|
|
540
605
|
* {@inheritDoc planned}
|
|
606
|
+
**/
|
|
607
|
+
function borrowed(result, at) {
|
|
608
|
+
return result.ok ? require_model_result.sdkOk({
|
|
609
|
+
state: result.state,
|
|
610
|
+
...at
|
|
611
|
+
}) : methodError(result);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* {@inheritDoc planned}
|
|
541
615
|
*
|
|
542
616
|
* Both routes are payload, error and all: `errors` says why a missing one is
|
|
543
617
|
* missing, and it stays on the error when neither route answered, since that is
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeAndWait } from "./kycUtils.js";
|
|
2
|
-
import { parseAbi, parseEther } from "viem";
|
|
2
|
+
import { parseAbi, parseEther, toFunctionSelector } from "viem";
|
|
3
3
|
//#region src/dev/midasUtils.ts
|
|
4
4
|
/**
|
|
5
5
|
* Midas vaults inherit their own `Pausable`, whose global pause is guarded by
|
|
@@ -9,14 +9,24 @@ const iMidasPausableVaultAbi = parseAbi([
|
|
|
9
9
|
"function paused() external view returns (bool)",
|
|
10
10
|
"function pause() external",
|
|
11
11
|
"function unpause() external",
|
|
12
|
+
"function fnPaused(bytes4 fn) external view returns (bool)",
|
|
13
|
+
"function pauseFn(bytes4 fn) external",
|
|
14
|
+
"function unpauseFn(bytes4 fn) external",
|
|
12
15
|
"function pauseAdminRole() external view returns (bytes32)",
|
|
13
16
|
"function accessControl() external view returns (address)"
|
|
14
17
|
]);
|
|
15
18
|
const iMidasAccessControlAbi = parseAbi(["function hasRole(bytes32 role, address account) external view returns (bool)", "function grantRole(bytes32 role, address account) external"]);
|
|
16
19
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Selector for `DepositVault.depositInstant(address,uint256,uint256,bytes32)`,
|
|
21
|
+
* the 4-arg mint Gearbox adapters encode. Distinct from the custom-recipient
|
|
22
|
+
* overload.
|
|
23
|
+
*/
|
|
24
|
+
const DEPOSIT_INSTANT_SELECTOR = toFunctionSelector("depositInstant(address,uint256,uint256,bytes32)");
|
|
25
|
+
/**
|
|
26
|
+
* Unpauses a Midas issuance vault on an anvil fork so that `depositInstant`
|
|
27
|
+
* stops reverting with `Pausable: paused` (global) or `Pausable: fn paused`
|
|
28
|
+
* (per-selector), and returns a callback that restores the original pause
|
|
29
|
+
* state of each layer that was cleared.
|
|
20
30
|
*
|
|
21
31
|
* Impersonates `admin` and grants it `pauseAdminRole()` when missing, same as
|
|
22
32
|
* `greenlistMidasGateway` does with the greenlist roles. The grant is not
|
|
@@ -24,11 +34,20 @@ const iMidasAccessControlAbi = parseAbi(["function hasRole(bytes32 role, address
|
|
|
24
34
|
*/
|
|
25
35
|
async function unpauseMidasIssuanceVault(props) {
|
|
26
36
|
const { anvil, vault, admin, logger } = props;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
const [paused, depositInstantPaused] = await anvil.multicall({
|
|
38
|
+
allowFailure: false,
|
|
39
|
+
contracts: [{
|
|
40
|
+
address: vault,
|
|
41
|
+
abi: iMidasPausableVaultAbi,
|
|
42
|
+
functionName: "paused"
|
|
43
|
+
}, {
|
|
44
|
+
address: vault,
|
|
45
|
+
abi: iMidasPausableVaultAbi,
|
|
46
|
+
functionName: "fnPaused",
|
|
47
|
+
args: [DEPOSIT_INSTANT_SELECTOR]
|
|
48
|
+
}]
|
|
49
|
+
});
|
|
50
|
+
if (!paused && !depositInstantPaused) {
|
|
32
51
|
logger?.debug(`midas: issuance vault ${vault} is not paused`);
|
|
33
52
|
return async () => {};
|
|
34
53
|
}
|
|
@@ -50,7 +69,7 @@ async function unpauseMidasIssuanceVault(props) {
|
|
|
50
69
|
functionName: "hasRole",
|
|
51
70
|
args: [pauseAdminRole, admin]
|
|
52
71
|
});
|
|
53
|
-
logger?.debug(`midas: unpausing issuance vault ${vault} as ${admin}, access control ${accessControl}, pause admin role ${pauseAdminRole}`);
|
|
72
|
+
logger?.debug(`midas: unpausing issuance vault ${vault} as ${admin}, access control ${accessControl}, pause admin role ${pauseAdminRole}` + (paused ? ", global" : "") + (depositInstantPaused ? ", depositInstant" : ""));
|
|
54
73
|
await anvil.impersonateAccount({ address: admin });
|
|
55
74
|
try {
|
|
56
75
|
await anvil.setBalance({
|
|
@@ -68,13 +87,21 @@ async function unpauseMidasIssuanceVault(props) {
|
|
|
68
87
|
});
|
|
69
88
|
logger?.debug(`midas: granted pause admin role to ${admin}`);
|
|
70
89
|
}
|
|
71
|
-
await writeAndWait(anvil, {
|
|
90
|
+
if (paused) await writeAndWait(anvil, {
|
|
72
91
|
account: admin,
|
|
73
92
|
chain: anvil.chain,
|
|
74
93
|
address: vault,
|
|
75
94
|
abi: iMidasPausableVaultAbi,
|
|
76
95
|
functionName: "unpause"
|
|
77
96
|
});
|
|
97
|
+
if (depositInstantPaused) await writeAndWait(anvil, {
|
|
98
|
+
account: admin,
|
|
99
|
+
chain: anvil.chain,
|
|
100
|
+
address: vault,
|
|
101
|
+
abi: iMidasPausableVaultAbi,
|
|
102
|
+
functionName: "unpauseFn",
|
|
103
|
+
args: [DEPOSIT_INSTANT_SELECTOR]
|
|
104
|
+
});
|
|
78
105
|
} finally {
|
|
79
106
|
await anvil.stopImpersonatingAccount({ address: admin });
|
|
80
107
|
}
|
|
@@ -89,13 +116,21 @@ async function unpauseMidasIssuanceVault(props) {
|
|
|
89
116
|
address: admin,
|
|
90
117
|
value: parseEther("100")
|
|
91
118
|
});
|
|
92
|
-
await writeAndWait(anvil, {
|
|
119
|
+
if (paused) await writeAndWait(anvil, {
|
|
93
120
|
account: admin,
|
|
94
121
|
chain: anvil.chain,
|
|
95
122
|
address: vault,
|
|
96
123
|
abi: iMidasPausableVaultAbi,
|
|
97
124
|
functionName: "pause"
|
|
98
125
|
});
|
|
126
|
+
if (depositInstantPaused) await writeAndWait(anvil, {
|
|
127
|
+
account: admin,
|
|
128
|
+
chain: anvil.chain,
|
|
129
|
+
address: vault,
|
|
130
|
+
abi: iMidasPausableVaultAbi,
|
|
131
|
+
functionName: "pauseFn",
|
|
132
|
+
args: [DEPOSIT_INSTANT_SELECTOR]
|
|
133
|
+
});
|
|
99
134
|
} catch (e) {
|
|
100
135
|
logger?.warn(`midas: failed to pause issuance vault ${vault} back: ${e}`);
|
|
101
136
|
} finally {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { insufficientBalance } from "../../../model/errors/operation-errors.js";
|
|
2
|
+
import { unsupportedCollateralToken } from "../../../model/errors/prepare-errors.js";
|
|
3
|
+
import "../../../model/index.js";
|
|
4
|
+
import { toToken } from "../../validation/helpers/token.js";
|
|
5
|
+
import { IntentPreviewError } from "../../validation/raise.js";
|
|
6
|
+
import "../../validation/index.js";
|
|
7
|
+
import { eq, toTargetDecimals } from "./utils/common.js";
|
|
8
|
+
import { assertCanBorrow, assertCollateralised, assertGrowthAllowed, assertMarketOperable, assertQuotaAvailable } from "./guards.js";
|
|
9
|
+
import { assertDebtLimits } from "./math.js";
|
|
10
|
+
import { unopenedAccountSlice } from "./utils/credit-account-slice.js";
|
|
11
|
+
import { collectPriceImpact } from "./utils/price-impact.js";
|
|
12
|
+
import { getQuotasForUpdate } from "./utils/quotas-for-update.js";
|
|
13
|
+
import { createRouterPaths } from "./utils/router-path.js";
|
|
14
|
+
import "./utils/index.js";
|
|
15
|
+
//#region src/onchain/accounts/intents/borrow.ts
|
|
16
|
+
/**
|
|
17
|
+
* Builds the state a borrow would leave behind.
|
|
18
|
+
*
|
|
19
|
+
* The debt is named rather than derived: a borrow asks for an amount, where an
|
|
20
|
+
* opening asks for a leverage and lets the collateral decide. Everything the
|
|
21
|
+
* loan pays out leaves the account, so the collateral alone backs it — which
|
|
22
|
+
* is what makes the health factor here a straight function of the two amounts.
|
|
23
|
+
*
|
|
24
|
+
* Produces no operation list, as opening never does: the caller hands the
|
|
25
|
+
* numbers and the calls to `sdk.accounts.openCA`, with `withdrawToken` set to
|
|
26
|
+
* the payout.
|
|
27
|
+
*/
|
|
28
|
+
async function buildBorrowState(props) {
|
|
29
|
+
const { sdk, creditManager, collateralAmount, borrowAmount, slippage = 0, quotaReserve, creditAccount: existing } = props;
|
|
30
|
+
const suite = sdk.marketRegister.findCreditManager(creditManager);
|
|
31
|
+
const market = sdk.marketRegister.findByCreditManager(creditManager);
|
|
32
|
+
assertMarketOperable(suite);
|
|
33
|
+
const underlying = market.pool.underlying.toLowerCase();
|
|
34
|
+
const collateralToken = props.collateralToken.toLowerCase();
|
|
35
|
+
const borrowToken = props.borrowToken.toLowerCase();
|
|
36
|
+
const convert = (from, to, amount) => market.priceOracle.safeConvert(from, to, amount).value;
|
|
37
|
+
if (eq(collateralToken, borrowToken)) throw new IntentPreviewError(unsupportedCollateralToken(toToken(sdk, collateralToken)), "borrow: the payout token cannot also be the collateral");
|
|
38
|
+
const rwaAsset = sdk.tokensMeta.rwaUnderlyings.get(underlying)?.asset?.toLowerCase();
|
|
39
|
+
const unwrapsPayout = !!rwaAsset && eq(borrowToken, rwaAsset);
|
|
40
|
+
if (rwaAsset && eq(borrowToken, underlying)) throw new IntentPreviewError(unsupportedCollateralToken(toToken(sdk, borrowToken)), `borrow: ${underlying} cannot leave the account, ask for the payout in ${rwaAsset}`);
|
|
41
|
+
if (collateralAmount <= 0n) throw new IntentPreviewError(insufficientBalance(), "borrow: no collateral supplied");
|
|
42
|
+
if (borrowAmount <= 0n) throw new IntentPreviewError(insufficientBalance(), "borrow: nothing to borrow");
|
|
43
|
+
const margin = convert(collateralToken, underlying, collateralAmount);
|
|
44
|
+
if (margin <= 0n) throw new IntentPreviewError(insufficientBalance(), "borrow: collateral is worth nothing in underlying");
|
|
45
|
+
const debt = eq(borrowToken, underlying) ? borrowAmount : unwrapsPayout ? toTargetDecimals(borrowAmount, borrowToken, underlying, sdk) : convert(borrowToken, underlying, borrowAmount);
|
|
46
|
+
assertDebtLimits(sdk, debt, suite.creditFacade, underlying);
|
|
47
|
+
assertCanBorrow(sdk, suite, debt);
|
|
48
|
+
const account = existing ?? unopenedAccountSlice({
|
|
49
|
+
creditManager,
|
|
50
|
+
creditFacade: suite.creditFacade.address,
|
|
51
|
+
underlying
|
|
52
|
+
});
|
|
53
|
+
const leg = eq(borrowToken, underlying) || unwrapsPayout ? void 0 : await createRouterPaths({
|
|
54
|
+
sdk,
|
|
55
|
+
creditAccount: account,
|
|
56
|
+
slippage
|
|
57
|
+
}).swap({
|
|
58
|
+
tokenIn: underlying,
|
|
59
|
+
tokenOut: borrowToken,
|
|
60
|
+
amount: debt,
|
|
61
|
+
keep: eq(collateralToken, underlying) ? collateralAmount : 0n
|
|
62
|
+
});
|
|
63
|
+
const unwrap = unwrapsPayout ? await sdk.accounts.assembleRWAUnwrapCalls(debt, creditManager) : void 0;
|
|
64
|
+
if (unwrapsPayout && !unwrap) throw new Error(`borrow: no unwrap calls found for ${borrowToken}`);
|
|
65
|
+
const assets = [{
|
|
66
|
+
token: collateralToken,
|
|
67
|
+
balance: collateralAmount
|
|
68
|
+
}];
|
|
69
|
+
const quotaIncrease = borrowCollateralQuota({
|
|
70
|
+
sdk,
|
|
71
|
+
creditManager,
|
|
72
|
+
assets,
|
|
73
|
+
quotaReserve
|
|
74
|
+
});
|
|
75
|
+
assertGrowthAllowed({
|
|
76
|
+
sdk,
|
|
77
|
+
suite,
|
|
78
|
+
market,
|
|
79
|
+
before: [],
|
|
80
|
+
after: assets
|
|
81
|
+
});
|
|
82
|
+
assertQuotaAvailable(sdk, market, quotaIncrease);
|
|
83
|
+
const snapshot = {
|
|
84
|
+
creditManager,
|
|
85
|
+
assets,
|
|
86
|
+
quotas: quotaIncrease,
|
|
87
|
+
totalDebt: debt,
|
|
88
|
+
totalValue: margin
|
|
89
|
+
};
|
|
90
|
+
const projection = sdk.positions.projection(snapshot, { availableLiquidityChange: -debt });
|
|
91
|
+
assertCollateralised(projection.safeHealthFactor, true);
|
|
92
|
+
const priced = (token, balance) => market.priceOracle.toTokenAmount(token, balance);
|
|
93
|
+
const payout = unwrapsPayout ? toTargetDecimals(debt, underlying, borrowToken, sdk) : leg ? leg.amount : debt;
|
|
94
|
+
return {
|
|
95
|
+
...projection,
|
|
96
|
+
currentPrice: sdk.positions.currentPrice(snapshot),
|
|
97
|
+
executionCost: void 0,
|
|
98
|
+
priceImpact: await collectPriceImpact(leg?.probe ? [leg.probe] : [], {
|
|
99
|
+
totalValue: margin,
|
|
100
|
+
netValue: margin - debt,
|
|
101
|
+
toUnderlying: (from, amount) => convert(from, underlying, amount)
|
|
102
|
+
}),
|
|
103
|
+
collateral: priced(collateralToken, collateralAmount),
|
|
104
|
+
borrowed: priced(borrowToken, payout),
|
|
105
|
+
minBorrowed: priced(borrowToken, leg ? leg.minAmount : payout),
|
|
106
|
+
slippage,
|
|
107
|
+
quotaIncrease,
|
|
108
|
+
calls: leg ? [...leg.calls] : unwrap ?? [],
|
|
109
|
+
creditAccount: existing?.creditAccount
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Quota a borrow buys for its collateral on an account that holds none yet.
|
|
114
|
+
*
|
|
115
|
+
* Shared with `maxBorrow`, because a quota short of the collateral's weighted
|
|
116
|
+
* value is what caps the loan: the two have to read the same number or the
|
|
117
|
+
* ceiling one offers is one the other refuses.
|
|
118
|
+
*
|
|
119
|
+
* @param props - {@link BorrowCollateralQuotaProps}
|
|
120
|
+
**/
|
|
121
|
+
function borrowCollateralQuota({ sdk, creditManager, assets, quotaReserve }) {
|
|
122
|
+
const suite = sdk.marketRegister.findCreditManager(creditManager);
|
|
123
|
+
const market = sdk.marketRegister.findByCreditManager(creditManager);
|
|
124
|
+
return getQuotasForUpdate({
|
|
125
|
+
assetsBefore: [],
|
|
126
|
+
assetsAfter: assets,
|
|
127
|
+
initialQuotas: [],
|
|
128
|
+
quotaReserve,
|
|
129
|
+
underlyingToken: market.pool.underlying,
|
|
130
|
+
liquidationThresholds: suite.creditManager.liquidationThresholds,
|
|
131
|
+
quotas: market.pool.pqk.quotas,
|
|
132
|
+
maxDebt: suite.creditFacade.maxDebt,
|
|
133
|
+
convert: (from, to, amount) => market.priceOracle.safeConvert(from, to, amount).value
|
|
134
|
+
}).quotaIncrease;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { borrowCollateralQuota, buildBorrowState };
|