@gearbox-protocol/sdk 15.1.0-next.21 → 15.1.0-next.23

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.
Files changed (29) hide show
  1. package/dist/cjs/new-sdk/prepare/PrepareApi.js +4 -4
  2. package/dist/cjs/sdk/accounts/intents/index.js +57 -28
  3. package/dist/cjs/sdk/accounts/intents/plan.js +40 -2
  4. package/dist/cjs/sdk/accounts/intents/realize.js +22 -12
  5. package/dist/cjs/sdk/accounts/intents/tail.js +120 -0
  6. package/dist/cjs/sdk/accounts/intents/utils/index.js +1 -0
  7. package/dist/cjs/sdk/accounts/intents/utils/router-path.js +32 -0
  8. package/dist/cjs/sdk/market/oracle/PriceOracleBaseContract.js +11 -0
  9. package/dist/cjs/sdk/positions/PositionsService.js +22 -11
  10. package/dist/esm/new-sdk/prepare/PrepareApi.js +4 -4
  11. package/dist/esm/sdk/accounts/intents/index.js +58 -29
  12. package/dist/esm/sdk/accounts/intents/plan.js +40 -3
  13. package/dist/esm/sdk/accounts/intents/realize.js +22 -12
  14. package/dist/esm/sdk/accounts/intents/tail.js +118 -0
  15. package/dist/esm/sdk/accounts/intents/utils/index.js +2 -2
  16. package/dist/esm/sdk/accounts/intents/utils/router-path.js +32 -1
  17. package/dist/esm/sdk/market/oracle/PriceOracleBaseContract.js +11 -0
  18. package/dist/esm/sdk/positions/PositionsService.js +22 -11
  19. package/dist/types/new-sdk/prepare/types.d.ts +22 -8
  20. package/dist/types/sdk/accounts/intents/index.d.ts +13 -6
  21. package/dist/types/sdk/accounts/intents/plan.d.ts +18 -2
  22. package/dist/types/sdk/accounts/intents/realize.d.ts +7 -0
  23. package/dist/types/sdk/accounts/intents/tail.d.ts +52 -0
  24. package/dist/types/sdk/accounts/intents/types.d.ts +46 -10
  25. package/dist/types/sdk/accounts/intents/utils/index.d.ts +2 -2
  26. package/dist/types/sdk/accounts/intents/utils/router-path.d.ts +15 -1
  27. package/dist/types/sdk/market/oracle/PriceOracleBaseContract.d.ts +4 -0
  28. package/dist/types/sdk/market/oracle/types.d.ts +9 -0
  29. package/package.json +1 -1
