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.
- package/README.md +264 -67
- package/dist/abis/OptimizerReader.json +292 -1
- package/dist/abis/OptimizerZapper.json +177 -0
- package/dist/chains/arbitrum.d.ts.map +1 -1
- package/dist/chains/arbitrum.js +14 -1
- package/dist/chains/arbitrum.js.map +1 -1
- package/dist/chains/index.d.ts +19 -0
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/monad.d.ts.map +1 -1
- package/dist/chains/monad.js +25 -2
- package/dist/chains/monad.js.map +1 -1
- package/dist/chains/services.d.ts +8 -0
- package/dist/chains/services.d.ts.map +1 -0
- package/dist/chains/services.js +9 -0
- package/dist/chains/services.js.map +1 -0
- package/dist/classes/Api.d.ts +7 -2
- package/dist/classes/Api.d.ts.map +1 -1
- package/dist/classes/Api.js +60 -24
- package/dist/classes/Api.js.map +1 -1
- package/dist/classes/BorrowableCToken.d.ts.map +1 -1
- package/dist/classes/BorrowableCToken.js +7 -2
- package/dist/classes/BorrowableCToken.js.map +1 -1
- package/dist/classes/CToken.d.ts +28 -15
- package/dist/classes/CToken.d.ts.map +1 -1
- package/dist/classes/CToken.js +217 -85
- package/dist/classes/CToken.js.map +1 -1
- package/dist/classes/DexAggregators/IDexAgg.d.ts +8 -0
- package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.d.ts +5 -2
- package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.js +41 -19
- package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts +7 -4
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.js +62 -16
- package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
- package/dist/classes/DexAggregators/helpers.d.ts +1 -1
- package/dist/classes/DexAggregators/helpers.d.ts.map +1 -1
- package/dist/classes/DexAggregators/helpers.js +1 -1
- package/dist/classes/DexAggregators/helpers.js.map +1 -1
- package/dist/classes/DexAggregators/index.d.ts +0 -1
- package/dist/classes/DexAggregators/index.d.ts.map +1 -1
- package/dist/classes/DexAggregators/index.js +0 -1
- package/dist/classes/DexAggregators/index.js.map +1 -1
- package/dist/classes/LendingOptimizer.d.ts +62 -2
- package/dist/classes/LendingOptimizer.d.ts.map +1 -1
- package/dist/classes/LendingOptimizer.js +251 -4
- package/dist/classes/LendingOptimizer.js.map +1 -1
- package/dist/classes/Market.d.ts +5 -0
- package/dist/classes/Market.d.ts.map +1 -1
- package/dist/classes/Market.js +129 -30
- package/dist/classes/Market.js.map +1 -1
- package/dist/classes/NativeToken.d.ts +5 -2
- package/dist/classes/NativeToken.d.ts.map +1 -1
- package/dist/classes/NativeToken.js +5 -5
- package/dist/classes/NativeToken.js.map +1 -1
- package/dist/classes/OptimizerReader.d.ts +44 -4
- package/dist/classes/OptimizerReader.d.ts.map +1 -1
- package/dist/classes/OptimizerReader.js +133 -62
- package/dist/classes/OptimizerReader.js.map +1 -1
- package/dist/classes/OptimizerZapper.d.ts +24 -0
- package/dist/classes/OptimizerZapper.d.ts.map +1 -0
- package/dist/classes/OptimizerZapper.js +117 -0
- package/dist/classes/OptimizerZapper.js.map +1 -0
- package/dist/classes/PositionManager.d.ts +1 -0
- package/dist/classes/PositionManager.d.ts.map +1 -1
- package/dist/classes/PositionManager.js +25 -0
- package/dist/classes/PositionManager.js.map +1 -1
- package/dist/classes/ProtocolReader.d.ts +3 -0
- package/dist/classes/ProtocolReader.d.ts.map +1 -1
- package/dist/classes/ProtocolReader.js +34 -0
- package/dist/classes/ProtocolReader.js.map +1 -1
- package/dist/classes/Zapper.d.ts +4 -1
- package/dist/classes/Zapper.d.ts.map +1 -1
- package/dist/classes/Zapper.js +34 -9
- package/dist/classes/Zapper.js.map +1 -1
- package/dist/classes/index.d.ts +2 -0
- package/dist/classes/index.d.ts.map +1 -1
- package/dist/classes/index.js +2 -0
- package/dist/classes/index.js.map +1 -1
- package/dist/contracts/index.d.ts +249 -248
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +3 -2
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/monad-mainnet.json +3 -2
- package/dist/feePolicy.d.ts +29 -26
- package/dist/feePolicy.d.ts.map +1 -1
- package/dist/feePolicy.js +43 -34
- package/dist/feePolicy.js.map +1 -1
- package/dist/format/index.d.ts +5 -0
- package/dist/format/index.d.ts.map +1 -1
- package/dist/format/index.js +28 -0
- package/dist/format/index.js.map +1 -1
- package/dist/helpers.d.ts +248 -248
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +5 -5
- package/dist/helpers.js.map +1 -1
- package/dist/immutability.d.ts +6 -0
- package/dist/immutability.d.ts.map +1 -0
- package/dist/immutability.js +25 -0
- package/dist/immutability.js.map +1 -0
- package/dist/integrations/merkl.d.ts +9 -2
- package/dist/integrations/merkl.d.ts.map +1 -1
- package/dist/integrations/merkl.js +219 -11
- package/dist/integrations/merkl.js.map +1 -1
- package/dist/integrations/snapshot.d.ts +3 -0
- package/dist/integrations/snapshot.d.ts.map +1 -1
- package/dist/integrations/snapshot.js +47 -3
- package/dist/integrations/snapshot.js.map +1 -1
- package/dist/setup.d.ts +13 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +101 -7
- package/dist/setup.js.map +1 -1
- 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.
|
|
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 } =
|
|
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; //
|
|
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
|
|
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
|
|
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() //
|
|
170
|
-
token.getPrice(true) //
|
|
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
|
|
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
|
|
380
|
+
await token.depositAsCollateral(amount, 'native-simple')
|
|
317
381
|
|
|
318
382
|
// Any ERC20 → swap → deposit
|
|
319
383
|
await token.approvePlugin('simple', 'zapper')
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
704
|
+
import Decimal from "decimal.js"
|
|
705
|
+
import { ERC20, LendingOptimizer, OptimizerReader, setupChain } from "curvance"
|
|
594
706
|
|
|
595
|
-
const
|
|
707
|
+
const optimizerReader = new OptimizerReader(optimizerReaderAddress, provider)
|
|
596
708
|
|
|
597
|
-
await
|
|
709
|
+
await optimizerReader.getOptimizerAPY(optimizerAddress)
|
|
598
710
|
// Returns: weighted-average optimizer APY in WAD
|
|
599
711
|
|
|
600
|
-
await
|
|
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
|
|
715
|
+
await optimizerReader.getOptimizerUserData(optimizerAddresses, account)
|
|
604
716
|
// Returns: user balance and redeemable amounts
|
|
605
717
|
|
|
606
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
|
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 //
|
|
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
|
|
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
|
-
|
|
717
|
-
`src/retry-provider.ts`, or any RPC-adjacent code:
|
|
918
|
+
### Post-Publish App Rollout Checks
|
|
718
919
|
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
750
|
-
(monadinfra 403'ing `staging.curvance.com`) that motivated
|
|
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
|
-
|
|
754
|
-
bump `curvance` in `package.json` to the new version
|
|
755
|
-
|
|
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.
|