@gearbox-protocol/sdk 16.0.0-next.41 → 16.0.0-next.42

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.
@@ -1,10 +1,10 @@
1
1
  import { hexEq } from "../../onchain/utils/hex.js";
2
2
  import { MultichainConstruct } from "../../onchain/base/MultichainConstruct.js";
3
- import { refuse } from "../../onchain/validation/refusal.js";
4
3
  import { toToken } from "../../onchain/validation/token.js";
5
- import { fetchCreditAccountSlice } from "../../onchain/accounts/intents/utils/credit-account-slice.js";
4
+ import { toCreditAccountSlice } from "../../onchain/accounts/intents/utils/credit-account-slice.js";
6
5
  import { CreditAccountOperationsService } from "../../onchain/accounts/intents/index.js";
7
6
  import "../../onchain/index.js";
7
+ import { creditAccountNotFound, noStrategyTargetCollateral, toPrepareError, unexpectedFailure } from "./errors.js";
8
8
  import { withdrawableCollaterals } from "./withdrawable-collaterals.js";
9
9
  //#region src/sdk/prepare/PrepareApi.ts
10
10
  /**
@@ -17,8 +17,13 @@ import { withdrawableCollaterals } from "./withdrawable-collaterals.js";
17
17
  *
18
18
  * A prepared operation names one chain, so it reads through
19
19
  * {@link MultichainConstruct.queryChain}: there is no second source to fall back
20
- * to, hence a chain the SDK does not cover, or one that fails the read, throws
21
- * rather than answering with empty metadata.
20
+ * to, hence a chain the SDK does not cover, or one that fails the read, is a
21
+ * failure of the whole request rather than a thinner answer.
22
+ *
23
+ * No method here throws. Every way a preparation can fail — the market's own
24
+ * refusals, the two the namespace decides itself, and anything the chain or the
25
+ * engine raises — comes back described in the envelope, see {@link PrepareError}.
26
+ * A caller writes one branch, not a branch and a `try`.
22
27
  **/
23
28
  var PrepareApi = class extends MultichainConstruct {
24
29
  #ensureFresh;
@@ -31,29 +36,77 @@ var PrepareApi = class extends MultichainConstruct {
31
36
  return super.queryChain(props);
32
37
  }
33
38
  /**
39
+ * Runs a one-chain request whose answer is an envelope, describing whatever
40
+ * the chain, the read or the engine throws on the way rather than letting it
41
+ * escape.
42
+ *
43
+ * So every method below answers instead of rejecting, and a caller has one
44
+ * thing to branch on. "This cannot be done" and "we could not find out" are
45
+ * still told apart, by `error.code`: only the second is
46
+ * `unexpectedFailure`, and only it marks the chain failed in `meta`.
47
+ **/
48
+ async #answer(chainId, run) {
49
+ try {
50
+ return await this.queryChain({
51
+ network: chainId,
52
+ run
53
+ });
54
+ } catch (e) {
55
+ return {
56
+ data: {
57
+ success: false,
58
+ error: unexpectedFailure(e)
59
+ },
60
+ meta: { chains: [failedChain(chainId, e)] }
61
+ };
62
+ }
63
+ }
64
+ /**
34
65
  * {@inheritDoc IOpportunitiesPrepare.finalize}
35
66
  **/
36
67
  async finalize(position, params) {
37
- return this.queryChain({
38
- network: position.chainId,
39
- run: async (sdk) => {
40
- const intent = resumable(params.intent ?? params.claimable.intent);
41
- if (!intent) return refuse("noRecordedIntent", void 0);
42
- return service(sdk).finishIntent({
43
- intent,
44
- claimable: toClaimableWithdrawal(params.claimable),
45
- creditAccount: await slice(sdk, position.creditAccount),
46
- sdk,
47
- slippage: params.slippage,
48
- quotaReserve: params.quotaReserve
49
- });
50
- }
68
+ return this.#answer(position.chainId, async (sdk) => {
69
+ const intent = resumable(params.intent ?? params.claimable.intent);
70
+ if (!intent) return {
71
+ success: false,
72
+ error: toPrepareError({
73
+ reason: "noRecordedIntent",
74
+ detail: void 0
75
+ })
76
+ };
77
+ const creditAccount = await slice(sdk, position.creditAccount);
78
+ if (!creditAccount) return {
79
+ success: false,
80
+ error: creditAccountNotFound(position.creditAccount)
81
+ };
82
+ return planned(await service(sdk).finishIntent({
83
+ intent,
84
+ claimable: toClaimableWithdrawal(params.claimable),
85
+ creditAccount,
86
+ sdk,
87
+ slippage: params.slippage,
88
+ quotaReserve: params.quotaReserve
89
+ }));
51
90
  });
52
91
  }
