@xoxno/sdk-js 1.0.228 → 1.0.230

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,7 +1,13 @@
1
1
  /**
2
2
  * rs-lending-xlm Soroban contract error codes → human-readable messages.
3
- * Mirrors `common/src/errors.rs` (`GenericError`, `CollateralError`,
4
- * `SpokeError`, `OracleError`). Keep in sync when that file changes.
3
+ * Mirrors every enum in `common/src/errors.rs` (`GenericError`,
4
+ * `CollateralError`, `OracleError`, `SpokeError`, `FlashLoanError`,
5
+ * `StrategyError`). Reserved codes stay listed so numbers are never reused.
6
+ * Keep in sync when that file changes.
7
+ *
8
+ * Codes are scoped per contract: the swap aggregator, XOXNO oracle and DeFindex
9
+ * adapter declare their own enums, so the same number can mean something else
10
+ * there. Pass `contracts` to map only errors emitted by the lending contracts.
5
11
  */ const SOROBAN_ERROR_CATALOG = {
6
12
  // GenericError
7
13
  1: {
@@ -16,10 +22,22 @@
16
22
  name: 'InvalidTicker',
17
23
  message: 'Invalid asset ticker.'
18
24
  },
25
+ 5: {
26
+ name: 'PoolAlreadyDeployed',
27
+ message: 'Singleton pool, controller, or price aggregator already recorded.'
28
+ },
19
29
  6: {
20
30
  name: 'InvalidAsset',
21
31
  message: 'Invalid asset address.'
22
32
  },
33
+ 7: {
34
+ name: 'AssetsAreTheSame',
35
+ message: 'Same market or token where distinct HubAssetKeys or underlying assets, respectively, are required.'
36
+ },
37
+ 8: {
38
+ name: 'WrongToken',
39
+ message: 'Params.asset_id differs from the market\'s asset.'
40
+ },
23
41
  10: {
24
42
  name: 'InvalidWasmHash',
25
43
  message: 'The WASM hash is empty or invalid.'
@@ -32,22 +50,78 @@
32
50
  name: 'PairNotActive',
33
51
  message: 'No oracle is configured for this asset.'
34
52
  },
53
+ 13: {
54
+ name: 'AccountNotInMarket',
55
+ message: 'Caller lacks the account\'s position NFT, or the account has no metadata.'
56
+ },
35
57
  14: {
36
58
  name: 'AmountMustBePositive',
37
59
  message: 'The amount must be greater than zero.'
38
60
  },
61
+ 16: {
62
+ name: 'InvalidPayments',
63
+ message: 'Payments or route list empty when required, non-empty when forbidden, or over the view limit.'
64
+ },
65
+ 18: {
66
+ name: 'NotSmartContract',
67
+ message: 'Address is not a deployed contract.'
68
+ },
69
+ 24: {
70
+ name: 'AccountNotFound',
71
+ message: 'Account id has no state, or its NFT owner is unresolvable.'
72
+ },
73
+ 25: {
74
+ name: 'AccountModeMismatch',
75
+ message: 'Account mode differs from the call\'s required mode.'
76
+ },
77
+ 27: {
78
+ name: 'AggregatorNotSet',
79
+ message: 'Swap or price aggregator not configured.'
80
+ },
81
+ 29: {
82
+ name: 'PositionLimitsNotSet',
83
+ message: 'No position limits stored.'
84
+ },
85
+ 30: {
86
+ name: 'PoolNotInitialized',
87
+ message: 'Pool or controller unset, or market params/state missing.'
88
+ },
39
89
  32: {
40
90
  name: 'OwnerNotSet',
41
91
  message: 'The contract owner has not been set.'
42
92
  },
93
+ 33: {
94
+ name: 'MathOverflow',
95
+ message: 'Checked arithmetic overflow or failed narrowing conversion.'
96
+ },
97
+ 34: {
98
+ name: 'InternalError',
99
+ message: 'Internal invariant failed. Report it.'
100
+ },
101
+ 36: {
102
+ name: 'InvalidPositionLimits',
103
+ message: 'Supply or borrow limit is zero or above POSITION_LIMIT_MAX (5).'
104
+ },
105
+ 38: {
106
+ name: 'SpotOnlyNotProductionSafe',
107
+ message: 'Every oracle source reads an unsmoothed spot market.'
108
+ },
43
109
  39: {
44
110
  name: 'InvalidTimelockDelay',
45
111
  message: 'The timelock delay is out of the allowed range.'
46
112
  },
113
+ 40: {
114
+ name: 'TimelockOperationExpired',
115
+ message: 'Scheduled operation\'s grace period elapsed.'
116
+ },
47
117
  41: {
48
118
  name: 'InvalidRole',
49
119
  message: 'The caller does not hold the required role.'
50
120
  },
121
+ 42: {
122
+ name: 'BlendPoolNotApproved',
123
+ message: 'Blend pool not approved.'
124
+ },
51
125
  43: {
52
126
  name: 'HubNotActive',
53
127
  message: 'This hub is not active.'
@@ -56,15 +130,99 @@
56
130
  name: 'NotAuthorized',
57
131
  message: 'The caller is not authorized to perform this action.'
58
132
  },
133
+ 45: {
134
+ name: 'RegistryCapReached',
135
+ message: 'Account already has MAX_DELEGATES (16).'
136
+ },
59
137
  46: {
60
138
  name: 'OperationNotCancellable',
61
139
  message: 'This governance operation cannot be cancelled.'
62
140
  },
141
+ 47: {
142
+ name: 'BorrowRoundsToZeroShares',
143
+ message: 'Positive borrow mints zero scaled debt shares.'
144
+ },
145
+ 48: {
146
+ name: 'CannotRemoveLastProposer',
147
+ message: 'Revoke would leave no proposer.'
148
+ },
149
+ 49: {
150
+ name: 'WithdrawRoundsToZeroShares',
151
+ message: 'Positive withdrawal burns zero scaled supply shares.'
152
+ },
153
+ 50: {
154
+ name: 'NetSettleRoundsToZeroShares',
155
+ message: 'Positive net settlement burns zero shares.'
156
+ },
157
+ 51: {
158
+ name: 'SupplyRoundsToZeroShares',
159
+ message: 'Positive supply mints zero scaled shares.'
160
+ },
161
+ 52: {
162
+ name: 'RepayRoundsToZeroShares',
163
+ message: 'Positive net repayment burns zero scaled debt shares.'
164
+ },
165
+ 53: {
166
+ name: 'PositionNftNotSet',
167
+ message: 'NFT address unset on a path needing it, such as account creation. Ownership lookup can return #24 instead.'
168
+ },
169
+ 54: {
170
+ name: 'PositionNftAlreadyDeployed',
171
+ message: 'Position-NFT address already recorded.'
172
+ },
173
+ 55: {
174
+ name: 'DivisionByZero',
175
+ message: 'Fixed-point multiply-divide with zero denominator.'
176
+ },
63
177
  // CollateralError
178
+ 100: {
179
+ name: 'InsufficientCollateral',
180
+ message: 'Post-operation LTV-weighted collateral below total debt, or health factor below 1 WAD.'
181
+ },
182
+ 101: {
183
+ name: 'HealthFactorTooHigh',
184
+ message: 'Target has no debt or health factor >= 1 WAD; not liquidatable.'
185
+ },
186
+ 102: {
187
+ name: 'HealthFactorTooLow',
188
+ message: 'Update_account_threshold with has_risks = true ends below 1.05 WAD. A gated liquidation-tuple refresh skips a tuple that fails its hypothetical health-factor check.'
189
+ },
190
+ 104: {
191
+ name: 'NotCollateral',
192
+ message: 'Spoke listing disallows the asset as collateral.'
193
+ },
194
+ 107: {
195
+ name: 'AssetNotBorrowable',
196
+ message: 'Spoke listing disallows borrowing the asset.'
197
+ },
198
+ 109: {
199
+ name: 'PositionLimitExceeded',
200
+ message: 'New slot exceeds the position limit, or a sub-3-decimal supply leg would coexist with another supply slot.'
201
+ },
202
+ 110: {
203
+ name: 'PositionNotFound',
204
+ message: 'Reserved code; no longer emitted.'
205
+ },
206
+ 111: {
207
+ name: 'InvalidPositionMode',
208
+ message: 'Mode is not Multiply, Long, or Short.'
209
+ },
210
+ 112: {
211
+ name: 'InsufficientLiquidity',
212
+ message: 'Market cash below the draw, or the draw breaks the liquidation buffer.'
213
+ },
64
214
  113: {
65
215
  name: 'InvalidLiqThreshold',
66
216
  message: 'LTV, liquidation threshold, or bonus is out of bounds.'
67
217
  },
218
+ 114: {
219
+ name: 'CannotCleanBadDebt',
220
+ message: 'Account fails the bad-debt gate.'
221
+ },
222
+ 115: {
223
+ name: 'WithdrawLessThanFee',
224
+ message: 'Liquidation protocol fee exceeds the gross withdrawal.'
225
+ },
68
226
  116: {
69
227
  name: 'InvalidBorrowParams',
70
228
  message: 'Supply or borrow cap configuration is invalid.'
@@ -73,10 +231,179 @@
73
231
  name: 'InvalidUtilRange',
74
232
  message: 'Interest-rate utilization breakpoints are invalid.'
75
233
  },
234
+ 118: {
235
+ name: 'OptUtilTooHigh',
236
+ message: 'Optimal utilization >= 100%.'
237
+ },
238
+ 119: {
239
+ name: 'InvalidReserveFactor',
240
+ message: 'Reserve factor >= 100%.'
241
+ },
242
+ 120: {
243
+ name: 'DebtPositionNotFound',
244
+ message: 'No debt position for that hub asset.'
245
+ },
246
+ 121: {
247
+ name: 'CollateralPositionNotFound',
248
+ message: 'No supply position for that hub asset.'
249
+ },
250
+ 122: {
251
+ name: 'CannotCloseWithRemainingDebt',
252
+ message: 'Close_position with borrow positions open.'
253
+ },
254
+ 123: {
255
+ name: 'PoolInsolvent',
256
+ message: 'Supplier claims exceed cash plus debt, or zero supply with debt outstanding.'
257
+ },
258
+ 126: {
259
+ name: 'MinBorrowCollateralNotMet',
260
+ message: 'With debt outstanding, LTV-weighted collateral below the USD floor, or a sub-3-decimal supply leg under 2 raw units after floor conversion.'
261
+ },
262
+ 127: {
263
+ name: 'UtilizationAboveMax',
264
+ message: 'Post-operation utilization above max_utilization.'
265
+ },
266
+ 128: {
267
+ name: 'BaseRateNegative',
268
+ message: 'Negative base borrow rate.'
269
+ },
270
+ 129: {
271
+ name: 'SlopeNonMonotonic',
272
+ message: 'Rate slopes do not increase monotonically.'
273
+ },
274
+ 130: {
275
+ name: 'MaxRateBelowBase',
276
+ message: 'Max borrow rate not strictly above base.'
277
+ },
278
+ 131: {
279
+ name: 'MaxBorrowRateTooHigh',
280
+ message: 'Max borrow rate exceeds MAX_BORROW_RATE_RAY (200% APR).'
281
+ },
76
282
  132: {
77
283
  name: 'AssetDecimalsTooHigh',
78
284
  message: 'Asset decimals exceed the protocol maximum (27).'
79
285
  },
286
+ 133: {
287
+ name: 'SelfLiquidationNotAllowed',
288
+ message: 'SeizeMode::Credit(id) receiver is the liquidated account. Owners may still liquidate their own account.'
289
+ },
290
+ 134: {
291
+ name: 'InvalidLiquidationCurve',
292
+ message: 'Curve bounds fail validation.'
293
+ },
294
+ 135: {
295
+ name: 'FullCloseRequired',
296
+ message: 'Retired; no error site.'
297
+ },
298
+ // OracleError
299
+ 201: {
300
+ name: 'InvalidAggregator',
301
+ message: 'Proposed aggregator is not a deployed contract.'
302
+ },
303
+ 204: {
304
+ name: 'InvalidOracleTokenType',
305
+ message: 'Reflector asset given as string; only address or symbol accepted.'
306
+ },
307
+ 205: {
308
+ name: 'UnsafePriceNotAllowed',
309
+ message: 'Two legs disagree beyond tolerance, or only one produced a reading.'
310
+ },
311
+ 206: {
312
+ name: 'PriceFeedStale',
313
+ message: 'Observation exceeds its staleness budget. Timestamps over 60 s ahead are dropped; the error then depends on remaining sources.'
314
+ },
315
+ 208: {
316
+ name: 'BadLastTolerance',
317
+ message: 'Upper ratio outside 10150..12500 (excess over BPS outside 150..2500 bps), or lower ratio is not the required rounded reciprocal.'
318
+ },
319
+ 210: {
320
+ name: 'NoLastPrice',
321
+ message: 'No readable source, or an Aquarius LP read failed (for example, reserves or shares).'
322
+ },
323
+ 211: {
324
+ name: 'NoAccumulator',
325
+ message: 'Controller has no revenue accumulator.'
326
+ },
327
+ 212: {
328
+ name: 'ReflectorHistoryEmpty',
329
+ message: 'Reflector returns no price history.'
330
+ },
331
+ 216: {
332
+ name: 'OracleNotConfigured',
333
+ message: 'No oracle for the price key, or the response omits a requested asset.'
334
+ },
335
+ 217: {
336
+ name: 'InvalidPrice',
337
+ message: 'Price <= 0, or LP fair-value math non-representable.'
338
+ },
339
+ 218: {
340
+ name: 'InvalidStalenessConfig',
341
+ message: 'Max_stale_seconds out of range or below the source\'s guaranteed freshness.'
342
+ },
343
+ 219: {
344
+ name: 'TwapInsufficientObservations',
345
+ message: 'Record count zero or below the smoothing minimum, or history too short, too long, or denser than feed resolution.'
346
+ },
347
+ 220: {
348
+ name: 'InvalidOracleBase',
349
+ message: 'The oracle quote asset/market is missing or invalid.'
350
+ },
351
+ 221: {
352
+ name: 'InvalidOracleDecimals',
353
+ message: 'The oracle decimals value is invalid.'
354
+ },
355
+ 222: {
356
+ name: 'InvalidOracleResolution',
357
+ message: 'Reflector resolution below minimum, above max_stale_seconds, or implying a TWAP span above it.'
358
+ },
359
+ 223: {
360
+ name: 'SanityBoundViolated',
361
+ message: 'Price outside the stored sanity band.'
362
+ },
363
+ 224: {
364
+ name: 'InvalidSanityBounds',
365
+ message: 'Oracle sanity bounds are invalid (require 0 < min < max).'
366
+ },
367
+ 225: {
368
+ name: 'OracleCycleDetected',
369
+ message: 'A cycle was detected in the oracle quote chain.'
370
+ },
371
+ 226: {
372
+ name: 'SanityBandTooWideForSingleSource',
373
+ message: 'Band exceeds the tighter limit for single-source, LP, or dual-source oracles with identical provider trust sets.'
374
+ },
375
+ 227: {
376
+ name: 'SanityBandMustTighten',
377
+ message: 'Set_sanity_band would widen the band. Widen via timelocked ConfigureAssetOracle.'
378
+ },
379
+ 228: {
380
+ name: 'TwapRecordsOutOfRange',
381
+ message: 'Record count above MAX_TWAP_RECORDS (12).'
382
+ },
383
+ 229: {
384
+ name: 'OracleDepthExceeded',
385
+ message: 'Composition nests deeper than MAX_RESOLUTION_DEPTH (3).'
386
+ },
387
+ 230: {
388
+ name: 'FactorOutOfBounds',
389
+ message: 'Scaled source\'s factor price outside its min/max.'
390
+ },
391
+ 231: {
392
+ name: 'SourceCountOutOfRange',
393
+ message: 'Zero or over two sources, or Aquarius LP combined with another source.'
394
+ },
395
+ 232: {
396
+ name: 'IndependenceNotDeclared',
397
+ message: 'Sources violate RequireDisjoint, or AllowShared is empty or not exactly the shared providers.'
398
+ },
399
+ 234: {
400
+ name: 'UnsupportedAquariusPool',
401
+ message: 'Aquarius pool of wrong type, or zero reserve or total shares.'
402
+ },
403
+ 235: {
404
+ name: 'InsufficientAquariusLiquidity',
405
+ message: 'Pool value below min_pool_value_wad.'
406
+ },
80
407
  // SpokeError
81
408
  300: {
82
409
  name: 'SpokeNotFound',
@@ -98,6 +425,18 @@
98
425
  name: 'SpokeAssetInUse',
99
426
  message: 'This spoke asset still has live positions; drain usage before removal.'
100
427
  },
428
+ 310: {
429
+ name: 'SpokeMismatch',
430
+ message: 'Account\'s spoke differs from the targeted spoke.'
431
+ },
432
+ 311: {
433
+ name: 'SpokeSupplyCapReached',
434
+ message: 'Supply would exceed the spoke supply cap.'
435
+ },
436
+ 312: {
437
+ name: 'SpokeBorrowCapReached',
438
+ message: 'Borrow would exceed the spoke borrow cap.'
439
+ },
101
440
  315: {
102
441
  name: 'SpokeAssetPaused',
103
442
  message: 'This spoke asset is paused.'
@@ -114,33 +453,87 @@
114
453
  name: 'SpokeAssetSeizureHalted',
115
454
  message: 'Liquidation seizure is halted for this spoke asset.'
116
455
  },
117
- // OracleError
118
- 220: {
119
- name: 'InvalidOracleBase',
120
- message: 'The oracle quote asset/market is missing or invalid.'
456
+ 319: {
457
+ name: 'SpokeFlagsEpochMismatch',
458
+ message: 'RelaxSpokeAssetFlags epoch is stale or wrong; checked at proposal and execution.'
121
459
  },
122
- 221: {
123
- name: 'InvalidOracleDecimals',
124
- message: 'The oracle decimals value is invalid.'
460
+ // FlashLoanError
461
+ 400: {
462
+ name: 'FlashLoanOngoing',
463
+ message: 'Flash loan in progress; re-entrant calls refused.'
125
464
  },
126
- 224: {
127
- name: 'InvalidSanityBounds',
128
- message: 'Oracle sanity bounds are invalid (require 0 < min < max).'
465
+ 401: {
466
+ name: 'FlashloanNotEnabled',
467
+ message: 'Market is_flashloanable is false.'
129
468
  },
130
- 225: {
131
- name: 'OracleCycleDetected',
132
- message: 'A cycle was detected in the oracle quote chain.'
469
+ 402: {
470
+ name: 'InvalidFlashloanRepay',
471
+ message: 'Allowance below principal plus fee, or a pool balance check after payout, callback, or repayment fails.'
472
+ },
473
+ 409: {
474
+ name: 'StrategyFeeExceeds',
475
+ message: 'Strategy fee exceeds the borrowed amount.'
476
+ },
477
+ 412: {
478
+ name: 'InvalidFlashloanReceiver',
479
+ message: 'Receiver not a deployed Wasm contract, or a controller payout targets the controller or pool.'
480
+ },
481
+ // StrategyError
482
+ 500: {
483
+ name: 'ConvertStepsRequired',
484
+ message: 'Multiply initial payment is neither collateral nor debt asset, with no convert_swap.'
485
+ },
486
+ 501: {
487
+ name: 'RouterOverspend',
488
+ message: 'Controller input balance rose during the swap, or router spent over amount_in.'
489
+ },
490
+ 502: {
491
+ name: 'NoSwapOutput',
492
+ message: 'Swap produced no positive measured output.'
493
+ },
494
+ 503: {
495
+ name: 'CollateralRequired',
496
+ message: 'Flash_position collateral list has no positive minimum. Empty list raises #16.'
497
+ },
498
+ 504: {
499
+ name: 'CollateralMinimumNotMet',
500
+ message: 'Measured deposit below its minimum, or none positive.'
501
+ },
502
+ 505: {
503
+ name: 'FlashPositionClosed',
504
+ message: 'Flash_position ended without supply or positive debt in its debt market.'
133
505
  }
134
506
  };
507
+ const ERROR_CODE = /Error\(Contract,\s*#(\d+)\)/;
508
+ const EMITTED_ERROR = /contract:(C[A-Z2-7]{55})[^\n]*?Error\(Contract,\s*#(\d+)\)/g;
509
+ const INVOKED_TAG = /\[xoxno-invoked:(C[A-Z2-7]{55})\]/;
510
+ const HAS_EVENT_LOG = /contract:C[A-Z2-7]{55}/;
511
+ const scopedCode = (raw, allowed)=>{
512
+ for (const m of raw.matchAll(EMITTED_ERROR)){
513
+ if (allowed.has(m[1])) return Number(m[2]);
514
+ }
515
+ if (HAS_EVENT_LOG.test(raw)) return null;
516
+ const tag = raw.match(INVOKED_TAG);
517
+ const first = raw.match(ERROR_CODE);
518
+ return tag && allowed.has(tag[1]) && first ? Number(first[1]) : null;
519
+ };
135
520
  /**
136
521
  * Extracts a `Error(Contract, #N)` code from a raw Soroban simulation/RPC error
137
522
  * string and resolves it against the rs-lending-xlm catalog. Matches only the
138
523
  * exact `Error(Contract, #N)` pattern — never loose digits, since raw error
139
524
  * strings can contain unrelated numbers (fees, ledgers, sequence numbers).
140
- */ export function mapSorobanError(raw) {
141
- const match = raw.match(/Error\(Contract,\s*#(\d+)\)/);
142
- if (!match) return null;
143
- const code = Number(match[1]);
525
+ *
526
+ * Without `opts.contracts` it maps the first code in the string, whichever
527
+ * contract emitted it.
528
+ */ export function mapSorobanError(raw, opts) {
529
+ let code;
530
+ if (opts?.contracts?.length) {
531
+ code = scopedCode(raw, new Set(opts.contracts));
532
+ } else {
533
+ const match = raw.match(ERROR_CODE);
534
+ code = match ? Number(match[1]) : null;
535
+ }
536
+ if (code === null) return null;
144
537
  const entry = SOROBAN_ERROR_CATALOG[code];
145
538
  return entry ? {
146
539
  code,