@@ -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 { instantOutput } from "./operations.js";
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 and what it recorded for the tail, or
85
- * `{ ok: false, reason }` — `noDelayedRoute` when this route does not exist
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
- if (!result.delayed) throw new Error("startDelayedIntent: plan started no withdrawal");
100
- return {
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: result.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 two operations that can genuinely be interrupted by a delay — a
153
- * withdrawal and a deleveraging. For the rest, the claim is the whole tail:
154
- * the tokens land on the account and only their quota has to catch up.
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
- const { intent, claimable } = props;
163
- return plain(await this.#preview(props, () => {
164
- const view = accountView(props.creditAccount, props.sdk);
165
- const claimed = () => {
166
- const output = instantOutput(claimable.outputs);
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) throw new IntentPreviewError("noDelayedRoute", "withdraw: taking the whole net value cannot be started as a redemption");
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: preview.outputs.some((o) => o.isDelayed) ? "delayed" : "instant"
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 };
@@ -137,6 +137,17 @@ var PriceOracleBaseContract = class extends BaseContract {
137
137
  return amount * 10n ** BigInt(this.tokensMeta.decimals(token)) / price;
138
138
  }
139
139
  /**
140
+ * {@inheritDoc IPriceOracleContract.safeConvert}
141
+ **/
142
+ safeConvert(from, to, amount) {
143
+ try {
144
+ return this.convert(from, to, amount);
145
+ } catch (e) {
146
+ this.logger?.debug(`cannot convert ${this.labelAddress(from)} to ${this.labelAddress(to)}: ${e}`);
147
+ return null;
148
+ }
149
+ }
150
+ /**
140
151
  * {@inheritDoc IPriceOracleContract.safeConvertToUSD}
141
152
  **/
142
153
  safeConvertToUSD(token, amount) {
@@ -153,6 +153,24 @@ var PositionsService = class extends SDKConstruct {
153
153
  const borrowRate = this.borrowRate(snapshot);
154
154
  const timeToLiquidation = this.timeToLiquidation(snapshot);
155
155
  const liquidationPrice = this.liquidationPrice(snapshot);
156
+ const zeroDebt = ca.debt === 0n;
157
+ const collaterals = [];
158
+ let totalValue = zeroDebt ? 0n : ca.totalValue;
159
+ let totalValueUSD = zeroDebt ? 0n : ca.totalValueUSD;
160
+ for (const t of ca.tokens) {
161
+ if (t.balance <= 10n) continue;
162
+ collaterals.push({
163
+ collateral: priceOracle.toTokenAmount(t.token, t.balance),
164
+ quota: priceOracle.toTokenAmount(market.underlying, t.quota),
165
+ withdrawals: withdrawals.get(t.token) ?? []
166
+ });
167
+ if (zeroDebt) {
168
+ const value = priceOracle.safeConvert(t.token, market.underlying, t.balance) || 0n;
169
+ totalValue += value;
170
+ const usd = priceOracle.safeConvertToUSD(t.token, t.balance) || 0n;
171
+ totalValueUSD += usd;
172
+ }
173
+ }
156
174
  return {
157
175
  kind: "strategy",
158
176
  chainId: this.sdk.chainId,
@@ -160,7 +178,7 @@ var PositionsService = class extends SDKConstruct {
160
178
  creditAccount: ca.creditAccount,
161
179
  name: target ? strategyName(this.sdk.tokensMeta.mustGetToken(target), token, this.sdk.chainId) : token.symbol,
162
180
  targetCollateral: target ? this.sdk.tokensMeta.mustGetToken(target) : null,
163
- leverage: calcPositionLeverage(ca.totalValue, totalDebtValue),
181
+ leverage: calcPositionLeverage(totalValue, totalDebtValue),
164
182
  borrowApy: calcBorrowApy(pool.baseInterestRate, suite.creditManager.feeInterest),
165
183
  totalDebt: {
166
184
  token,
@@ -169,21 +187,14 @@ var PositionsService = class extends SDKConstruct {
169
187
  },
170
188
  totalValue: {
171
189
  token,
172
- value: ca.totalValue,
173
- valueUsd: usdToNumber(ca.totalValueUSD)
190
+ value: totalValue,
191
+ valueUsd: usdToNumber(totalValueUSD)
174
192
  },
175
193
  healthFactor: healthFactorBps(ca.healthFactor),
176
194
  borrowRate,
177
195
  timeToLiquidation,
178
196
  liquidationPrice,
179
- collaterals: ca.tokens.flatMap((t) => {
180
- if (t.balance <= 10n) return [];
181
- return [{
182
- collateral: priceOracle.toTokenAmount(t.token, t.balance),
183
- quota: priceOracle.toTokenAmount(market.underlying, t.quota),
184
- withdrawals: withdrawals.get(t.token) ?? []
185
- }];
186
- })
197
+ collaterals
187
198
  };
188
199
  }
189
200
  /**
@@ -73,10 +73,12 @@ type StrategySimulate = {
73
73
  };
74
74
  /**
75
75
  * What the leading half of a delayed operation would yield: the request
76
- * transaction, plus what it recorded for the tail.
76
+ * transaction, plus what it recorded for the tail and where that tail leads.
77
77
  *
78
78
  * Shaped like {@link StrategySimulate} with one field more, so the instant and
79
- * the delayed route of the same request are compared side by side.
79
+ * the delayed route of the same request are compared side by side — and they
80
+ * are meant to be compared on the same footing, so `preview` is the end of the
81
+ * operation in both, not the end of the transaction.
80
82
  **/
81
83
  type DelayedStrategySimulate = {
82
84
  ok: true;
@@ -85,9 +87,14 @@ type DelayedStrategySimulate = {
85
87
  **/
86
88
  operations: AccountCalculatorOperation[];
87
89
  /**
88
- * State once the request executes: the source token is gone and the
89
- * withdrawal position stands in its place, with debt untouched the
90
- * repayment belongs to the tail.
90
+ * Where the operation ends: the account once the redemption has matured,
91
+ * been claimed and the tail has run the same place the instant route
92
+ * reaches in one transaction, which is what makes the two comparable.
93
+ *
94
+ * The tail's half of it is an estimate priced by the oracle: the funds it
95
+ * trades do not exist yet, so no route can be quoted for them. The state
96
+ * the request alone lands in — source spent, withdrawal position in its
97
+ * place, debt untouched — is `delayed.afterRequest`.
91
98
  **/
92
99
  preview: OperationState;
93
100
  /**
@@ -398,7 +405,13 @@ interface OpportunitiesPrepare {
398
405
  * many-to-one route, the debt is settled in full and every balance left goes
399
406
  * to `to`. `tokenOut` is ignored, since the proceeds are already the
400
407
  * underlying (unwrapped on an RWA market). The account stays open with
401
- * nothing on it. An exit has no delayed route.
408
+ * nothing on it.
409
+ *
410
+ * An exit has both routes as well, which is what lets an account whose
411
+ * position only redeems through its issuer leave at all: the delayed route
412
+ * redeems `sourceToken` whole and records the exit, and {@link finalize}
413
+ * rebuilds it against the account the claim finds — the debt as it stands by
414
+ * then, whatever else is on the account, everything sold in one route.
402
415
  *
403
416
  * @see withdrawCollateral to move an asset out without touching debt, which
404
417
  * raises leverage instead.
@@ -463,8 +476,9 @@ interface OpportunitiesPrepare {
463
476
  /**
464
477
  * The tail of a delayed route: claim the matured withdrawal, then whatever the
465
478
  * operation that requested it still owes — repaying debt and paying the wallet
466
- * out for a withdrawal, repaying alone for a deleveraging, nothing beyond the
467
- * claim for the rest.
479
+ * out for a withdrawal, repaying alone for a deleveraging, selling the rest of
480
+ * the account and settling the loan for an exit, nothing beyond the claim for
481
+ * the rest.
468
482
  *
469
483
  * The route is requested by {@link withdrawStrategy} or
470
484
  * {@link adjustLeverage}, whose `delayed` branch is the transaction that
@@ -75,10 +75,16 @@ declare class CreditAccountOperationsService extends SDKConstruct {
75
75
  * {@link intentRoutes}; this is the one to call when the delayed route is the
76
76
  * only one of interest.
77
77
  *
78
+ * `preview` is where the intent ends — the account once the redemption has
79
+ * matured, been claimed and the tail has run — because that is what the
80
+ * caller asked for; the half-way state the request itself lands in is
81
+ * `delayed.afterRequest`. Both are validated, so a request whose tail could
82
+ * not be completed is refused instead of started.
83
+ *
78
84
  * @param props - Intent plus account slice, quota reserve and slippage
79
- * @returns The request transaction and what it recorded for the tail, or
80
- * `{ ok: false, reason }` — `noDelayedRoute` when this route does not exist
81
- * for the account at all
85
+ * @returns The request transaction, the state it ends in and what it recorded
86
+ * for the tail, or `{ ok: false, reason }` — `noDelayedRoute` when this route
87
+ * does not exist for the account at all
82
88
  */
83
89
  startDelayedIntent(props: StartIntentProps & {
84
90
  intent: DelayableIntent;
@@ -109,9 +115,10 @@ declare class CreditAccountOperationsService extends SDKConstruct {
109
115
  * Previews the tail of a delayed intent, once the withdrawal it started has
110
116
  * matured: the claim, then whatever the intent still owes.
111
117
  *
112
- * Serves the two operations that can genuinely be interrupted by a delay — a
113
- * withdrawal and a deleveraging. For the rest, the claim is the whole tail:
114
- * the tokens land on the account and only their quota has to catch up.
118
+ * Serves the three operations that can genuinely be interrupted by a delay —
119
+ * a withdrawal, a deleveraging and an exit. For the rest, the claim is the
120
+ * whole tail: the tokens land on the account and only their quota has to
121
+ * catch up.
115
122
  *
116
123
  * @param props - The recorded intent, the account slice as it stands now, and
117
124
  * the matured claimable
@@ -153,7 +153,8 @@ declare function planWithdraw(intent: Omit<WithdrawStrategyIntent, "type">, view
153
153
  *
154
154
  * The tail is planned at claim time by {@link planFinishWithdraw}, from the
155
155
  * intent this request records — only then is the claimed amount, and the token
156
- * it arrived in, known.
156
+ * it arrived in, known. An exit records {@link planFinishCloseAccount}'s intent
157
+ * instead, and rebuilds itself from the account rather than from the request.
157
158
  */
158
159
  declare function planWithdrawDelayed(intent: Omit<WithdrawStrategyIntent, "type">, view: AccountView): Step[];
159
160
  /**
@@ -185,7 +186,22 @@ declare function planFinishWithdraw(intent: {
185
186
  token: Address;
186
187
  amount: bigint;
187
188
  }, view: AccountView): Step[];
189
+ /**
190
+ * The tail of an exit: the claim lands, everything the account holds is sold
191
+ * into the underlying, the loan is settled out of the proceeds and the rest
192
+ * goes to the wallet. The account survives it, empty and owing nothing.
193
+ *
194
+ * Nothing is quoted from the request — the same shape {@link planWithdraw}
195
+ * builds for an instant exit is rebuilt here against the account as it stands
196
+ * now, which is the only state that can name these amounts.
197
+ */
198
+ declare function planFinishCloseAccount(intent: {
199
+ to: Address;
200
+ }, claimable: ClaimableWithdrawal, claimed: {
201
+ token: Address;
202
+ amount: bigint;
203
+ }, view: AccountView): Step[];
188
204
  /** Nothing is owed beyond the claim: the tokens land and quotas catch up. */
189
205
  declare function planFinishClaimOnly(claimable: ClaimableWithdrawal): Step[];
190
206
  //#endregion
191
- export { AccountView, Amount, RAISED, Step, planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planFinishClaimOnly, planFinishDecreaseLeverage, planFinishWithdraw, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed };
207
+ export { AccountView, Amount, RAISED, Step, planAddCollateral, planAdjustLeverage, planAdjustLeverageDelayed, planDeposit, planFinishClaimOnly, planFinishCloseAccount, planFinishDecreaseLeverage, planFinishWithdraw, planRepay, planWithdraw, planWithdrawAsset, planWithdrawDelayed };
@@ -4,6 +4,7 @@ import { CreditAccountSlice, DelayedStart, OperationState } from "./types.js";
4
4
  import { AccountCalculatorOperation } from "./operations.js";
5
5
  import "../../index.js";
6
6
  import { Step } from "./plan.js";
7
+ import { RouterPaths } from "./utils/router-path.js";
7
8
  //#region src/sdk/accounts/intents/realize.d.ts
8
9
  interface RealizeProps {
9
10
  creditAccount: CreditAccountSlice;
@@ -12,6 +13,12 @@ interface RealizeProps {
12
13
  slippage: number;
13
14
  /** Extra quota headroom in PERCENTAGE_FORMAT. */
14
15
  quotaReserve: number | undefined;
16
+ /**
17
+ * Where routed legs are quoted. Defaults to the pathfinder, which is what
18
+ * anything that will be sent needs; a walk that only projects a state passes
19
+ * `createOraclePaths` instead.
20
+ */
21
+ paths?: RouterPaths;
15
22
  }
16
23
  interface Realized {
17
24
  operations: AccountCalculatorOperation[];