@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 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 间隔,不代表这个 client 只能注册某个 venue。一个 `AcexClient` 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户,用同一个 `AccountManager` 对比风险值。
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 使用 Jupiter Portfolio API 读取 Solana 钱包的借贷仓位,不需要私钥,也不支持 supply / borrow / repay / withdraw 等写操作。`accountId` 是你自定义的 SDK 账户名;Solana 钱包地址放在 `options.walletAddress`。如果只想观察某个 Juplend NFT position,可传 `options.positionId`。
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 只读,不支持订单和链上写操作;token 数量来自 USD / oracle price 反算
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`:Jupiter Portfolio API / vault 元数据连通性、lending balance facet、账户级 `riskRatio`、可选 `--position-id` 过滤单个 NFT position
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
- JUPITER_API_KEY=... JUPLEND_WALLET_ADDRESS=<wallet> bun run test:live:juplend -- --show-amounts
219
- JUPITER_API_KEY=... bun run test:live:juplend -- --account-id jup-loop-a --wallet-address <wallet> --position-id <nftId> --show-amounts
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 间隔,不会把 client 限定为某个 venue:
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` 是自定义逻辑账户名;Solana 钱包地址必须放在 `options.walletAddress`,可用 `options.positionId` 把同钱包下单个 NFT position 映射为独立账户
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` / `equity` / ...)都是 `BigNumber`。
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
- interface JuplendAccountCredentials {
965
- apiKey: string;
966
- }
967
-
968
- interface JuplendAccountOptions {
969
- walletAddress: string;
970
- positionId?: string;
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: JuplendAccountCredentials;
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
- equity?: BigNumber;
1252
+ netEquity?: BigNumber;
1253
+ riskEquity?: BigNumber;
1226
1254
  riskRatio?: BigNumber;
1227
- actualLeverage?: BigNumber;
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 失败或缺 `options.walletAddress` |
1445
- | `CREDENTIALS_MISSING` | 私有订阅 / 下单缺必要凭证,例如 Binance 缺 `apiKey/secret` 或 Juplend 缺 `apiKey` |
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 数量精度**:Portfolio API 提供 USD 聚合值,token 数量由 USD / vault oracle price 反算
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.4",
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 equity = firstString(input.accountEquity, input.totalEquity);
296
- const actualLeverage = calculateActualLeverage(equity, positions);
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
- equity,
300
+ netEquity,
301
+ riskEquity,
299
302
  riskRatio,
300
- actualLeverage,
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.equity &&
317
+ !risk.netEquity &&
318
+ !risk.riskEquity &&
315
319
  !risk.riskRatio &&
316
- !risk.actualLeverage &&
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 calculateActualLeverage(
327
- equity: string | undefined,
330
+ function calculateRiskLeverage(
331
+ riskEquity: string | undefined,
328
332
  positions: BinancePapiUmPosition[],
329
333
  ): string | undefined {
330
- if (!equity) {
334
+ if (!riskEquity) {
331
335
  return undefined;
332
336
  }
333
337
 
334
- const equityValue = new BigNumber(equity);
335
- if (!equityValue.isFinite() || equityValue.isZero()) {
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(equityValue).toString(10);
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
+ }