curvance 5.1.9 → 5.2.1

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 (115) hide show
  1. package/README.md +264 -67
  2. package/dist/abis/OptimizerReader.json +292 -1
  3. package/dist/abis/OptimizerZapper.json +177 -0
  4. package/dist/chains/arbitrum.d.ts.map +1 -1
  5. package/dist/chains/arbitrum.js +14 -1
  6. package/dist/chains/arbitrum.js.map +1 -1
  7. package/dist/chains/index.d.ts +19 -0
  8. package/dist/chains/index.d.ts.map +1 -1
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/monad.d.ts.map +1 -1
  11. package/dist/chains/monad.js +25 -2
  12. package/dist/chains/monad.js.map +1 -1
  13. package/dist/chains/services.d.ts +8 -0
  14. package/dist/chains/services.d.ts.map +1 -0
  15. package/dist/chains/services.js +9 -0
  16. package/dist/chains/services.js.map +1 -0
  17. package/dist/classes/Api.d.ts +7 -2
  18. package/dist/classes/Api.d.ts.map +1 -1
  19. package/dist/classes/Api.js +60 -24
  20. package/dist/classes/Api.js.map +1 -1
  21. package/dist/classes/BorrowableCToken.d.ts.map +1 -1
  22. package/dist/classes/BorrowableCToken.js +7 -2
  23. package/dist/classes/BorrowableCToken.js.map +1 -1
  24. package/dist/classes/CToken.d.ts +28 -15
  25. package/dist/classes/CToken.d.ts.map +1 -1
  26. package/dist/classes/CToken.js +217 -85
  27. package/dist/classes/CToken.js.map +1 -1
  28. package/dist/classes/DexAggregators/IDexAgg.d.ts +8 -0
  29. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  30. package/dist/classes/DexAggregators/KyberSwap.d.ts +5 -2
  31. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  32. package/dist/classes/DexAggregators/KyberSwap.js +41 -19
  33. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  34. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +7 -4
  35. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  36. package/dist/classes/DexAggregators/MultiDexAgg.js +62 -16
  37. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  38. package/dist/classes/DexAggregators/helpers.d.ts +1 -1
  39. package/dist/classes/DexAggregators/helpers.d.ts.map +1 -1
  40. package/dist/classes/DexAggregators/helpers.js +1 -1
  41. package/dist/classes/DexAggregators/helpers.js.map +1 -1
  42. package/dist/classes/DexAggregators/index.d.ts +0 -1
  43. package/dist/classes/DexAggregators/index.d.ts.map +1 -1
  44. package/dist/classes/DexAggregators/index.js +0 -1
  45. package/dist/classes/DexAggregators/index.js.map +1 -1
  46. package/dist/classes/LendingOptimizer.d.ts +62 -2
  47. package/dist/classes/LendingOptimizer.d.ts.map +1 -1
  48. package/dist/classes/LendingOptimizer.js +251 -4
  49. package/dist/classes/LendingOptimizer.js.map +1 -1
  50. package/dist/classes/Market.d.ts +5 -0
  51. package/dist/classes/Market.d.ts.map +1 -1
  52. package/dist/classes/Market.js +129 -30
  53. package/dist/classes/Market.js.map +1 -1
  54. package/dist/classes/NativeToken.d.ts +5 -2
  55. package/dist/classes/NativeToken.d.ts.map +1 -1
  56. package/dist/classes/NativeToken.js +5 -5
  57. package/dist/classes/NativeToken.js.map +1 -1
  58. package/dist/classes/OptimizerReader.d.ts +44 -4
  59. package/dist/classes/OptimizerReader.d.ts.map +1 -1
  60. package/dist/classes/OptimizerReader.js +133 -62
  61. package/dist/classes/OptimizerReader.js.map +1 -1
  62. package/dist/classes/OptimizerZapper.d.ts +24 -0
  63. package/dist/classes/OptimizerZapper.d.ts.map +1 -0
  64. package/dist/classes/OptimizerZapper.js +117 -0
  65. package/dist/classes/OptimizerZapper.js.map +1 -0
  66. package/dist/classes/PositionManager.d.ts +1 -0
  67. package/dist/classes/PositionManager.d.ts.map +1 -1
  68. package/dist/classes/PositionManager.js +25 -0
  69. package/dist/classes/PositionManager.js.map +1 -1
  70. package/dist/classes/ProtocolReader.d.ts +3 -0
  71. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  72. package/dist/classes/ProtocolReader.js +34 -0
  73. package/dist/classes/ProtocolReader.js.map +1 -1
  74. package/dist/classes/Zapper.d.ts +4 -1
  75. package/dist/classes/Zapper.d.ts.map +1 -1
  76. package/dist/classes/Zapper.js +34 -9
  77. package/dist/classes/Zapper.js.map +1 -1
  78. package/dist/classes/index.d.ts +2 -0
  79. package/dist/classes/index.d.ts.map +1 -1
  80. package/dist/classes/index.js +2 -0
  81. package/dist/classes/index.js.map +1 -1
  82. package/dist/contracts/index.d.ts +249 -248
  83. package/dist/contracts/index.d.ts.map +1 -1
  84. package/dist/contracts/index.js +3 -2
  85. package/dist/contracts/index.js.map +1 -1
  86. package/dist/contracts/monad-mainnet.json +3 -2
  87. package/dist/feePolicy.d.ts +29 -26
  88. package/dist/feePolicy.d.ts.map +1 -1
  89. package/dist/feePolicy.js +43 -34
  90. package/dist/feePolicy.js.map +1 -1
  91. package/dist/format/index.d.ts +5 -0
  92. package/dist/format/index.d.ts.map +1 -1
  93. package/dist/format/index.js +28 -0
  94. package/dist/format/index.js.map +1 -1
  95. package/dist/helpers.d.ts +248 -248
  96. package/dist/helpers.d.ts.map +1 -1
  97. package/dist/helpers.js +5 -5
  98. package/dist/helpers.js.map +1 -1
  99. package/dist/immutability.d.ts +6 -0
  100. package/dist/immutability.d.ts.map +1 -0
  101. package/dist/immutability.js +25 -0
  102. package/dist/immutability.js.map +1 -0
  103. package/dist/integrations/merkl.d.ts +9 -2
  104. package/dist/integrations/merkl.d.ts.map +1 -1
  105. package/dist/integrations/merkl.js +219 -11
  106. package/dist/integrations/merkl.js.map +1 -1
  107. package/dist/integrations/snapshot.d.ts +3 -0
  108. package/dist/integrations/snapshot.d.ts.map +1 -1
  109. package/dist/integrations/snapshot.js +47 -3
  110. package/dist/integrations/snapshot.js.map +1 -1
  111. package/dist/setup.d.ts +13 -3
  112. package/dist/setup.d.ts.map +1 -1
  113. package/dist/setup.js +101 -7
  114. package/dist/setup.js.map +1 -1
  115. package/package.json +7 -4
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
 
