curvance 5.1.8 → 5.2.0

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 (114) hide show
  1. package/README.md +251 -70
  2. package/dist/abis/OptimizerReader.json +292 -1
  3. package/dist/chains/arbitrum.d.ts.map +1 -1
  4. package/dist/chains/arbitrum.js +14 -1
  5. package/dist/chains/arbitrum.js.map +1 -1
  6. package/dist/chains/index.d.ts +19 -0
  7. package/dist/chains/index.d.ts.map +1 -1
  8. package/dist/chains/index.js.map +1 -1
  9. package/dist/chains/monad.d.ts.map +1 -1
  10. package/dist/chains/monad.js +25 -2
  11. package/dist/chains/monad.js.map +1 -1
  12. package/dist/chains/services.d.ts +8 -0
  13. package/dist/chains/services.d.ts.map +1 -0
  14. package/dist/chains/services.js +9 -0
  15. package/dist/chains/services.js.map +1 -0
  16. package/dist/classes/Api.d.ts +7 -2
  17. package/dist/classes/Api.d.ts.map +1 -1
  18. package/dist/classes/Api.js +60 -24
  19. package/dist/classes/Api.js.map +1 -1
  20. package/dist/classes/BorrowableCToken.d.ts.map +1 -1
  21. package/dist/classes/BorrowableCToken.js +7 -2
  22. package/dist/classes/BorrowableCToken.js.map +1 -1
  23. package/dist/classes/CToken.d.ts +36 -29
  24. package/dist/classes/CToken.d.ts.map +1 -1
  25. package/dist/classes/CToken.js +225 -166
  26. package/dist/classes/CToken.js.map +1 -1
  27. package/dist/classes/DexAggregators/IDexAgg.d.ts +8 -0
  28. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  29. package/dist/classes/DexAggregators/KyberSwap.d.ts +5 -2
  30. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  31. package/dist/classes/DexAggregators/KyberSwap.js +41 -19
  32. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  33. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +7 -4
  34. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  35. package/dist/classes/DexAggregators/MultiDexAgg.js +62 -16
  36. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  37. package/dist/classes/DexAggregators/helpers.d.ts +1 -1
  38. package/dist/classes/DexAggregators/helpers.d.ts.map +1 -1
  39. package/dist/classes/DexAggregators/helpers.js +1 -1
  40. package/dist/classes/DexAggregators/helpers.js.map +1 -1
  41. package/dist/classes/DexAggregators/index.d.ts +0 -1
  42. package/dist/classes/DexAggregators/index.d.ts.map +1 -1
  43. package/dist/classes/DexAggregators/index.js +0 -1
  44. package/dist/classes/DexAggregators/index.js.map +1 -1
  45. package/dist/classes/LendingOptimizer.d.ts +7 -0
  46. package/dist/classes/LendingOptimizer.d.ts.map +1 -1
  47. package/dist/classes/LendingOptimizer.js +12 -2
  48. package/dist/classes/LendingOptimizer.js.map +1 -1
  49. package/dist/classes/Market.d.ts +5 -0
  50. package/dist/classes/Market.d.ts.map +1 -1
  51. package/dist/classes/Market.js +129 -30
  52. package/dist/classes/Market.js.map +1 -1
  53. package/dist/classes/NativeToken.d.ts +5 -2
  54. package/dist/classes/NativeToken.d.ts.map +1 -1
  55. package/dist/classes/NativeToken.js +5 -5
  56. package/dist/classes/NativeToken.js.map +1 -1
  57. package/dist/classes/OptimizerReader.d.ts +44 -4
  58. package/dist/classes/OptimizerReader.d.ts.map +1 -1
  59. package/dist/classes/OptimizerReader.js +133 -62
  60. package/dist/classes/OptimizerReader.js.map +1 -1
  61. package/dist/classes/PositionManager.d.ts +1 -0
  62. package/dist/classes/PositionManager.d.ts.map +1 -1
  63. package/dist/classes/PositionManager.js +25 -0
  64. package/dist/classes/PositionManager.js.map +1 -1
  65. package/dist/classes/ProtocolReader.d.ts +3 -0
  66. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  67. package/dist/classes/ProtocolReader.js +34 -0
  68. package/dist/classes/ProtocolReader.js.map +1 -1
  69. package/dist/classes/Zapper.d.ts +4 -1
  70. package/dist/classes/Zapper.d.ts.map +1 -1
  71. package/dist/classes/Zapper.js +34 -9
  72. package/dist/classes/Zapper.js.map +1 -1
  73. package/dist/classes/index.d.ts +1 -1
  74. package/dist/classes/index.d.ts.map +1 -1
  75. package/dist/classes/index.js +1 -1
  76. package/dist/classes/index.js.map +1 -1
  77. package/dist/contracts/index.d.ts +249 -245
  78. package/dist/contracts/index.d.ts.map +1 -1
  79. package/dist/contracts/index.js +3 -2
  80. package/dist/contracts/index.js.map +1 -1
  81. package/dist/contracts/monad-mainnet.json +4 -0
  82. package/dist/feePolicy.d.ts +29 -26
  83. package/dist/feePolicy.d.ts.map +1 -1
  84. package/dist/feePolicy.js +43 -34
  85. package/dist/feePolicy.js.map +1 -1
  86. package/dist/format/index.d.ts +5 -0
  87. package/dist/format/index.d.ts.map +1 -1
  88. package/dist/format/index.js +28 -0
  89. package/dist/format/index.js.map +1 -1
  90. package/dist/helpers.d.ts +248 -245
  91. package/dist/helpers.d.ts.map +1 -1
  92. package/dist/helpers.js +5 -5
  93. package/dist/helpers.js.map +1 -1
  94. package/dist/immutability.d.ts +6 -0
  95. package/dist/immutability.d.ts.map +1 -0
  96. package/dist/immutability.js +25 -0
  97. package/dist/immutability.js.map +1 -0
  98. package/dist/integrations/merkl.d.ts +9 -2
  99. package/dist/integrations/merkl.d.ts.map +1 -1
  100. package/dist/integrations/merkl.js +219 -11
  101. package/dist/integrations/merkl.js.map +1 -1
  102. package/dist/integrations/snapshot.d.ts +3 -0
  103. package/dist/integrations/snapshot.d.ts.map +1 -1
  104. package/dist/integrations/snapshot.js +47 -3
  105. package/dist/integrations/snapshot.js.map +1 -1
  106. package/dist/setup.d.ts +13 -3
  107. package/dist/setup.d.ts.map +1 -1
  108. package/dist/setup.js +101 -7
  109. package/dist/setup.js.map +1 -1
  110. package/package.json +7 -5
  111. package/dist/classes/Redstone.d.ts +0 -15
  112. package/dist/classes/Redstone.d.ts.map +0 -1
  113. package/dist/classes/Redstone.js +0 -56
  114. package/dist/classes/Redstone.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="https://pbs.twimg.com/profile_banners/1445781144125857796/1773687595/1500x500" alt="Curvance"/>
