@imbingox/acex 0.3.0-beta.4 → 0.3.0-beta.6
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 +18 -9
- package/docs/api.md +47 -19
- package/package.json +3 -1
- package/src/adapters/binance/private-adapter.ts +16 -12
- package/src/adapters/juplend/lend-read.ts +150 -0
- package/src/adapters/juplend/private-adapter.ts +434 -163
- package/src/adapters/types.ts +3 -2
- package/src/client/context.ts +1 -1
- package/src/client/private-subscription-coordinator.ts +2 -2
- package/src/client/runtime.ts +4 -1
- package/src/managers/account-manager.ts +12 -8
- package/src/managers/order-manager.ts +8 -0
- package/src/types/account.ts +3 -2
- package/src/types/shared.ts +14 -9
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ await client.stop();
|
|
|
60
60
|
|
|
61
61
|
### 同一个 client 同时使用 Binance + Juplend
|
|
62
62
|
|
|
63
|
-
`createClient({ account: { binance: { riskPollIntervalMs }, juplend: { pollIntervalMs } } })` 只是分别配置 Binance 风险/仓位校准间隔和 Juplend 账户 polling
|
|
63
|
+
`createClient({ account: { binance: { riskPollIntervalMs }, juplend: { pollIntervalMs, rpcUrl, jupApiKey } } })` 只是分别配置 Binance 风险/仓位校准间隔和 Juplend 账户 polling / RPC / Jup API,不代表这个 client 只能注册某个 venue。一个 `AcexClient` 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户,用同一个 `AccountManager` 对比风险值。
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
const client = createClient({
|
|
@@ -70,6 +70,8 @@ const client = createClient({
|
|
|
70
70
|
},
|
|
71
71
|
juplend: {
|
|
72
72
|
pollIntervalMs: 30_000,
|
|
73
|
+
rpcUrl: process.env.SOL_HELIUS_RPC,
|
|
74
|
+
jupApiKey: process.env.JUP_API,
|
|
73
75
|
},
|
|
74
76
|
},
|
|
75
77
|
});
|
|
@@ -87,15 +89,21 @@ await client.registerAccount({
|
|
|
87
89
|
await client.registerAccount({
|
|
88
90
|
accountId: "jup-loop-a",
|
|
89
91
|
venue: "juplend",
|
|
90
|
-
credentials: {
|
|
91
|
-
apiKey: process.env.JUPITER_API_KEY!,
|
|
92
|
-
},
|
|
93
92
|
options: {
|
|
94
93
|
walletAddress: "<solana-wallet-address>",
|
|
95
94
|
positionId: "<optional-nft-position-id>",
|
|
96
95
|
},
|
|
97
96
|
});
|
|
98
97
|
|
|
98
|
+
await client.registerAccount({
|
|
99
|
+
accountId: "jup-loop-direct",
|
|
100
|
+
venue: "juplend",
|
|
101
|
+
options: {
|
|
102
|
+
vaultId: "<vault-id>",
|
|
103
|
+
positionId: "<nft-position-id>",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
99
107
|
await client.account.subscribeAccount({ accountId: "jup-loop-a" });
|
|
100
108
|
await client.account.subscribeAccount({ accountId: "main-binance" });
|
|
101
109
|
await client.order.subscribeOrders({ accountId: "main-binance" });
|
|
@@ -115,7 +123,7 @@ console.log({
|
|
|
115
123
|
await client.stop();
|
|
116
124
|
```
|
|
117
125
|
|
|
118
|
-
Juplend 使用
|
|
126
|
+
Juplend 使用 `@jup-ag/lend-read` 通过 Solana RPC 读取原生借贷仓位,不需要私钥,也不支持 supply / borrow / repay / withdraw 等写操作。`accountId` 是你自定义的 SDK 账户名。聚合钱包全部仓位时传 `options.walletAddress`;只想观察单个仓位时,可直接传 `options.vaultId + options.positionId`,这样不会先扫整个钱包。`account.juplend.rpcUrl` 可显式指定 RPC;未指定时默认读取 `SOL_HELIUS_RPC`,再 fallback 到 SDK 默认 RPC。token metadata / price 优先走 Jup 官方 `Tokens V2 + Price V3`,可通过 `account.juplend.jupApiKey` 或环境变量 `JUP_API` 注入;拿不到时退回 lite vault metadata。
|
|
119
127
|
|
|
120
128
|
### 查询 venue 能力
|
|
121
129
|
|
|
@@ -151,7 +159,7 @@ const capabilities = client.listVenueCapabilities();
|
|
|
151
159
|
|
|
152
160
|
- 运行时 market/order 能力只支持 `binance`;`okx` / `bybit` / `gate` 仅类型定义
|
|
153
161
|
- 账户视图支持 Binance PAPI UM 与 Juplend 只读借贷账户
|
|
154
|
-
- Juplend
|
|
162
|
+
- Juplend 只读,不支持订单和链上写操作;仓位数量来自 `@jup-ag/lend-read` 原生 position 数据
|
|
155
163
|
- Funding Rate 仅支持 Binance 永续合约,来自 mark price websocket;不支持现货和交割合约
|
|
156
164
|
- `createOrder()` 只支持 `limit` / `market`;条件单、改单、账户级全撤不支持
|
|
157
165
|
- 双向持仓账户下单时必须显式传 `positionSide`
|
|
@@ -209,14 +217,15 @@ bun run test:live:order:soak
|
|
|
209
217
|
|
|
210
218
|
- `market`:`loadMarkets()`、`subscribeL1Book()`、`subscribeFundingRate()`、`getL1Book()` / `getL1Books()`、`getFundingRate()` / `getFundingRates()`、对应事件流和可选断线重连(`--disconnect-target funding` 可单独验证资金费率重连)
|
|
211
219
|
- `account`:Binance PAPI UM 账户 bootstrap、余额/仓位/风险投影、private stream 更新和可选重连
|
|
212
|
-
- `juplend
|
|
220
|
+
- `juplend`:`@jup-ag/lend-read` + Jup Tokens/Price API 连通性、lending balance facet、账户级 `riskRatio`、支持 `--wallet-address` 聚合或 `--vault-id + --position-id` 单仓直读
|
|
213
221
|
- `order`:open orders bootstrap、`subscribeOrders()`、订单事件投影和可选重连
|
|
214
222
|
|
|
215
223
|
Juplend live smoke 示例:
|
|
216
224
|
|
|
217
225
|
```bash
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
SOL_HELIUS_RPC=... JUPLEND_WALLET_ADDRESS=<wallet> bun run test:live:juplend -- --show-amounts
|
|
227
|
+
bun run test:live:juplend -- --account-id jup-loop-a --wallet-address <wallet> --position-id <nftId> --rpc-url <rpc> --show-amounts
|
|
228
|
+
bun run test:live:juplend -- --account-id jup-loop-a --vault-id <vaultId> --position-id <nftId> --rpc-url <rpc> --show-amounts
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
### 发布流程
|
package/docs/api.md
CHANGED
|
@@ -64,7 +64,7 @@ for await (const event of client.market.events.l1BookUpdates({
|
|
|
64
64
|
await client.stop();
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
同一个 client 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户。`account.binance.riskPollIntervalMs` 只配置 Binance 风险/仓位校准间隔;`account.juplend.pollIntervalMs` 只配置 Juplend polling
|
|
67
|
+
同一个 client 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户。`account.binance.riskPollIntervalMs` 只配置 Binance 风险/仓位校准间隔;`account.juplend.pollIntervalMs` / `rpcUrl` / `jupApiKey` 只配置 Juplend polling、RPC 和 Jup API,不会把 client 限定为某个 venue:
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
70
|
const client = createClient({
|
|
@@ -74,6 +74,8 @@ const client = createClient({
|
|
|
74
74
|
},
|
|
75
75
|
juplend: {
|
|
76
76
|
pollIntervalMs: 30_000,
|
|
77
|
+
rpcUrl: process.env.SOL_HELIUS_RPC,
|
|
78
|
+
jupApiKey: process.env.JUP_API,
|
|
77
79
|
},
|
|
78
80
|
},
|
|
79
81
|
});
|
|
@@ -91,13 +93,21 @@ await client.registerAccount({
|
|
|
91
93
|
await client.registerAccount({
|
|
92
94
|
accountId: "jup-loop-a",
|
|
93
95
|
venue: "juplend",
|
|
94
|
-
credentials: { apiKey: process.env.JUPITER_API_KEY! },
|
|
95
96
|
options: {
|
|
96
97
|
walletAddress: "<solana-wallet-address>",
|
|
97
98
|
positionId: "<optional-nft-position-id>",
|
|
98
99
|
},
|
|
99
100
|
});
|
|
100
101
|
|
|
102
|
+
await client.registerAccount({
|
|
103
|
+
accountId: "jup-loop-direct",
|
|
104
|
+
venue: "juplend",
|
|
105
|
+
options: {
|
|
106
|
+
vaultId: "<vault-id>",
|
|
107
|
+
positionId: "<nft-position-id>",
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
101
111
|
await client.account.subscribeAccount({ accountId: "main-binance" });
|
|
102
112
|
await client.account.subscribeAccount({ accountId: "jup-loop-a" });
|
|
103
113
|
|
|
@@ -216,6 +226,7 @@ const client = createClient({
|
|
|
216
226
|
},
|
|
217
227
|
juplend: {
|
|
218
228
|
pollIntervalMs: 30_000,
|
|
229
|
+
rpcUrl: process.env.SOL_HELIUS_RPC,
|
|
219
230
|
},
|
|
220
231
|
},
|
|
221
232
|
});
|
|
@@ -295,12 +306,20 @@ await client.registerAccount({
|
|
|
295
306
|
await client.registerAccount({
|
|
296
307
|
accountId: "jup-loop-a",
|
|
297
308
|
venue: "juplend",
|
|
298
|
-
credentials: { apiKey: jupiterApiKey },
|
|
299
309
|
options: {
|
|
300
310
|
walletAddress,
|
|
301
311
|
positionId: "101", // 可选;不传则聚合该钱包全部 Juplend positions
|
|
302
312
|
},
|
|
303
313
|
});
|
|
314
|
+
|
|
315
|
+
await client.registerAccount({
|
|
316
|
+
accountId: "jup-loop-direct",
|
|
317
|
+
venue: "juplend",
|
|
318
|
+
options: {
|
|
319
|
+
vaultId: "1",
|
|
320
|
+
positionId: "101", // 直接读取单个 vault 内的 NFT position
|
|
321
|
+
},
|
|
322
|
+
});
|
|
304
323
|
```
|
|
305
324
|
|
|
306
325
|
约束:
|
|
@@ -309,7 +328,8 @@ await client.registerAccount({
|
|
|
309
328
|
- 凭证校验发生在 `subscribeAccount()` / `subscribeOrders()` 时,不是注册时
|
|
310
329
|
- `updateAccountCredentials()` 可以在私有订阅活跃时调用,SDK 会按需重建私有链路
|
|
311
330
|
- `removeAccount()` 比 `unsubscribeAccount()` 更彻底:账户配置、凭证、账户级缓存都会清理
|
|
312
|
-
- Juplend 的 `accountId`
|
|
331
|
+
- Juplend 的 `accountId` 是自定义逻辑账户名;可传 `options.walletAddress` 聚合钱包全部仓位,或传 `options.vaultId + options.positionId` 直接读取单个仓位
|
|
332
|
+
- Juplend 不要求 `credentials`;原生 read 默认读取 `SOL_HELIUS_RPC`,也可通过 `account.juplend.rpcUrl` 显式覆盖;token metadata / price 优先读取 `account.juplend.jupApiKey` 或环境变量 `JUP_API`
|
|
313
333
|
|
|
314
334
|
### 4.5 `getStatus()` / `getHealth()`
|
|
315
335
|
|
|
@@ -618,7 +638,9 @@ const btcPosition = client.account.getPosition({
|
|
|
618
638
|
const risk = client.account.getRiskSnapshot("main-binance");
|
|
619
639
|
```
|
|
620
640
|
|
|
621
|
-
所有数量字段(`free` / `used` / `total` / `size` / `entryPrice` / `
|
|
641
|
+
所有数量字段(`free` / `used` / `total` / `size` / `entryPrice` / `netEquity` / `riskEquity` / ...)都是 `BigNumber`。
|
|
642
|
+
|
|
643
|
+
`RiskSnapshot.netEquity` 表示不含风控折算的净资产价值;`riskEquity` 表示抵押系数或清算阈值折算后的风控净权益。Binance 使用 `actualEquity` / `accountEquity` 映射这两个字段;Juplend 使用 `totalCollateralUsd - totalDebtUsd` / `Σ(suppliedValue × liquidationThreshold) - totalDebtUsd`。
|
|
622
644
|
|
|
623
645
|
> **注意**:`AccountSnapshot.balances` 是 `Record<string, BalanceSnapshot>`,不是数组;需要数组视图用 `getBalances()`。
|
|
624
646
|
|
|
@@ -938,6 +960,8 @@ interface AccountRuntimeOptions {
|
|
|
938
960
|
};
|
|
939
961
|
juplend?: {
|
|
940
962
|
pollIntervalMs?: number;
|
|
963
|
+
rpcUrl?: string;
|
|
964
|
+
jupApiKey?: string;
|
|
941
965
|
};
|
|
942
966
|
}
|
|
943
967
|
|
|
@@ -961,14 +985,17 @@ interface BinanceAccountOptions {
|
|
|
961
985
|
recvWindow?: number;
|
|
962
986
|
}
|
|
963
987
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
988
|
+
type JuplendAccountOptions =
|
|
989
|
+
| {
|
|
990
|
+
walletAddress: string;
|
|
991
|
+
vaultId?: string;
|
|
992
|
+
positionId?: string;
|
|
993
|
+
}
|
|
994
|
+
| {
|
|
995
|
+
walletAddress?: string;
|
|
996
|
+
vaultId: string;
|
|
997
|
+
positionId: string;
|
|
998
|
+
};
|
|
972
999
|
|
|
973
1000
|
type RegisterAccountInput =
|
|
974
1001
|
| {
|
|
@@ -980,7 +1007,7 @@ type RegisterAccountInput =
|
|
|
980
1007
|
| {
|
|
981
1008
|
accountId: string;
|
|
982
1009
|
venue: "juplend";
|
|
983
|
-
credentials
|
|
1010
|
+
credentials?: AccountCredentials;
|
|
984
1011
|
options: JuplendAccountOptions;
|
|
985
1012
|
};
|
|
986
1013
|
|
|
@@ -1222,9 +1249,10 @@ interface PositionSnapshot {
|
|
|
1222
1249
|
interface RiskSnapshot {
|
|
1223
1250
|
accountId: string;
|
|
1224
1251
|
venue: Venue;
|
|
1225
|
-
|
|
1252
|
+
netEquity?: BigNumber;
|
|
1253
|
+
riskEquity?: BigNumber;
|
|
1226
1254
|
riskRatio?: BigNumber;
|
|
1227
|
-
|
|
1255
|
+
riskLeverage?: BigNumber;
|
|
1228
1256
|
initialMargin?: BigNumber;
|
|
1229
1257
|
maintenanceMargin?: BigNumber;
|
|
1230
1258
|
exchangeTs?: number;
|
|
@@ -1441,8 +1469,8 @@ try {
|
|
|
1441
1469
|
| `MARKET_STREAM_TIMEOUT` | market stream 首条消息超时 |
|
|
1442
1470
|
| `ACCOUNT_ALREADY_EXISTS` | 重复注册同一个 `accountId` |
|
|
1443
1471
|
| `ACCOUNT_NOT_FOUND` | `accountId` 未注册或已被移除 |
|
|
1444
|
-
| `ACCOUNT_BOOTSTRAP_FAILED` | `subscribeAccount()` 过程中账户快照拉取失败,例如 Juplend HTTP/API
|
|
1445
|
-
| `CREDENTIALS_MISSING` | 私有订阅 / 下单缺必要凭证,例如 Binance 缺 `apiKey/secret`
|
|
1472
|
+
| `ACCOUNT_BOOTSTRAP_FAILED` | `subscribeAccount()` 过程中账户快照拉取失败,例如 Juplend HTTP/API 失败、缺 `options.walletAddress`,或 direct 模式下缺失/提供了无效的 `options.vaultId`、`options.positionId` |
|
|
1473
|
+
| `CREDENTIALS_MISSING` | 私有订阅 / 下单缺必要凭证,例如 Binance 缺 `apiKey/secret` |
|
|
1446
1474
|
| `ORDER_BOOTSTRAP_FAILED` | `subscribeOrders()` 过程中 open orders 拉取失败 |
|
|
1447
1475
|
| `ORDER_INPUT_INVALID` | 下单/撤单本地输入校验失败(如缺 price、缺 id) |
|
|
1448
1476
|
| `ORDER_CREATE_FAILED` | 交易所拒单 / REST 报错 |
|
|
@@ -1455,7 +1483,7 @@ try {
|
|
|
1455
1483
|
- **市场数据**:真实落地 Binance L1 Book(Spot + USDⓈ-M + COIN-M)和 Binance 永续 Funding Rate
|
|
1456
1484
|
- **私有链路**:Binance PAPI UM 使用 listenKey/WebSocket;Juplend 使用 HTTP polling,只提供账户只读视图
|
|
1457
1485
|
- **Juplend 写操作**:不支持 supply / borrow / repay / withdraw,不支持 `OrderManager` 下单撤单
|
|
1458
|
-
- **Juplend
|
|
1486
|
+
- **Juplend 数据源**:账户主数据来自 `@jup-ag/lend-read` 原生 position read;token metadata / spot price 优先来自 Jup 官方 `Tokens V2 + Price V3`,lite vault metadata 只做 fallback
|
|
1459
1487
|
- **Funding Rate**:仅永续合约支持,来自 mark price websocket;不支持现货和交割合约
|
|
1460
1488
|
- **下单类型**:`createOrder()` 仅支持 `limit` / `market`;条件单、改单不支持
|
|
1461
1489
|
- **撤单范围**:`cancelAllOrders()` 必须传 `symbol`,不支持账户级全撤
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.6",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"typescript": "^6.0.2"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@jup-ag/lend-read": "^0.0.12",
|
|
57
|
+
"@solana/web3.js": "^1.98.4",
|
|
56
58
|
"bignumber.js": "^11.0.0"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -38,6 +38,7 @@ interface BinancePapiBalance {
|
|
|
38
38
|
|
|
39
39
|
interface BinancePapiAccount {
|
|
40
40
|
accountEquity?: string;
|
|
41
|
+
actualEquity?: string;
|
|
41
42
|
totalEquity?: string;
|
|
42
43
|
accountInitialMargin?: string;
|
|
43
44
|
totalInitialMargin?: string;
|
|
@@ -292,12 +293,14 @@ function mapAccountRisk(
|
|
|
292
293
|
const riskRatio = uniMmr
|
|
293
294
|
? new BigNumber(1).dividedBy(uniMmr).toString(10)
|
|
294
295
|
: undefined;
|
|
295
|
-
const
|
|
296
|
-
const
|
|
296
|
+
const netEquity = firstString(input.actualEquity);
|
|
297
|
+
const riskEquity = firstString(input.accountEquity, input.totalEquity);
|
|
298
|
+
const riskLeverage = calculateRiskLeverage(riskEquity, positions);
|
|
297
299
|
const risk: RawRiskUpdate = {
|
|
298
|
-
|
|
300
|
+
netEquity,
|
|
301
|
+
riskEquity,
|
|
299
302
|
riskRatio,
|
|
300
|
-
|
|
303
|
+
riskLeverage,
|
|
301
304
|
initialMargin: firstString(
|
|
302
305
|
input.accountInitialMargin,
|
|
303
306
|
input.totalInitialMargin,
|
|
@@ -311,9 +314,10 @@ function mapAccountRisk(
|
|
|
311
314
|
};
|
|
312
315
|
|
|
313
316
|
if (
|
|
314
|
-
!risk.
|
|
317
|
+
!risk.netEquity &&
|
|
318
|
+
!risk.riskEquity &&
|
|
315
319
|
!risk.riskRatio &&
|
|
316
|
-
!risk.
|
|
320
|
+
!risk.riskLeverage &&
|
|
317
321
|
!risk.initialMargin &&
|
|
318
322
|
!risk.maintenanceMargin
|
|
319
323
|
) {
|
|
@@ -323,16 +327,16 @@ function mapAccountRisk(
|
|
|
323
327
|
return risk;
|
|
324
328
|
}
|
|
325
329
|
|
|
326
|
-
function
|
|
327
|
-
|
|
330
|
+
function calculateRiskLeverage(
|
|
331
|
+
riskEquity: string | undefined,
|
|
328
332
|
positions: BinancePapiUmPosition[],
|
|
329
333
|
): string | undefined {
|
|
330
|
-
if (!
|
|
334
|
+
if (!riskEquity) {
|
|
331
335
|
return undefined;
|
|
332
336
|
}
|
|
333
337
|
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
338
|
+
const riskEquityValue = new BigNumber(riskEquity);
|
|
339
|
+
if (!riskEquityValue.isFinite() || riskEquityValue.isZero()) {
|
|
336
340
|
return undefined;
|
|
337
341
|
}
|
|
338
342
|
|
|
@@ -348,7 +352,7 @@ function calculateActualLeverage(
|
|
|
348
352
|
|
|
349
353
|
return grossExposure.isZero()
|
|
350
354
|
? undefined
|
|
351
|
-
: grossExposure.dividedBy(
|
|
355
|
+
: grossExposure.dividedBy(riskEquityValue).toString(10);
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
function mapUmPosition(
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Client, DEFAULT_RPC_URL } from "@jup-ag/lend-read";
|
|
2
|
+
import { PublicKey } from "@solana/web3.js";
|
|
3
|
+
|
|
4
|
+
interface StringLike {
|
|
5
|
+
toString(): string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface JuplendVaultLike {
|
|
9
|
+
constantViews: {
|
|
10
|
+
vaultId: number;
|
|
11
|
+
supplyToken: StringLike;
|
|
12
|
+
borrowToken: StringLike;
|
|
13
|
+
};
|
|
14
|
+
configs: {
|
|
15
|
+
liquidationThreshold: StringLike;
|
|
16
|
+
};
|
|
17
|
+
exchangePricesAndRates: {
|
|
18
|
+
supplyRateVault: StringLike;
|
|
19
|
+
borrowRateVault: StringLike;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JuplendPositionLike {
|
|
24
|
+
nftId: number;
|
|
25
|
+
supply: StringLike;
|
|
26
|
+
borrow: StringLike;
|
|
27
|
+
dustBorrow: StringLike;
|
|
28
|
+
vault: JuplendVaultLike;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface JuplendVaultReader {
|
|
32
|
+
getAllUserPositions(user: PublicKey): Promise<JuplendPositionLike[]>;
|
|
33
|
+
getPositionByVaultId(
|
|
34
|
+
vaultId: number,
|
|
35
|
+
nftId: number,
|
|
36
|
+
): Promise<JuplendPositionLike>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface JuplendReadSdkLike {
|
|
40
|
+
createVaultReader(rpcUrl: string): JuplendVaultReader;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface JuplendReadPosition {
|
|
44
|
+
nftId: string;
|
|
45
|
+
vaultId: string;
|
|
46
|
+
supplyAmount: string;
|
|
47
|
+
borrowAmount: string;
|
|
48
|
+
dustBorrowAmount: string;
|
|
49
|
+
liquidationThresholdRaw: string;
|
|
50
|
+
supplyRateRaw: string;
|
|
51
|
+
borrowRateRaw: string;
|
|
52
|
+
supplyMintAddress: string;
|
|
53
|
+
borrowMintAddress: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const defaultSdk: JuplendReadSdkLike = {
|
|
57
|
+
createVaultReader(rpcUrl: string): JuplendVaultReader {
|
|
58
|
+
return new Client(rpcUrl).vault;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let activeSdk: JuplendReadSdkLike = defaultSdk;
|
|
63
|
+
let readerCache = new Map<string, JuplendVaultReader>();
|
|
64
|
+
|
|
65
|
+
export function getJuplendRpcUrl(explicitRpcUrl?: string): string {
|
|
66
|
+
return explicitRpcUrl || process.env.SOL_HELIUS_RPC || DEFAULT_RPC_URL;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getVaultReader(rpcUrl: string): JuplendVaultReader {
|
|
70
|
+
const cached = readerCache.get(rpcUrl);
|
|
71
|
+
if (cached) {
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const created = activeSdk.createVaultReader(rpcUrl);
|
|
76
|
+
readerCache.set(rpcUrl, created);
|
|
77
|
+
return created;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toJuplendId(value: string, field: "vaultId" | "positionId"): number {
|
|
81
|
+
const parsed = Number(value);
|
|
82
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
83
|
+
throw new Error(`Invalid Juplend ${field}: ${value}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mapReadPosition(position: JuplendPositionLike): JuplendReadPosition {
|
|
90
|
+
const vaultId = position.vault.constantViews.vaultId;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
nftId: `${position.nftId}`,
|
|
94
|
+
vaultId: `${vaultId}`,
|
|
95
|
+
supplyAmount: position.supply.toString(),
|
|
96
|
+
borrowAmount: position.borrow.toString(),
|
|
97
|
+
dustBorrowAmount: position.dustBorrow.toString(),
|
|
98
|
+
liquidationThresholdRaw:
|
|
99
|
+
position.vault.configs.liquidationThreshold.toString(),
|
|
100
|
+
supplyRateRaw:
|
|
101
|
+
position.vault.exchangePricesAndRates.supplyRateVault.toString(),
|
|
102
|
+
borrowRateRaw:
|
|
103
|
+
position.vault.exchangePricesAndRates.borrowRateVault.toString(),
|
|
104
|
+
supplyMintAddress: position.vault.constantViews.supplyToken.toString(),
|
|
105
|
+
borrowMintAddress: position.vault.constantViews.borrowToken.toString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function readJuplendPositions(input: {
|
|
110
|
+
walletAddress?: string;
|
|
111
|
+
vaultId?: string;
|
|
112
|
+
positionId?: string;
|
|
113
|
+
explicitRpcUrl?: string;
|
|
114
|
+
}): Promise<{
|
|
115
|
+
rpcUrl: string;
|
|
116
|
+
positions: JuplendReadPosition[];
|
|
117
|
+
}> {
|
|
118
|
+
const rpcUrl = getJuplendRpcUrl(input.explicitRpcUrl);
|
|
119
|
+
const reader = getVaultReader(rpcUrl);
|
|
120
|
+
const rawPositions =
|
|
121
|
+
input.vaultId && input.positionId
|
|
122
|
+
? [
|
|
123
|
+
await reader.getPositionByVaultId(
|
|
124
|
+
toJuplendId(input.vaultId, "vaultId"),
|
|
125
|
+
toJuplendId(input.positionId, "positionId"),
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
: input.walletAddress
|
|
129
|
+
? await reader.getAllUserPositions(new PublicKey(input.walletAddress))
|
|
130
|
+
: (() => {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Juplend read requires options.walletAddress or options.vaultId + options.positionId",
|
|
133
|
+
);
|
|
134
|
+
})();
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
rpcUrl,
|
|
138
|
+
positions: rawPositions.map((position) => mapReadPosition(position)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setJuplendReadSdkForTests(sdk: JuplendReadSdkLike): void {
|
|
143
|
+
activeSdk = sdk;
|
|
144
|
+
readerCache = new Map<string, JuplendVaultReader>();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resetJuplendReadSdkForTests(): void {
|
|
148
|
+
activeSdk = defaultSdk;
|
|
149
|
+
readerCache = new Map<string, JuplendVaultReader>();
|
|
150
|
+
}
|