@@ -509,7 +581,7 @@ import {
509
581
  | `toDecimal(value, decimals)` | `bigint` → `Decimal` |
510
582
  | `toBigInt(value, decimals)` | `Decimal` → `bigint` |
511
583
  | `getDepositApy(token, opportunities, apyOverrides)` | Total deposit yield (interest + Merkl + native) |
512
- | `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 |
513
585
  | `getInterestYield(token)` | Lending APY only |
514
586
  | `getNativeYield(token, apyOverrides)` | Native yield component |
515
587
  | `getMerklDepositIncentives(tokenAddress, opportunities)` | Merkl reward APR for deposits |
@@ -520,25 +592,58 @@ import {
520
592
 
521
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.
522
594
 
595
+ For standard Curvance app/front-end usage, omit `options.feePolicy` and let
596
+ `setupChain()` build the default Curvance fee policy:
597
+
523
598
  ```ts
524
- import { flatFeePolicy, NO_FEE_POLICY } from "curvance"
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
+
607
+ ```ts
608
+ import {
609
+ CURVANCE_FEE_BPS,
610
+ flatFeePolicy,
611
+ setupChain,
612
+ } from "curvance"
613
+
614
+ const defaultSetup = await setupChain("monad-mainnet", wallet)
525
615
 
526
616
  const feePolicy = flatFeePolicy({
527
- bps: 10n, // 0.1% default fee
528
- feeReceiver: "0xYourAddress",
617
+ // Kyber-backed chains require one exact checker-compatible DEX fee.
618
+ bps: CURVANCE_FEE_BPS,
619
+ feeReceiver: defaultSetup.setupConfigSnapshot.feePolicy.feeReceiver,
529
620
  chain: "monad-mainnet",
530
- stableToStableBps: 2n, // optional lower fee for stable↔stable swaps
531
621
  })
532
622
 
533
623
  const { markets } = await setupChain("monad-mainnet", wallet, undefined, { feePolicy })
534
624
  ```
535
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
+
536
634
  The SDK automatically returns 0 bps for native ↔ wrapped-native swaps and same-token no-op zaps.
537
635
 
538
636
  ```ts
539
- // FeePolicy interface implement your own
637
+ // FeePolicy interface: implement your own
540
638
  interface FeePolicy {
639
+ // "any" marks chain-agnostic no-op policies; chain-bound policies must match setupChain.
640
+ chain?: "monad-mainnet" | "arb-sepolia" | "any";
541
641
  feeReceiver: address;
642
+ // Required on checker-bound routes when the policy is custom.
643
+ checkerCompatibility?: {
644
+ exactFeeBpsForDexSwaps: bigint;
645
+ feeReceiver: address;
646
+ };
542
647
  getFeeBps(ctx: FeePolicyContext): bigint;
543
648
  }
544
649
 
@@ -560,14 +665,18 @@ interface FeePolicyContext {
560
665
  ```ts
561
666
  import { fetchMerklOpportunities, fetchMerklUserRewards, fetchMerklCampaignsBySymbol } from "curvance"
562
667
 
563
- // All active opportunities (APR, token, type)
564
- const opportunities = await fetchMerklOpportunities()
668
+ // Active opportunities for a production display path (APR, token, type)
669
+ const opportunities = await fetchMerklOpportunities({ chainId: 143 })
565
670
 
566
671
  // Pending rewards for a user
567
672
  const rewards = await fetchMerklUserRewards({ wallet: address, chainId: 143 })
568
673
 
569
- // Campaigns for a specific token
570
- 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({})
571
680
  ```
572
681
 
573
682
  ### Portfolio snapshots
@@ -575,9 +684,11 @@ const campaigns = await fetchMerklCampaignsBySymbol({ tokenSymbol: "USDC" })
575
684
  ```ts
576
685
  import { takePortfolioSnapshot, snapshotMarket } from "curvance"
577
686
 
578
- // Full portfolio across all markets
687
+ // Full portfolio across the current active-chain markets
579
688
  const snapshot = await takePortfolioSnapshot(account)
580
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 })
581
692
 