53
92
  /**
54
93
  * {@inheritDoc IOpportunitiesPrepare.deposit}
55
94
  **/
56
95
  deposit(pool, params) {
96
+ try {
97
+ return this.#depositPlan(pool, params);
98
+ } catch (e) {
99
+ return {
100
+ success: false,
101
+ error: unexpectedFailure(e)
102
+ };
103
+ }
104
+ }
105
+ /**
106
+ * The arithmetic behind {@link deposit}, which throws where the SDK holds
107
+ * nothing for the pool or the chain it was handed.
108
+ **/
109
+ #depositPlan(pool, params) {
57
110
  const chain = this.sdk.chain(pool.chainId);
58
111
  const { marketRegister, pools } = chain;
59
112
  const tokenIn = params.tokenIn ?? marketRegister.findByPool(pool.pool).pool.underlying;
@@ -76,16 +129,31 @@ var PrepareApi = class extends MultichainConstruct {
76
129
  });
77
130
  if (!call) return unroutable(chain, tokenIn, tokenOut);
78
131
  return {
79
- ok: true,
80
- operations: [],
81
- state,
82
- calls: call.calls
132
+ success: true,
133
+ data: {
134
+ operations: [],
135
+ state,
136
+ calls: call.calls
137
+ }
83
138
  };
84
139
  }
85
140
  /**
86
141
  * {@inheritDoc IOpportunitiesPrepare.withdraw}
87
142
  **/
88
143
  withdraw(pool, params) {
144
+ try {
145
+ return this.#withdrawPlan(pool, params);
146
+ } catch (e) {
147
+ return {
148
+ success: false,
149
+ error: unexpectedFailure(e)
150
+ };
151
+ }
152
+ }
153
+ /**
154
+ * {@inheritDoc PrepareApi.depositPlan}
155
+ **/
156
+ #withdrawPlan(pool, params) {
89
157
  const chain = this.sdk.chain(pool.chainId);
90
158
  const { pools } = chain;
91
159
  const tokenIn = params.tokenIn ?? pool.pool;
@@ -106,16 +174,31 @@ var PrepareApi = class extends MultichainConstruct {
106
174
  mode: "withdraw"
107
175
  });
108
176
  return {
109
- ok: true,
110
- operations: [],
111
- state,
112
- calls
177
+ success: true,
178
+ data: {
179
+ operations: [],
180
+ state,
181
+ calls
182
+ }
113
183
  };
114
184
  }
115
185
  /**
116
186
  * {@inheritDoc IOpportunitiesPrepare.redeem}
117
187
  **/
118
188
  redeem(pool, params) {
189
+ try {
190
+ return this.#redeemPlan(pool, params);
191
+ } catch (e) {
192
+ return {
193
+ success: false,
194
+ error: unexpectedFailure(e)
195
+ };
196
+ }
197
+ }
198
+ /**
199
+ * {@inheritDoc PrepareApi.depositPlan}
200
+ **/
201
+ #redeemPlan(pool, params) {
119
202
  const chain = this.sdk.chain(pool.chainId);
120
203
  const { pools } = chain;
121
204
  const tokenIn = params.tokenIn ?? pool.pool;
@@ -136,32 +219,34 @@ var PrepareApi = class extends MultichainConstruct {
136
219
  mode: "redeem"
137
220
  });
138
221
  return {
139
- ok: true,
140
- operations: [],
141
- state,
142
- calls
222
+ success: true,
223
+ data: {
224
+ operations: [],
225
+ state,
226
+ calls
227
+ }
143
228
  };
144
229
  }
145
230
  /**
146
231
  * {@inheritDoc IOpportunitiesPrepare.openNewStrategy}
147
232
  **/