3
3
  </p>
4
4
 
5
- A TypeScript SDK for interacting with the Curvance protocol. Built on ethers v6 with a bulk-loaded cache model `setupChain()` preloads all market data in 1–3 RPC calls, and all subsequent reads are synchronous from cache.
5
+ A TypeScript SDK for interacting with the Curvance protocol. It uses ethers v6 and a setup-bound cache model: `setupChain()` loads market state up front, snapshots the chain configuration it used, and returns markets whose reads are synchronous until an explicit refresh runs.
6
6
 
7
7
  ## ❯ Install
8
8
 
@@ -14,10 +14,21 @@ $ npm install --save curvance
14
14
 
15
15
  Chain identifiers use Alchemy-style prefixes:
16
16
 
17
- | Chain | Identifier |
18
- |---|---|
19
- | Monad Mainnet | `monad-mainnet` |
20
- | Arbitrum Sepolia | `arb-sepolia` |
17
+ | Chain | Identifier | Support |
18
+ |---|---|---|
19
+ | Monad Mainnet | `monad-mainnet` | Production mainnet; setup/read, rewards, Kyber-backed simple zaps/leverage where configured |
20
+ | Arbitrum Sepolia | `arb-sepolia` | Testnet read/setup surface; DEX routes fail closed through `UnsupportedDexAgg` |
21
+
22
+ Adding a production chain is an explicit SDK release task. A chain is not
23
+ supported just because a wallet or app can switch to it. The SDK needs:
24
+
25
+ - a `chain_config` entry and `chain_rpc_config` entry with matching `chainId`
26
+ - a contract manifest under `src/contracts`
27
+ - Curvance API service aliases or an explicit disabled state
28
+ - DEX service config, fee/checker policy, or `UnsupportedDexAgg`
29
+ - native/wrapped-native and vault metadata in chain config
30
+ - route-matrix tests for advertised zap/leverage support
31
+ - fork or live-read proof once deployments exist
21
32
 
22
33
  ## ❯ Quick Start
23
34
 
@@ -26,7 +37,8 @@ import { setupChain } from "curvance";
26
37
  import { ethers } from "ethers";
27
38
 
28
39
  const wallet = new ethers.Wallet(privateKey, provider);
29
- const { markets, reader, dexAgg, global_milestone } = await setupChain("monad-mainnet", wallet);
40
+ const { chain, chainId, setupConfigSnapshot, markets, reader, dexAgg, global_milestone } =
41
+ await setupChain("monad-mainnet", wallet);
30
42
  ```
31
43
 
32
44
  `setupChain` signature:
@@ -37,11 +49,14 @@ setupChain(
37
49
  provider: curvance_provider | null = null, // signer (wallet) OR read-only provider; null → SDK default
38
50
  api_url: string = "https://api.curvance.com",
39
51
  options: {
40
- feePolicy?: FeePolicy; // zap/leverage fee routing (default: NO_FEE_POLICY)
52
+ feePolicy?: FeePolicy; // default is setup-resolved; Kyber chains require checker-compatible policy
41
53
  account?: address | null; // user address for user-specific reads without a signer
42
54
  readProvider?: curvance_read_provider | null; // explicit override for read transport
43
55
  } = {}
44
56
  ): Promise<{
57
+ chain: ChainRpcPrefix,
58
+ chainId: number,
59
+ setupConfigSnapshot: Readonly<SetupConfigSnapshot>, // includes chain asset metadata and service policies
45
60
  markets: Market[],
46
61
  reader: ProtocolReader,
47
62
  dexAgg: IDexAgg,
@@ -49,6 +64,26 @@ setupChain(
49
64
  }>
50
65
  ```