582
693
  // Single market
583
694
  const marketSnapshot = snapshotMarket(market)
@@ -590,28 +701,47 @@ const marketSnapshot = snapshotMarket(market)
590
701
  The `OptimizerReader` reads yield-rebalancing vaults that allocate across markets.
591
702
 
592
703
  ```ts
593
- import { ERC20, LendingOptimizer, OptimizerReader } from "curvance"
704
+ import Decimal from "decimal.js"
705
+ import { ERC20, LendingOptimizer, OptimizerReader, setupChain } from "curvance"
594
706
 
595
- const optimizer = new OptimizerReader(optimizerReaderAddress, provider)
707
+ const optimizerReader = new OptimizerReader(optimizerReaderAddress, provider)
596
708
 
597
- await optimizer.getOptimizerAPY(optimizerAddress)
709
+ await optimizerReader.getOptimizerAPY(optimizerAddress)
598
710
  // Returns: weighted-average optimizer APY in WAD
599
711
 
600
- await optimizer.getOptimizerMarketData(optimizerAddresses)
601
- // Returns: { totalAssets, sharePrice, performanceFee, apy, markets[] }
712
+ await optimizerReader.getOptimizerMarketData(optimizerAddresses)
713
+ // Returns: { totalAssets, sharePrice, performanceFee, apy, markets: [{ address, allocatedAssets, liquidity, allocationCap, allocationCapUtilizationBps }] }
602
714
 
603
- await optimizer.getOptimizerUserData(optimizerAddresses, account)
715
+ await optimizerReader.getOptimizerUserData(optimizerAddresses, account)
604
716
  // Returns: user balance and redeemable amounts
605
717
 
606
- await optimizer.optimalRebalance(optimizer, 100n)
718
+ await optimizerReader.optimalRebalance(optimizerAddress, 100n)
607
719
  // Returns: { actions: { cToken, assetsOrBps }[], bounds: { cToken, minBps, maxBps }[] }
608
720
 
721
+ await optimizerReader.optimalRebalanceAt(optimizerAddress, 100n, timestamp)
722
+ // Returns the rebalance plan projected at a specific timestamp
723
+
724
+ await optimizerReader.isBad(optimizerAddress)
725
+ // Returns bad cToken markets for the optimizer
726
+
609
727
  const asset = new ERC20(provider, assetAddress, undefined, undefined, signer)
610
- const vault = new LendingOptimizer(optimizerAddress, asset, provider, signer)
728
+ const setup = await setupChain("monad-mainnet", signer)
729
+ const vault = new LendingOptimizer(optimizerAddress, asset, provider, signer, {
730
+ setup: setup.setupConfigSnapshot,
731
+ dexAgg: setup.dexAgg,
732
+ })
611
733
 
612
734
  await vault.deposit(amount, account)
613
735
  await vault.withdraw(amount, account, account)
614
736
  await vault.redeem(shares, account, account)
737
+
738
+ const zap = {
739
+ type: "optimizer",
740
+ inputToken: usdcAddress,
741
+ slippage: new Decimal("0.01"),
742
+ } as const
743
+ await vault.approveZapAsset(zap, amount)
744
+ await vault.deposit(amount, zap, account)
615
745
  ```