148
233
  async openNewStrategy(strategy, params) {
149
- return this.queryChain({
150
- network: strategy.chainId,
151
- run: (sdk) => {
152
- const targetToken = params.targetToken ?? sdk.marketRegister.findCreditManager(strategy.creditManager).strategyTargetCollateral;
153
- if (!targetToken) throw new Error(`credit manager ${strategy.creditManager} has no strategy target collateral`);
154
- return service(sdk).openStrategyIntent({
155
- sdk,
156
- creditManager: strategy.creditManager,
157
- collateral: params.collateral,
158
- targetToken,
159
- leverage: params.leverage,
160
- leftoverBalances: params.leftoverBalances,
161
- slippage: params.slippage,
162
- quotaReserve: params.quotaReserve
163
- });
164
- }
234
+ return this.#answer(strategy.chainId, async (sdk) => {
235
+ const targetToken = params.targetToken ?? sdk.marketRegister.findCreditManager(strategy.creditManager).strategyTargetCollateral;
236
+ if (!targetToken) return {
237
+ success: false,
238
+ error: noStrategyTargetCollateral(strategy.creditManager)
239
+ };
240
+ return opened(await service(sdk).openStrategyIntent({
241
+ sdk,
242
+ creditManager: strategy.creditManager,
243
+ collateral: params.collateral,
244
+ targetToken,
245
+ leverage: params.leverage,
246
+ leftoverBalances: params.leftoverBalances,
247
+ slippage: params.slippage,
248
+ quotaReserve: params.quotaReserve
249
+ }));
165
250
  });
166
251
  }
167
252
  /**
@@ -193,12 +278,19 @@ var PrepareApi = class extends MultichainConstruct {
193
278
  * {@inheritDoc IOpportunitiesPrepare.maxWithdraw}
194
279
  **/
195
280
  async maxWithdraw(position) {
196
- return this.queryChain({
197
- network: position.chainId,
198
- run: async (sdk) => service(sdk).maxWithdraw({
199
- creditAccount: await slice(sdk, position.creditAccount),
200
- sdk
201
- })
281
+ return this.#answer(position.chainId, async (sdk) => {
282
+ const creditAccount = await slice(sdk, position.creditAccount);
283
+ if (!creditAccount) return {
284
+ success: false,
285
+ error: creditAccountNotFound(position.creditAccount)
286
+ };
287
+ return {
288
+ success: true,
289
+ data: await service(sdk).maxWithdraw({
290
+ creditAccount,
291
+ sdk
292
+ })
293
+ };
202
294
  });
203
295
  }
204
296
  /**
@@ -216,12 +308,19 @@ var PrepareApi = class extends MultichainConstruct {
216
308
  * {@inheritDoc IOpportunitiesPrepare.maxRepay}
217
309
  **/
218
310
  async maxRepay(position) {
219
- return this.queryChain({
220
- network: position.chainId,
221
- run: async (sdk) => service(sdk).maxRepay({
222
- creditAccount: await slice(sdk, position.creditAccount),
223
- sdk
224
- })
311
+ return this.#answer(position.chainId, async (sdk) => {
312
+ const creditAccount = await slice(sdk, position.creditAccount);
313
+ if (!creditAccount) return {
314
+ success: false,
315
+ error: creditAccountNotFound(position.creditAccount)
316
+ };
317
+ return {
318
+ success: true,
319
+ data: await service(sdk).maxRepay({
320
+ creditAccount,
321
+ sdk
322
+ })
323
+ };
225
324
  });
226
325
  }
227
326
  /**
@@ -278,14 +377,21 @@ var PrepareApi = class extends MultichainConstruct {
278
377
  * {@inheritDoc IOpportunitiesPrepare.maxWithdrawCollateral}
279
378
  **/
280
379
  async maxWithdrawCollateral(position, token, targetHF) {
281
- return this.queryChain({
282
- network: position.chainId,
283
- run: async (sdk) => service(sdk).maxWithdrawCollateral({
284
- creditAccount: await slice(sdk, position.creditAccount),
285
- sdk,
286
- token,
287
- targetHF
288
- })
380
+ return this.#answer(position.chainId, async (sdk) => {
381
+ const creditAccount = await slice(sdk, position.creditAccount);
382
+ if (!creditAccount) return {
383
+ success: false,
384
+ error: creditAccountNotFound(position.creditAccount)
385
+ };
386
+ return {
387
+ success: true,
388
+ data: await service(sdk).maxWithdrawCollateral({
389
+ creditAccount,
390
+ sdk,
391
+ token,
392
+ targetHF
393
+ })
394
+ };
289
395
  });
290
396
  }