51
66
 
67
+ `setupChain()` still publishes a single active setup for compatibility helpers
68
+ such as `getActiveUserMarkets()` and snapshot calls without explicit `markets`.
69
+ Multichain-safe code should pass explicit `markets`, `reader`, provider,
70
+ account, or setup context instead of relying on the latest singleton.
71
+
72
+ ### Architecture contract
73
+
74
+ The current SDK architecture is result-bound. A `setupChain(...)` result owns
75
+ the chain context that produced it, and downstream objects should keep using
76
+ that context even if another chain boots later.
77
+
78
+ | Layer | Contract |
79
+ |---|---|
80
+ | Setup snapshot | `setupConfigSnapshot` contains chain id, environment, cloned/frozen asset metadata, cloned/frozen external service policy, contract addresses, read transport, signer/account, API URL, and fee policy. |
81
+ | Returned markets | `Market` and `CToken` instances keep the setup snapshot and reader they were created with. Explicit returned-result calls stay on that chain after the module singleton moves. |
82
+ | Compatibility globals | `setup_config` and `all_markets` exist for single-active-chain consumers. Treat no-argument helpers as compatibility paths, not multichain-safe state. |
83
+ | External services | Curvance API reward/native-yield slugs and Kyber API/router/chain aliases live in chain config and are cloned into the setup snapshot. Helper code should read the snapshot, not mutable exported config. |
84
+ | DEX routing | Markets receive a setup-bound `dexAgg` after boot. `CToken` route discovery and zap/leverage execution do not fall back to `chain_config.dexAgg`; unsupported or manually constructed markets fail closed unless a market-bound adapter is attached. |
85
+ | Fee/checker policy | Kyber-backed chains validate checker-compatible fee policy during setup. The current checker requires `CURVANCE_FEE_BPS` and the setup-resolved DAO receiver before routes are advertised or markets finish booting. |
86
+
52
87
  ### RPC routing
53
88
 
54
89
  - **Wallet connected** (signer with a `.provider`) → the wallet's own provider is the **primary** read source; the chain's configured RPC + fallbacks absorb wallet RPC failures. This distributes read load across users' wallet RPCs and respects whichever endpoint each user chose.