616
746
 
617
747
  ## ❯ TypeScript Types
@@ -626,9 +756,25 @@ type USD_WAD = bigint // USD in 1e18 WAD format
626
756
  type TokenInput = Decimal // human-readable token amount
627
757
  type TypeBPS = bigint // basis points (10000 = 100%)
628
758
  type ChainRpcPrefix = "monad-mainnet" | "arb-sepolia"
759
+ type ChainEnvironment = "production-mainnet" | "testnet" | "local"
760
+ type curvance_read_provider = JsonRpcProvider
629
761
  type curvance_provider = JsonRpcSigner | Wallet | JsonRpcProvider
630
762
  type curvance_signer = JsonRpcSigner | Wallet
631
763
 
764
+ interface SetupConfigSnapshot {
765
+ chain: ChainRpcPrefix
766
+ chainId: number
767
+ environment: ChainEnvironment
768
+ assets: Readonly<ChainAssetConfig>
769
+ services: Readonly<ChainServiceConfig>
770
+ contracts: Readonly<Record<string, unknown>>
771
+ readProvider: curvance_read_provider
772
+ signer: curvance_signer | null
773
+ account: address | null
774
+ api_url: string
775
+ feePolicy: FeePolicy
776
+ }
777
+
632
778
  // Market categorization
633
779
  type MarketCategory = "stablecoin" | "staking" | "restaking" | "yield-stablecoin" | "blue-chip" | "native"
634
780
  type CollateralSource = "Renzo" | "Upshift" | "Yuzu" | "Native" | "Circle" | "Fastlane" | "Apriori" | "Mu Digital" | "Kintsu" | "Reservoir"
@@ -648,7 +794,9 @@ interface Quote {
648
794
  }
649
795
  ```
650
796
 
651
- All numeric return values are `bigint` or `Decimal` never plain JS `number`.
797
+ Core monetary, token, share, health, APY, and fixed-point values use `bigint`
798
+ or `Decimal`. Backend API DTOs may expose raw `number` fields before SDK
799
+ normalization; do not use those DTO fields as contract-scale values.
652
800
 
653
801
  ## ❯ Constants
654
802
 
@@ -666,7 +814,7 @@ DEFAULT_SLIPPAGE_BPS // 100n (1%)
666
814
 
667
815
  ### Leverage tuning (`LEVERAGE`)
668
816
 
669
- 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.
817
+ 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.
670
818
 
671
819
  ```ts
672
820
  import { LEVERAGE } from 'curvance';
@@ -682,10 +830,10 @@ LEVERAGE.LEVERAGE_UP_BUFFER_BPS // 10n
682
830
  // Oracle price drift between snapshot RPC and tx broadcast. NOT amplified
683
831
  // by (L-1); the contract's equity-fraction denominator handles amplification.
684
832
 
685
- LEVERAGE.DELEVERAGE_OVERHEAD_BPS // 20n
833
+ LEVERAGE.DELEVERAGE_OVERHEAD_BPS // 60n
686
834
  // BPS overhead added to full-deleverage swap sizing to absorb DEX impact
687
835
  // and oracle drift without leaving dust debt. The contract returns any
688
- // excess debt token to the user, so economic loss is zero but
836
+ // excess debt token to the user, so economic loss is zero, but
689
837
  // `checkSlippage` treats the intentional overshoot as equity loss and
690
838
  // amplifies it by (L-1), which the contract-slippage expansion compensates.
691
839
 