291
397
  /**
@@ -293,42 +399,60 @@ var PrepareApi = class extends MultichainConstruct {
293
399
  * two routes to offer: one account read, one intent, both routes quoted.
294
400
  **/
295
401
  async #startRoutes(position, options, intent) {
296
- return this.queryChain({
297
- network: position.chainId,
298
- run: async (sdk) => service(sdk).intentRoutes({
299
- intent,
300
- creditAccount: await slice(sdk, position.creditAccount),
301
- sdk,
302
- slippage: options.slippage,
303
- quotaReserve: options.quotaReserve
304
- })
305
- });
402
+ try {
403
+ return await this.queryChain({
404
+ network: position.chainId,
405
+ run: async (sdk) => {
406
+ const creditAccount = await slice(sdk, position.creditAccount);
407
+ if (!creditAccount) return neitherRoute(creditAccountNotFound(position.creditAccount));
408
+ return routed(await service(sdk).intentRoutes({
409
+ intent,
410
+ creditAccount,
411
+ sdk,
412
+ slippage: options.slippage,
413
+ quotaReserve: options.quotaReserve
414
+ }));
415
+ }
416
+ });
417
+ } catch (e) {
418
+ return {
419
+ data: neitherRoute(unexpectedFailure(e)),
420
+ meta: { chains: [failedChain(position.chainId, e)] }
421
+ };
422
+ }
306
423
  }
307
424
  /**
308
425
  * Shared path of the five flows that act on an existing account: read the
309
426
  * account, then run the intent through the engine.
310
427
  **/
311
428
  async #startIntent(position, options, intent) {
312
- return this.queryChain({
313
- network: position.chainId,
314
- run: async (sdk) => {
315
- const creditAccount = await slice(sdk, position.creditAccount);
316
- return service(sdk).startIntent({
317
- intent,
318
- creditAccount,
319
- sdk,
320
- slippage: options.slippage,
321
- quotaReserve: options.quotaReserve
322
- });
323
- }
429
+ return this.#answer(position.chainId, async (sdk) => {
430
+ const creditAccount = await slice(sdk, position.creditAccount);
431
+ if (!creditAccount) return {
432
+ success: false,
433
+ error: creditAccountNotFound(position.creditAccount)
434
+ };
435
+ return planned(await service(sdk).startIntent({
436
+ intent,
437
+ creditAccount,
438
+ sdk,
439
+ slippage: options.slippage,
440
+ quotaReserve: options.quotaReserve
441
+ }));
324
442
  });
325
443
  }
326
444
  };
327
445
  function service(sdk) {
328
446
  return new CreditAccountOperationsService(sdk);
329
447
  }