@@ -69,7 +104,7 @@ setupChain(
69
104
  for (const market of markets) {
70
105
  console.log(`${market.name} | deposits: ${market.totalDeposits} | debt: ${market.totalDebt}`);
71
106
  for (const token of market.tokens) {
72
- console.log(` ${token.symbol} | price: ${token.getPrice()} | apy: ${token.getApy(true)}%`);
107
+ console.log(` ${token.symbol} | price: ${token.getPrice(true)} | apy: ${token.getApy(true)}%`);
73
108
  }
74
109
  }
75
110
  ```
@@ -85,7 +120,7 @@ for (const market of markets) {
85
120
  | `ethers.JsonRpcProvider` | Read-only or custom RPC |
86
121
  | `null` | SDK constructs a provider from chain config |
87
122
 
88
- `curvance_signer` = `JsonRpcSigner | Wallet` required for write operations (deposit, borrow, etc.)
123
+ `curvance_signer` = `JsonRpcSigner | Wallet`, required for write operations (deposit, borrow, etc.)
89
124
 
90
125
  ## ❯ Markets
91
126
 
@@ -113,7 +148,7 @@ market.userDebt // total outstanding debt
113
148
  market.userMaxDebt // maximum allowable debt
114
149
  market.userRemainingCredit // available borrow capacity (with 0.1% buffer)
115
150
  market.userCollateral // posted collateral (in shares)
116
- market.positionHealth // health factor null means infinite (no debt)
151
+ market.positionHealth // health factor; null means infinite (no debt)
117
152
  market.userNet // deposits - debt
118
153
  ```
119
154
 
@@ -166,8 +201,8 @@ token.mintPaused
166
201
  ### Prices & conversions
167
202
 
168
203
  ```ts
169
- token.getPrice() // asset price (USD, Decimal)
170
- token.getPrice(true) // share price
204
+ token.getPrice() // share price (USD, Decimal)
205
+ token.getPrice(true) // asset price
171
206
  token.convertTokensToUsd(amount) // TokenInput → USD
172
207
  token.convertUsdToTokens(usd) // USD → TokenInput
173
208
  token.convertTokenInputToShares(amount) // user input → shares
@@ -306,29 +341,60 @@ const zapper = token.getZapper('simple')
306
341
  const positionManager = token.getPositionManager('simple')
307
342
  ```
308
343
 
344
+ Prefer `token.getZapper(...)` so the zapper carries the token's setup-bound DEX aggregator. Direct construction must use the same setup result as the CToken it will operate on:
345
+
346
+ ```ts
347
+ import { Zapper } from "curvance"
348
+
349
+ const setup = await setupChain("monad-mainnet", wallet)
350
+ const token = setup.markets[0].tokens[0]
351
+ const zapperAddress = token.getPluginAddress("simple", "zapper")
352
+ if (zapperAddress == null) throw new Error("Simple zapper is not configured")
353
+
354
+ const zapper = new Zapper(
355
+ zapperAddress,
356
+ wallet,
357
+ "simple",
358
+ setup.setupConfigSnapshot,
359
+ setup.dexAgg,
360
+ )
361
+ ```
362
+
363
+ A direct `Zapper` built without the setup-bound adapter throws. A `Zapper`
364
+ from one setup result also refuses to build calldata for a CToken from another
365
+ setup result.
366
+
309
367
  ## ❯ Zapping (Swap + Deposit)
310
368
 
311
- Zap deposits allow depositing any token by swapping to the required underlying via the DEX aggregator.
369
+ Zap deposits allow depositing another token by swapping to the required underlying through the setup-bound DEX aggregator.
370
+
371
+ `token.getDepositTokens(search?)` is the route-discovery entrypoint. It always
372
+ includes the direct deposit asset and then adds native, vault, and simple-swap
373
+ routes only when the token and chain can execute them. DEX-sourced simple
374
+ routes require a market-bound executable adapter; unsupported DEX chains expose
375
+ readable markets but no simple zap or simple leverage routes.
312
376
 
313
377
  ```ts
314
378
  // Native token (MON) → deposit
315
379
  await token.approvePlugin('native-simple', 'zapper')
316
- await zapper.nativeZap(ctoken, amount, collateralize)
380
+ await token.depositAsCollateral(amount, 'native-simple')
317
381
 
318
382
  // Any ERC20 → swap → deposit
319
383
  await token.approvePlugin('simple', 'zapper')
320
- await token.approveUnderlying(amount)
321
- await token.depositAsCollateral(amount, {
384
+ const simpleZap = {
322
385
  type: 'simple',
323
386
  inputToken: inputTokenAddress,
324
387
  slippage: new Decimal(0.01) // 1%
325
- })
388
+ } as const
389
+ await token.approveZapAsset(simpleZap, amount)
390
+ await token.depositAsCollateral(amount, simpleZap)
326
391
  ```
327
392
 
328
393
  Check approval status for a zap before executing:
329
394
 
330
395
  ```ts
331
- const approved = await token.isZapAssetApproved(instructions, amount)
396
+ const rawZapAmount = toBigInt(amount, inputTokenDecimals)
397
+ const approved = await token.isZapAssetApproved(instructions, rawZapAmount)
332
398
  if (!approved) await token.approveZapAsset(instructions, amount)
333
399
  ```
334
400
 
@@ -338,11 +404,14 @@ Leverage uses the PositionManager plugin to atomically borrow and swap into the
338
404
 
339
405
  ```ts
340
406
  // One-step: deposit collateral + leverage
341
- await collateralToken.approveUnderlying(amount)
342
- await collateralToken.approvePlugin('simple', 'positionManager')
407
+ const positionManager = collateralToken.getPluginAddress('simple', 'positionManager')
408
+ if (positionManager == null) throw new Error("Simple position manager is not configured")
409
+
410
+ await collateralToken.approveUnderlying(amount, positionManager)
343
411
  await collateralToken.depositAndLeverage(amount, borrowToken, targetLeverage, 'simple', slippage)
344
412
 
345
413
  // Separate: deposit first, then leverage
414
+ await collateralToken.approveUnderlying(amount)
346
415
  await collateralToken.depositAsCollateral(amount)
347
416
  await collateralToken.leverageUp(borrowToken, new Decimal(3), 'simple', new Decimal(0.005))
348
417
 
@@ -371,7 +440,7 @@ await market.previewPositionHealthDeposit(ctoken, amount)
371
440
  await market.previewPositionHealthRedeem(ctoken, amount)
372
441
  await market.previewPositionHealthBorrow(borrowToken, amount)
373
442
  await market.previewPositionHealthRepay(borrowToken, amount)
374
- await market.previewPositionHealthLeverageUp(depositCToken, depositAmount, borrowCToken, borrowAmount)
443
+ await market.previewPositionHealthLeverageUp(depositCToken, borrowCToken, newLeverage, depositAmount)
375
444
  await market.previewPositionHealthLeverageDown(depositCToken, borrowCToken, newLeverage, currentLeverage)
376
445
 
377
446
  // Generic preview
@@ -386,7 +455,7 @@ const health = await market.previewPositionHealthBorrow(borrowToken, new Decimal
386
455
  if (health === null) {
387
456
  // remains solvent with infinite health
388
457
  } else if (health.lt(0.1)) {
389
- console.warn("Would drop to 10% health too risky")
458
+ console.warn("Would drop to 10% health - too risky")
390
459
  }
391
460
  ```
392
461
 
@@ -402,7 +471,10 @@ await market.multiHoldExpiresAt(markets) // cooldown across multiple markets
402
471
 
403
472
  ## ❯ Format Utilities
404
473
 
405
- Pure calculation helpers for building UI or simulating outcomes. All accept and return `Decimal`.
474
+ Pure calculation helpers for building UI or simulating outcomes. Amount and
475
+ leverage helpers primarily use `Decimal`; validation, health-status, slippage,
476
+ and normalization helpers also expose `number`, `bigint`, string, and structured
477
+ result types where those are the safer UI boundary.
406
478
 
407
479
  ### Leverage math
408
480
 
@@ -431,13 +503,13 @@ import { amplifyContractSlippage, toContractSwapSlippage } from "curvance"
431
503
  // user's raw `slippage` budget (reserved for variable DEX impact + drift).
432
504
  amplifyContractSlippage(baseSlippageBps, leverageDelta, bpsToAmplify)
433
505
 
434
- // Used by DEX aggregator adapters (KyberSwap etc.) in `quoteAction` to
506
+ // Used by DEX aggregator adapters in `quoteAction` to
435
507
  // compute the WAD-BPS slippage tolerance for the `Swap.slippage` struct
436
- // field consumed by on-chain `_swapSafe`. When the aggregator pre-deducts
437
- // a currency_in fee, the expansion absorbs that fee so `_swapSafe` doesn't
438
- // double-count it as swap slippage. Adapters whose fee model does NOT
439
- // pre-deduct (e.g., out-of-band referrer paid from output) should call
440
- // with `feeBps` omitted / 0n so no expansion applies.
508
+ // field consumed by on-chain `_swapSafe`. When an adapter's fee model is
509
+ // represented as value loss in the swap calldata (for example Kyber's
510
+ // currency_in fee), pass `feeBps` so `_swapSafe`
511
+ // does not treat deterministic fee loss as user slippage. Adapters whose
512
+ // fees are not observable as swap value loss should omit `feeBps` / pass 0n.
441
513
  toContractSwapSlippage(userSlippageBps, feeBps?)
442
514
  ```
443
515
 
@@ -491,7 +563,6 @@ amounts.normalizeCurrencyAmounts({ amount, currencyView, tokenDecimals, price })
491
563
  import {
492
564
  getContractAddresses,
493
565
  contractSetup,
494
- handleTransactionWithOracles,
495
566
  toDecimal, toBigInt,
496
567
  getDepositApy, getBorrowCost,
497
568
  getInterestYield, getNativeYield,
@@ -507,11 +578,10 @@ import {
507
578
  |---|---|
508
579
  | `getContractAddresses(chain)` | All contract addresses for a chain |
509
580
  | `contractSetup(provider, address, abi)` | Create a typed contract instance |
510
- | `handleTransactionWithOracles(...)` | Wraps a tx in a Redstone multicall when a pull oracle price write is required |
511
581
  | `toDecimal(value, decimals)` | `bigint` → `Decimal` |
512
582
  | `toBigInt(value, decimals)` | `Decimal` → `bigint` |
513
583
  | `getDepositApy(token, opportunities, apyOverrides)` | Total deposit yield (interest + Merkl + native) |
514
- | `getBorrowCost(token, opportunities)` | Net borrow cost may be negative when rewards exceed rate |
584
+ | `getBorrowCost(token, opportunities)` | Net borrow cost; may be negative when rewards exceed rate |
515
585
  | `getInterestYield(token)` | Lending APY only |
516
586
  | `getNativeYield(token, apyOverrides)` | Native yield component |
517
587
  | `getMerklDepositIncentives(tokenAddress, opportunities)` | Merkl reward APR for deposits |
@@ -522,25 +592,58 @@ import {
522
592
 
523
593
  The SDK supports configurable fees applied at the DEX aggregator layer for swaps. Fees are denominated in BPS of the swap input and charged on leverage, deleverage, deposit+leverage, and zap operations.
524
594
 
595
+ For standard Curvance app/front-end usage, omit `options.feePolicy` and let
596
+ `setupChain()` build the default Curvance fee policy:
597
+
598
+ ```ts
599
+ const { markets } = await setupChain("monad-mainnet", wallet)
600
+ ```
601
+
602
+ The default policy charges `CURVANCE_FEE_BPS` and resolves the fee receiver from
603
+ `CentralRegistry.daoAddress()` once during setup. App consumers should not
604
+ hardcode or pin a DAO fee receiver locally. Pass `options.feePolicy` only for an
605
+ intentional custom integration override.
606
+
525
607
  ```ts
526
- import { flatFeePolicy, NO_FEE_POLICY } from "curvance"
608
+ import {
609
+ CURVANCE_FEE_BPS,
610
+ flatFeePolicy,
611
+ setupChain,
612
+ } from "curvance"
613
+
614
+ const defaultSetup = await setupChain("monad-mainnet", wallet)
527
615
 
528
616
  const feePolicy = flatFeePolicy({
529
- bps: 10n, // 0.1% default fee
530
- feeReceiver: "0xYourAddress",
617
+ // Kyber-backed chains require one exact checker-compatible DEX fee.
618
+ bps: CURVANCE_FEE_BPS,
619
+ feeReceiver: defaultSetup.setupConfigSnapshot.feePolicy.feeReceiver,
531
620
  chain: "monad-mainnet",
532
- stableToStableBps: 2n, // optional lower fee for stable↔stable swaps
533
621
  })
534
622
 
535
623
  const { markets } = await setupChain("monad-mainnet", wallet, undefined, { feePolicy })
536
624
  ```
537
625
 
626
+ On Kyber-backed chains, setup validates explicit policies before rewards or
627
+ markets boot. A zero-fee policy, wrong BPS value, or wrong receiver rejects
628
+ with a checker-policy error. Context-dependent lower tiers such as
629
+ `stableToStableBps` are not valid for checker-bound Kyber routes because the
630
+ on-chain checker enforces one exact BPS value and DAO receiver. On
631
+ unsupported-Dex chains such as `arb-sepolia`, `NO_FEE_POLICY` remains valid and
632
+ setup skips the DAO lookup because no DEX route can execute there.
633
+
538
634
  The SDK automatically returns 0 bps for native ↔ wrapped-native swaps and same-token no-op zaps.
539
635
 
540
636
  ```ts
541
- // FeePolicy interface implement your own
637
+ // FeePolicy interface: implement your own
542
638
  interface FeePolicy {
639
+ // "any" marks chain-agnostic no-op policies; chain-bound policies must match setupChain.
640
+ chain?: "monad-mainnet" | "arb-sepolia" | "any";
543
641
  feeReceiver: address;
642
+ // Required on checker-bound routes when the policy is custom.
643
+ checkerCompatibility?: {
644
+ exactFeeBpsForDexSwaps: bigint;
645
+ feeReceiver: address;
646
+ };
544
647
  getFeeBps(ctx: FeePolicyContext): bigint;
545
648
  }
546
649
 
@@ -562,14 +665,18 @@ interface FeePolicyContext {
562
665
  ```ts
563
666
  import { fetchMerklOpportunities, fetchMerklUserRewards, fetchMerklCampaignsBySymbol } from "curvance"
564
667
 
565
- // All active opportunities (APR, token, type)
566
- const opportunities = await fetchMerklOpportunities()
668
+ // Active opportunities for a production display path (APR, token, type)
669
+ const opportunities = await fetchMerklOpportunities({ chainId: 143 })
567
670
 
568
671
  // Pending rewards for a user
569
672
  const rewards = await fetchMerklUserRewards({ wallet: address, chainId: 143 })
570
673
 
571
- // Campaigns for a specific token
572
- const campaigns = await fetchMerklCampaignsBySymbol({ tokenSymbol: "USDC" })
674
+ // Campaigns for a specific token on one chain
675
+ const campaigns = await fetchMerklCampaignsBySymbol({ tokenSymbol: "USDC", chainId: 143 })
676
+
677
+ // Chainless Merkl calls are all-chain utilities. Filter explicitly before
678
+ // using them in production multichain display paths.
679
+ const allChainOpportunities = await fetchMerklOpportunities({})
573
680
  ```
574
681
 
575
682
  ### Portfolio snapshots
@@ -577,9 +684,11 @@ const campaigns = await fetchMerklCampaignsBySymbol({ tokenSymbol: "USDC" })
577
684
  ```ts
578
685
  import { takePortfolioSnapshot, snapshotMarket } from "curvance"
579
686
 
580
- // Full portfolio across all markets
687
+ // Full portfolio across the current active-chain markets
581
688
  const snapshot = await takePortfolioSnapshot(account)
582
689
  // Returns: { account, chain, timestamp, totalDepositsUSD, totalDebtUSD, netUSD, dailyEarnings, dailyCost, markets[] }
690
+ // Each market row includes { chain, chainId }. Mixed-chain snapshots require:
691
+ // takePortfolioSnapshot(account, { markets, allowMixedChains: true })
583
692
 
584
693
  // Single market
585
694
  const marketSnapshot = snapshotMarket(market)
@@ -594,20 +703,26 @@ The `OptimizerReader` reads yield-rebalancing vaults that allocate across market
594
703
  ```ts
595
704
  import { ERC20, LendingOptimizer, OptimizerReader } from "curvance"
596
705
 
597
- const optimizer = new OptimizerReader(optimizerReaderAddress, provider)
706
+ const optimizerReader = new OptimizerReader(optimizerReaderAddress, provider)
598
707
 
599
- await optimizer.getOptimizerAPY(optimizerAddress)
708
+ await optimizerReader.getOptimizerAPY(optimizerAddress)
600
709
  // Returns: weighted-average optimizer APY in WAD
601
710
 
602
- await optimizer.getOptimizerMarketData(optimizerAddresses)
603
- // Returns: { totalAssets, sharePrice, performanceFee, apy, markets[] }
711
+ await optimizerReader.getOptimizerMarketData(optimizerAddresses)
712
+ // Returns: { totalAssets, sharePrice, performanceFee, apy, markets: [{ address, allocatedAssets, liquidity, allocationCap, allocationCapUtilizationBps }] }
604
713
 
605
- await optimizer.getOptimizerUserData(optimizerAddresses, account)
714
+ await optimizerReader.getOptimizerUserData(optimizerAddresses, account)
606
715
  // Returns: user balance and redeemable amounts
607
716
 
608
- await optimizer.optimalRebalance(optimizer, 100n)
717
+ await optimizerReader.optimalRebalance(optimizerAddress, 100n)
609
718
  // Returns: { actions: { cToken, assetsOrBps }[], bounds: { cToken, minBps, maxBps }[] }
610
719
 
720
+ await optimizerReader.optimalRebalanceAt(optimizerAddress, 100n, timestamp)
721
+ // Returns the rebalance plan projected at a specific timestamp
722
+
723
+ await optimizerReader.isBad(optimizerAddress)
724
+ // Returns bad cToken markets for the optimizer
725
+
611
726
  const asset = new ERC20(provider, assetAddress, undefined, undefined, signer)
612
727
  const vault = new LendingOptimizer(optimizerAddress, asset, provider, signer)
613
728
 
@@ -628,9 +743,25 @@ type USD_WAD = bigint // USD in 1e18 WAD format
628
743
  type TokenInput = Decimal // human-readable token amount
629
744
  type TypeBPS = bigint // basis points (10000 = 100%)
630
745
  type ChainRpcPrefix = "monad-mainnet" | "arb-sepolia"
746
+ type ChainEnvironment = "production-mainnet" | "testnet" | "local"
747
+ type curvance_read_provider = JsonRpcProvider
631
748
  type curvance_provider = JsonRpcSigner | Wallet | JsonRpcProvider
632
749
  type curvance_signer = JsonRpcSigner | Wallet
633
750
 
751
+ interface SetupConfigSnapshot {
752
+ chain: ChainRpcPrefix
753
+ chainId: number
754
+ environment: ChainEnvironment
755
+ assets: Readonly<ChainAssetConfig>
756
+ services: Readonly<ChainServiceConfig>
757
+ contracts: Readonly<Record<string, unknown>>
758
+ readProvider: curvance_read_provider
759
+ signer: curvance_signer | null
760
+ account: address | null
761
+ api_url: string
762
+ feePolicy: FeePolicy
763
+ }
764
+
634
765
  // Market categorization
635
766
  type MarketCategory = "stablecoin" | "staking" | "restaking" | "yield-stablecoin" | "blue-chip" | "native"
636
767
  type CollateralSource = "Renzo" | "Upshift" | "Yuzu" | "Native" | "Circle" | "Fastlane" | "Apriori" | "Mu Digital" | "Kintsu" | "Reservoir"
@@ -650,7 +781,9 @@ interface Quote {
650
781
  }
651
782
  ```
652
783
 
653
- All numeric return values are `bigint` or `Decimal` never plain JS `number`.
784
+ Core monetary, token, share, health, APY, and fixed-point values use `bigint`
785
+ or `Decimal`. Backend API DTOs may expose raw `number` fields before SDK
786
+ normalization; do not use those DTO fields as contract-scale values.
654
787
 
655
788
  ## ❯ Constants
656
789
 
@@ -668,7 +801,7 @@ DEFAULT_SLIPPAGE_BPS // 100n (1%)
668
801
 
669
802
  ### Leverage tuning (`LEVERAGE`)
670
803
 
671
- Exposed tuning block used by leverage preview / mutation paths. Values are considered tunable across releases SDK consumers pinning against specific values opt into the coupling.
804
+ Exposed tuning block used by leverage preview / mutation paths. Values are considered tunable across releases. SDK consumers pinning against specific values opt into the coupling.
672
805
 
673
806
  ```ts
674
807
  import { LEVERAGE } from 'curvance';
@@ -676,18 +809,18 @@ import { LEVERAGE } from 'curvance';
676
809
  LEVERAGE.MAX_LEVERAGE_FACTOR // Decimal(0.98)
677
810
  // Cap applied to the theoretical max leverage span. Reserves ~2% of the
678
811
  // equity-fraction slippage budget for deterministic loss channels
679
- // (CURVANCE_FEE_BPS, pool-fee variance, Redstone drift, share rounding)
812
+ // (CURVANCE_FEE_BPS, pool-fee variance, oracle drift, share rounding)
680
813
  // that would otherwise push post-op LTV above collRatio at the boundary.
681
814
 
682
815
  LEVERAGE.LEVERAGE_UP_BUFFER_BPS // 10n
683
816
  // Flat BPS buffer added to leverage-up slippage for share-rounding + fresh
684
- // Redstone price drift between snapshot RPC and tx broadcast. NOT amplified
817
+ // Oracle price drift between snapshot RPC and tx broadcast. NOT amplified
685
818
  // by (L-1); the contract's equity-fraction denominator handles amplification.
686
819
 
687
- LEVERAGE.DELEVERAGE_OVERHEAD_BPS // 20n
820
+ LEVERAGE.DELEVERAGE_OVERHEAD_BPS // 60n
688
821
  // BPS overhead added to full-deleverage swap sizing to absorb DEX impact
689
822
  // and oracle drift without leaving dust debt. The contract returns any
690
- // excess debt token to the user, so economic loss is zero but
823
+ // excess debt token to the user, so economic loss is zero, but
691
824
  // `checkSlippage` treats the intentional overshoot as equity loss and
692
825
  // amplifies it by (L-1), which the contract-slippage expansion compensates.
693
826
 
@@ -712,19 +845,68 @@ LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS // 30n
712
845
  |---|---|
713
846
  | [ethers v6](https://www.npmjs.com/package/ethers) | Typed contract interactions, providers, and signer handling |
714
847
  | [decimal.js](https://www.npmjs.com/package/decimal.js) | Arbitrary-precision math for all token amounts, prices, and rates |
715
- | [@redstone-finance/sdk](https://www.npmjs.com/package/@redstone-finance/sdk) | Price feed writes bundled into multicalls for pull-oracle adaptors |
716
848
 
717
- ## ❯ Pre-Publish Checklist
849
+ ## ❯ SDK Pre-Publish Checklist
850
+
851
+ Run before every SDK `npm publish`:
852
+
853
+ 1. **Typecheck, build, and deterministic transport gate green.**
854
+
855
+ ```bash
856
+ node node_modules/typescript/bin/tsc --noEmit
857
+ npm run build
858
+ npm run test:transport
859
+ ```
860
+
861
+ `npm test` is an alias for `test:transport`. `tests/rpc-config-shape.test.ts`
862
+ locks the structural invariants of `chain_rpc_config` (no known-bad RPCs,
863
+ no duplicate fallbacks, policy fields within sane ranges).
864
+
865
+ 2. **Fork gate green, or explicitly classified as pending.** `npm run test:fork`
866
+ is the live fork/write gate. If it skips because `TEST_RPC`, deployer keys,
867
+ or a generated fixture are missing, the SDK can be called
868
+ deterministic/package covered, but not fork-covered.
869
+
870
+ 3. **Package artifact smoke green.**
871
+
872
+ ```bash
873
+ npm run test:dist-smoke
874
+ npm pack --dry-run --json
875
+ ```
876
+
877
+ `prepack` and `prepublishOnly` rebuild `dist`, and `test:dist-smoke`
878
+ imports the packed package root. Package consumers load the artifact, so
879
+ source-green or build-green alone is not package-boundary proof.
880
+ The dry-run package should contain `README.md`, `package.json`, and `dist/**`;
881
+ source files and tests should not be published.
882
+
883
+ 4. **Workspace hygiene clean.**
884
+
885
+ ```bash
886
+ git diff --check
887
+ git status --short
888
+ ```
889
+
890
+ Confirm new imported production files are tracked. This matters because
891
+ dirty-tree tests can pass while a clean package checkout cannot import an
892
+ untracked source file.
893
+
894
+ ### SDK gate interpretation
895
+
896
+ - `test:transport`, `test:all`, `test:dist-smoke`, `npm pack --dry-run --json`,
897
+ and `git diff --check` green means the SDK is deterministic/package covered.
898
+ - `test:fork` must execute against a local Anvil-compatible fork before calling
899
+ the SDK fork-covered. A command that exits 0 after skip messages is not live
900
+ fork proof.
901
+ - App build, app Cypress/Vitest, and app RPC-origin probes are downstream
902
+ adoption checks. Run them after publishing or linking the packed SDK into the
903
+ app repo; they are not part of the SDK-only publish gate.
718
904
 
719
- Run before every `npm publish` that touches `src/chains/`, `src/setup.ts`,
720
- `src/retry-provider.ts`, or any RPC-adjacent code:
905
+ ### Post-Publish App Rollout Checks
721
906
 
722
- 1. **Unit tests green.** `npm test` must show all `test:transport` tests
723
- passing. `tests/rpc-config-shape.test.ts` locks the structural invariants
724
- of `chain_rpc_config` (no known-bad RPCs, no duplicate fallbacks, policy
725
- fields within sane ranges).
907
+ After publishing or linking a packed SDK artifact into the app repo:
726
908
 
727
- 2. **Live RPC probe against both app origins.** In the app repo:
909
+ 1. **For RPC-adjacent SDK changes, run the app-origin RPC probe.**
728
910
 
729
911
  ```bash
730
912
  cd path/to/curvance-app
@@ -746,13 +928,12 @@ Run before every `npm publish` that touches `src/chains/`, `src/setup.ts`,
746
928
  Deeper-cascade fallbacks (`fallbacks[1]+`) MAY have looser limits if
747
929
  documented inline with a comment in `chain_rpc_config`.
748
930
 
749
- 3. **Do not add the probe to CI.** The probe fires ~500 requests per run
931
+ 2. **Do not add the probe to CI.** The probe fires ~500 requests per run
750
932
  across 5-10 public RPCs from a single IP. Running it on every PR would
751
933
  trip per-IP rate limits and eventually provoke origin bans from the
752
- free RPCs we depend on recreating the exact failure mode
753
- (monadinfra 403'ing `staging.curvance.com`) that motivated building
754
- this probe.
934
+ free RPCs we depend on. That recreates the exact failure mode
935
+ (monadinfra 403'ing `staging.curvance.com`) that motivated this probe.
755
936
 
756
- 4. **Republish workflow.** Version bump `npm publish` in app repo,
757
- bump `curvance` in `package.json` to the new version `yarn install`
758
- commit `yarn.lock` deploy.
937
+ 3. **App rollout workflow.** Version bump -> `npm publish` -> in app repo,
938
+ bump `curvance` in `package.json` to the new version -> `yarn install`
939
+ -> commit `yarn.lock` -> deploy.