@@ -711,17 +859,67 @@ LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS // 30n
711
859
  | [ethers v6](https://www.npmjs.com/package/ethers) | Typed contract interactions, providers, and signer handling |
712
860
  | [decimal.js](https://www.npmjs.com/package/decimal.js) | Arbitrary-precision math for all token amounts, prices, and rates |
713
861
 
714
- ## ❯ Pre-Publish Checklist
862
+ ## ❯ SDK Pre-Publish Checklist
863
+
864
+ Run before every SDK `npm publish`:
865
+
866
+ 1. **Typecheck, build, and deterministic transport gate green.**
867
+
868
+ ```bash
869
+ node node_modules/typescript/bin/tsc --noEmit
870
+ npm run build
871
+ npm run test:transport
872
+ ```
873
+
874
+ `npm test` is an alias for `test:transport`. `tests/rpc-config-shape.test.ts`
875
+ locks the structural invariants of `chain_rpc_config` (no known-bad RPCs,
876
+ no duplicate fallbacks, policy fields within sane ranges).
877
+
878
+ 2. **Fork gate green, or explicitly classified as pending.** `npm run test:fork`
879
+ is the live fork/write gate. If it skips because `TEST_RPC`, deployer keys,
880
+ or a generated fixture are missing, the SDK can be called
881
+ deterministic/package covered, but not fork-covered.
882
+
883
+ 3. **Package artifact smoke green.**
884
+
885
+ ```bash
886
+ npm run test:dist-smoke
887
+ npm pack --dry-run --json
888
+ ```
889
+
890
+ `prepack` and `prepublishOnly` rebuild `dist`, and `test:dist-smoke`
891
+ imports the packed package root. Package consumers load the artifact, so
892
+ source-green or build-green alone is not package-boundary proof.
893
+ The dry-run package should contain `README.md`, `package.json`, and `dist/**`;
894
+ source files and tests should not be published.
895
+
896
+ 4. **Workspace hygiene clean.**
897
+
898
+ ```bash
899
+ git diff --check
900
+ git status --short
901
+ ```
902
+
903
+ Confirm new imported production files are tracked. This matters because
904
+ dirty-tree tests can pass while a clean package checkout cannot import an
905
+ untracked source file.
906
+
907
+ ### SDK gate interpretation
908
+
909
+ - `test:transport`, `test:all`, `test:dist-smoke`, `npm pack --dry-run --json`,
910
+ and `git diff --check` green means the SDK is deterministic/package covered.
911
+ - `test:fork` must execute against a local Anvil-compatible fork before calling
912
+ the SDK fork-covered. A command that exits 0 after skip messages is not live
913
+ fork proof.
914
+ - App build, app Cypress/Vitest, and app RPC-origin probes are downstream
915
+ adoption checks. Run them after publishing or linking the packed SDK into the
916
+ app repo; they are not part of the SDK-only publish gate.
715
917
 
716
- Run before every `npm publish` that touches `src/chains/`, `src/setup.ts`,
717
- `src/retry-provider.ts`, or any RPC-adjacent code:
918
+ ### Post-Publish App Rollout Checks
718
919
 
719
- 1. **Unit tests green.** `npm test` must show all `test:transport` tests
720
- passing. `tests/rpc-config-shape.test.ts` locks the structural invariants
721
- of `chain_rpc_config` (no known-bad RPCs, no duplicate fallbacks, policy
722
- fields within sane ranges).
920
+ After publishing or linking a packed SDK artifact into the app repo:
723
921
 
724
- 2. **Live RPC probe against both app origins.** In the app repo:
922
+ 1. **For RPC-adjacent SDK changes, run the app-origin RPC probe.**
725
923
 
726
924
  ```bash
727
925
  cd path/to/curvance-app
@@ -743,13 +941,12 @@ Run before every `npm publish` that touches `src/chains/`, `src/setup.ts`,
743
941
  Deeper-cascade fallbacks (`fallbacks[1]+`) MAY have looser limits if
744
942
  documented inline with a comment in `chain_rpc_config`.
745
943
 
746
- 3. **Do not add the probe to CI.** The probe fires ~500 requests per run
944
+ 2. **Do not add the probe to CI.** The probe fires ~500 requests per run
747
945
  across 5-10 public RPCs from a single IP. Running it on every PR would
748
946
  trip per-IP rate limits and eventually provoke origin bans from the
749
- free RPCs we depend on recreating the exact failure mode
750
- (monadinfra 403'ing `staging.curvance.com`) that motivated building
751
- this probe.
947
+ free RPCs we depend on. That recreates the exact failure mode
948
+ (monadinfra 403'ing `staging.curvance.com`) that motivated this probe.
752
949
 
753
- 4. **Republish workflow.** Version bump `npm publish` in app repo,
754
- bump `curvance` in `package.json` to the new version `yarn install`
755
- commit `yarn.lock` deploy.
950
+ 3. **App rollout workflow.** Version bump -> `npm publish` -> in app repo,
951
+ bump `curvance` in `package.json` to the new version -> `yarn install`
952
+ -> commit `yarn.lock` -> deploy.