330
- function slice(sdk, creditAccount) {
331
- return fetchCreditAccountSlice(sdk, creditAccount);
448
+ /**
449
+ * The account the request names, or nothing where the markets this SDK is
450
+ * connected to hold no such account — closed since it was listed, or named on
451
+ * the wrong chain. Read rather than thrown, so the caller gets a code for it.
452
+ **/
453
+ async function slice(sdk, creditAccount) {
454
+ const data = await sdk.accounts.getCreditAccountData(creditAccount);
455
+ return data && toCreditAccountSlice(data);
332
456
  }
333
457
  /**
334
458
  * The operation a claim resumes, or `undefined` when there is none to resume:
@@ -361,6 +485,36 @@ function toClaimableWithdrawal(claimable) {
361
485
  };
362
486
  }
363
487
  /**
488
+ * A flow with two routes, refused before either could be quoted: the error is
489
+ * the same one any other flow would report, with nothing to say about the
490
+ * routes because neither was reached.
491
+ **/
492
+ function neitherRoute(error) {
493
+ return {
494
+ success: false,
495
+ error: {
496
+ ...error,
497
+ refused: {
498
+ instant: void 0,
499
+ delayed: void 0
500
+ }
501
+ }
502
+ };
503
+ }
504
+ /**
505
+ * The chain entry for a request that was answered with
506
+ * {@link UnexpectedFailureError}: the read did not happen, so the metadata says
507
+ * so rather than reporting a block it never got.
508
+ **/
509
+ function failedChain(chainId, error) {
510
+ return {
511
+ chainId,
512
+ status: "error",
513
+ source: "onchain",
514
+ error
515
+ };
516
+ }
517
+ /**
364
518
  * A pool route the market does not offer, as the refusal a caller reads.
365
519
  *
366
520
  * `to` is absent where {@link lpRoute} found no output to name at all, which
@@ -368,10 +522,89 @@ function toClaimableWithdrawal(claimable) {
368
522
  * ours implements it.
369
523
  **/
370
524
  function unroutable(sdk, from, to) {
371
- return refuse("unsupportedTokenPair", {
372
- from: toToken(sdk, from),
373
- to: to === void 0 ? void 0 : toToken(sdk, to)
374
- });
525
+ return {
526
+ success: false,
527
+ error: toPrepareError({
528
+ reason: "unsupportedTokenPair",
529
+ detail: {
530
+ from: toToken(sdk, from),
531
+ to: to === void 0 ? void 0 : toToken(sdk, to)
532
+ }
533
+ })
534
+ };
535
+ }
536
+ /**
537
+ * The engine's answer, as the envelope the namespace speaks in: what the
538
+ * operation comes to under `data`, or the refusal as an error carrying its own
539
+ * numbers, see {@link toPrepareError}.
540
+ *
541
+ * The engine keeps its `ok` union — it is the shape the planners, the guards
542
+ * and their tests are written against — and the boundary is the one place the
543
+ * two vocabularies meet.
544
+ **/
545
+ function planned(result) {
546
+ if (!result.ok) return {
547
+ success: false,
548
+ error: toPrepareError(result)
549
+ };
550
+ const { operations, state, calls } = result;
551
+ return {
552
+ success: true,
553
+ data: {
554
+ operations,
555
+ state,
556
+ calls
557
+ }
558
+ };
559
+ }
560
+ /**
561
+ * {@inheritDoc planned}
562
+ **/
563
+ function opened(result) {
564
+ return result.ok ? {
565
+ success: true,
566
+ data: { state: result.state }
567
+ } : {
568
+ success: false,
569
+ error: toPrepareError(result)
570
+ };
571
+ }
572
+ /**
573
+ * {@inheritDoc planned}
574
+ *
575
+ * Both routes are payload, refusal and all: `refused` says why a missing one is
576
+ * missing, and it stays on the error when neither route answered, since that is
577
+ * the same question asked of a request that has no viable half at all.
578
+ **/
579
+ function routed(result) {
580
+ if (!result.ok) {
581
+ const { refused, ...issue } = result;
582
+ return {
583
+ success: false,
584
+ error: {
585
+ ...toPrepareError(issue),
586
+ refused
587
+ }
588
+ };
589
+ }
590
+ const { instant, delayed, refused } = result;
591
+ return {
592
+ success: true,
593
+ data: {
594
+ instant: instant && {
595
+ operations: instant.operations,
596
+ state: instant.state,
597
+ calls: instant.calls
598
+ },
599
+ delayed: delayed && {
600
+ operations: delayed.operations,
601
+ state: delayed.state,
602
+ calls: delayed.calls,
603
+ delayed: delayed.delayed
604
+ },
605
+ refused
606
+ }
607
+ };
375
608
  }
376
609
  /**
377
610
  * Picks the route the operation takes out of `tokenIn`, as a value rather than
@@ -0,0 +1,86 @@
1
+ //#region src/sdk/prepare/errors.ts
2
+ /**
3
+ * One sentence per refusal, naming what was ruled out rather than restating the
4
+ * numbers the error already carries.
5
+ *
6
+ * English and loggable, not a string to put on a screen: a form renders the
7
+ * code and the amounts in its own words, see {@link IGearboxError.message}.
8
+ **/
9
+ const MESSAGES = {
10
+ debtOutOfRange: "The debt this request implies is outside the market's band.",
11
+ leverageOutOfRange: "The leverage asked for cannot be expressed as a plan.",
12
+ insufficientSourceBalance: "Neither the account nor the wallet holds enough to fund this request.",
13
+ unsupportedCollateralToken: "This flow does not accept that token.",
14
+ unsupportedTokenPair: "No route exists between these two tokens.",
15
+ noDelayedRoute: "This request cannot be served as a delayed redemption.",
16
+ multipleDelayedWithdrawals: "The source asset has several redemption venues and none was named.",
17
+ withdrawalInProgress: "A redemption of this asset is already in flight on the account.",
18
+ noRecordedIntent: "The claim names no operation to resume.",
19
+ marketPaused: "The market is paused.",
20
+ marketExpired: "The market is past its expiration date.",
21
+ insufficientPoolLiquidity: "The pool cannot lend what this plan draws.",
22
+ quotaLimitReached: "The market takes no more quota for a token this plan holds.",
23
+ forbiddenToken: "This plan would increase the balance of a forbidden token.",
24
+ insufficientCollateral: "The account would end this transaction under-collateralised.",
25
+ poolSunset: "The pool is winding down and takes no more deposits.",
26
+ quotaCountExceeded: "The account would hold more quoted tokens than the facade enables at once.",
27
+ malformedTransaction: "The transaction could not be replayed."
28
+ };
29
+ /**
30
+ * The engine's refusal, as the error the namespace answers with.
31
+ *
32
+ * One place does the lifting, so the two shapes cannot drift: `reason` becomes
33
+ * `code`, the detail is spread onto the error, and the sentence comes from
34
+ * {@link MESSAGES}. A malformed transaction is spelled out rather than spread,
35
+ * because its detail names a `code` and a `message` of its own and they are not
36
+ * the envelope's.
37
+ **/
38
+ function toPrepareError(issue) {
39
+ if (issue.reason === "malformedTransaction") return {
40
+ code: "malformedTransaction",
41
+ message: MESSAGES.malformedTransaction,
42
+ previewCode: issue.detail.code,
43
+ detail: issue.detail.message
44
+ };
45
+ return {
46
+ code: issue.reason,
47
+ message: MESSAGES[issue.reason],
48
+ ...issue.detail
49
+ };
50
+ }
51
+ /**
52
+ * {@inheritDoc NoStrategyTargetCollateralError}
53
+ **/
54
+ function noStrategyTargetCollateral(creditManager) {
55
+ return {
56
+ code: "noStrategyTargetCollateral",
57
+ message: `Credit manager ${creditManager} has no strategy target collateral, and none was named.`,
58
+ creditManager
59
+ };
60
+ }
61
+ /**
62
+ * {@inheritDoc CreditAccountNotFoundError}
63
+ **/
64
+ function creditAccountNotFound(creditAccount) {
65
+ return {
66
+ code: "creditAccountNotFound",
67
+ message: `Credit account not found: ${creditAccount}.`,
68
+ creditAccount
69
+ };
70
+ }
71
+ /**
72
+ * {@inheritDoc UnexpectedFailureError}
73
+ *
74
+ * Takes what was thrown, whatever that is: a `throw` is not obliged to raise an
75
+ * `Error`, and `cause` promises one.
76
+ **/
77
+ function unexpectedFailure(thrown) {
78
+ const cause = thrown instanceof Error ? thrown : new Error(String(thrown));
79
+ return {
80
+ code: "unexpectedFailure",
81
+ message: `The SDK could not prepare this operation: ${cause.message}`,
82
+ cause
83
+ };
84
+ }
85
+ //#endregion
86
+ export { creditAccountNotFound, noStrategyTargetCollateral, toPrepareError, unexpectedFailure };
@@ -1,4 +1,5 @@
1
1
  import { IntentPreviewError, raise, refuse } from "../../onchain/validation/refusal.js";
2
+ import { creditAccountNotFound, noStrategyTargetCollateral, toPrepareError, unexpectedFailure } from "./errors.js";
2
3
  import { PrepareApi } from "./PrepareApi.js";
3
4
  import "./types.js";
4
- export { IntentPreviewError, PrepareApi, raise, refuse };
5
+ export { IntentPreviewError, PrepareApi, creditAccountNotFound, noStrategyTargetCollateral, raise, refuse, toPrepareError, unexpectedFailure };