@imbingox/acex 0.3.0-beta.1 → 0.3.0-beta.3
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 +71 -24
- package/docs/api.md +1457 -0
- package/package.json +5 -2
- package/src/adapters/binance/adapter.ts +11 -2
- package/src/adapters/binance/market-catalog.ts +2 -2
- package/src/adapters/binance/private-adapter.ts +44 -4
- package/src/adapters/juplend/private-adapter.ts +517 -0
- package/src/adapters/types.ts +34 -4
- package/src/client/context.ts +16 -11
- package/src/client/private-subscription-coordinator.ts +101 -47
- package/src/client/runtime.ts +64 -20
- package/src/client/venue-capabilities.ts +109 -0
- package/src/errors.ts +1 -1
- package/src/internal/filters.ts +9 -9
- package/src/managers/account-manager.ts +95 -58
- package/src/managers/market-manager.ts +45 -45
- package/src/managers/order-manager.ts +49 -56
- package/src/types/account.ts +30 -10
- package/src/types/client.ts +73 -2
- package/src/types/market.ts +12 -16
- package/src/types/order.ts +7 -7
- package/src/types/shared.ts +43 -7
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.3",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"index.ts",
|
|
19
|
-
"src/"
|
|
19
|
+
"src/",
|
|
20
|
+
"docs/api.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"changeset": "changeset",
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
"test:live:account": "bun run scripts/live-account-smoke.ts",
|
|
31
32
|
"test:live:account:smoke": "bun run scripts/live-account-smoke.ts --duration 10",
|
|
32
33
|
"test:live:account:soak": "bun run scripts/live-account-smoke.ts --duration 60 --disconnect-after 5",
|
|
34
|
+
"test:live:juplend": "bun run scripts/live-juplend-account-smoke.ts",
|
|
35
|
+
"test:live:juplend:smoke": "bun run scripts/live-juplend-account-smoke.ts --duration 35 --show-amounts",
|
|
33
36
|
"test:live:market": "bun run scripts/live-market-smoke.ts",
|
|
34
37
|
"test:live:market:smoke": "bun run scripts/live-market-smoke.ts --duration 10",
|
|
35
38
|
"test:live:market:soak": "bun run scripts/live-market-smoke.ts --duration 60 --disconnect-after 5 --disconnect-target perp",
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MarketDefinition,
|
|
3
|
+
VenueMarketCapabilities,
|
|
4
|
+
} from "../../types/index.ts";
|
|
2
5
|
import type {
|
|
3
6
|
FundingRateStreamCallbacks,
|
|
4
7
|
FundingRateStreamOptions,
|
|
@@ -15,7 +18,13 @@ import {
|
|
|
15
18
|
} from "./market-catalog.ts";
|
|
16
19
|
|
|
17
20
|
export class BinanceMarketAdapter implements MarketAdapter {
|
|
18
|
-
readonly
|
|
21
|
+
readonly venue = "binance" as const;
|
|
22
|
+
readonly marketCapabilities: VenueMarketCapabilities = {
|
|
23
|
+
catalog: "supported",
|
|
24
|
+
l1Book: "supported",
|
|
25
|
+
fundingRate: "market_dependent",
|
|
26
|
+
marketTypes: ["spot", "swap", "future"],
|
|
27
|
+
};
|
|
19
28
|
|
|
20
29
|
private readonly definitions = new Map<string, BinanceMarketDefinition>();
|
|
21
30
|
|
|
@@ -135,7 +135,7 @@ function normalizeSpotSymbol(
|
|
|
135
135
|
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
|
-
|
|
138
|
+
venue: "binance",
|
|
139
139
|
family: "spot",
|
|
140
140
|
symbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
|
|
141
141
|
id: symbol.symbol,
|
|
@@ -180,7 +180,7 @@ function normalizeDerivativesSymbol(
|
|
|
180
180
|
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
181
181
|
|
|
182
182
|
return {
|
|
183
|
-
|
|
183
|
+
venue: "binance",
|
|
184
184
|
family,
|
|
185
185
|
symbol: buildFuturesSymbol(
|
|
186
186
|
symbol.baseAsset,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHmac } from "node:crypto";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
2
3
|
import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AccountCredentials,
|
|
6
|
+
PositionSide,
|
|
7
|
+
VenueAccountCapabilities,
|
|
8
|
+
VenueOrderCapabilities,
|
|
9
|
+
} from "../../types/index.ts";
|
|
4
10
|
import type {
|
|
5
11
|
CancelAllOrdersRequest,
|
|
6
12
|
CancelOrderRequest,
|
|
@@ -280,9 +286,13 @@ function mapAccountRisk(
|
|
|
280
286
|
input: BinancePapiAccount,
|
|
281
287
|
receivedAt: number,
|
|
282
288
|
): RawRiskUpdate | undefined {
|
|
289
|
+
const uniMmr = firstString(input.uniMMR);
|
|
290
|
+
const riskRatio = uniMmr
|
|
291
|
+
? new BigNumber(1).dividedBy(uniMmr).toString(10)
|
|
292
|
+
: undefined;
|
|
283
293
|
const risk: RawRiskUpdate = {
|
|
284
294
|
equity: firstString(input.accountEquity, input.totalEquity),
|
|
285
|
-
|
|
295
|
+
riskRatio,
|
|
286
296
|
initialMargin: firstString(
|
|
287
297
|
input.accountInitialMargin,
|
|
288
298
|
input.totalInitialMargin,
|
|
@@ -297,7 +307,7 @@ function mapAccountRisk(
|
|
|
297
307
|
|
|
298
308
|
if (
|
|
299
309
|
!risk.equity &&
|
|
300
|
-
!risk.
|
|
310
|
+
!risk.riskRatio &&
|
|
301
311
|
!risk.initialMargin &&
|
|
302
312
|
!risk.maintenanceMargin
|
|
303
313
|
) {
|
|
@@ -473,7 +483,37 @@ async function readJson<T>(response: Response, url: string): Promise<T> {
|
|
|
473
483
|
}
|
|
474
484
|
|
|
475
485
|
export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
476
|
-
readonly
|
|
486
|
+
readonly venue = "binance" as const;
|
|
487
|
+
readonly readOnly = false;
|
|
488
|
+
readonly notes = [
|
|
489
|
+
"Capabilities describe the current SDK runtime, not Binance's full exchange API surface.",
|
|
490
|
+
"Funding rate support depends on the market type.",
|
|
491
|
+
"Order commands currently target Binance PAPI UM USD-M symbols; venue-level order.supported does not mean every Binance market type is orderable.",
|
|
492
|
+
];
|
|
493
|
+
readonly accountCapabilities: VenueAccountCapabilities = {
|
|
494
|
+
register: "supported",
|
|
495
|
+
snapshot: "supported",
|
|
496
|
+
updates: "websocket",
|
|
497
|
+
balances: "supported",
|
|
498
|
+
positions: "supported",
|
|
499
|
+
risk: "supported",
|
|
500
|
+
lending: "unsupported",
|
|
501
|
+
credentialsRequired: true,
|
|
502
|
+
};
|
|
503
|
+
readonly orderCapabilities: VenueOrderCapabilities = {
|
|
504
|
+
supported: true,
|
|
505
|
+
openOrders: "supported",
|
|
506
|
+
updates: "websocket",
|
|
507
|
+
create: "supported",
|
|
508
|
+
cancel: "supported",
|
|
509
|
+
cancelAll: "symbol",
|
|
510
|
+
orderTypes: ["limit", "market"],
|
|
511
|
+
timeInForce: ["gtc", "post_only"],
|
|
512
|
+
postOnly: true,
|
|
513
|
+
reduceOnly: true,
|
|
514
|
+
positionSide: "required_for_hedge",
|
|
515
|
+
clientOrderId: true,
|
|
516
|
+
};
|
|
477
517
|
|
|
478
518
|
async bootstrapAccount(
|
|
479
519
|
credentials: AccountCredentials,
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { AcexError } from "../../errors.ts";
|
|
3
|
+
import type {
|
|
4
|
+
AccountCredentials,
|
|
5
|
+
VenueAccountCapabilities,
|
|
6
|
+
VenueOrderCapabilities,
|
|
7
|
+
} from "../../types/index.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CancelAllOrdersRequest,
|
|
10
|
+
CancelOrderRequest,
|
|
11
|
+
CreateOrderRequest,
|
|
12
|
+
PrivateStreamCallbacks,
|
|
13
|
+
PrivateStreamOptions,
|
|
14
|
+
PrivateUserDataAdapter,
|
|
15
|
+
RawAccountBootstrap,
|
|
16
|
+
RawBalanceUpdate,
|
|
17
|
+
RawOrderUpdate,
|
|
18
|
+
RawRiskUpdate,
|
|
19
|
+
StreamHandle,
|
|
20
|
+
} from "../types.ts";
|
|
21
|
+
|
|
22
|
+
interface JuplendPortfolioResponse {
|
|
23
|
+
elements?: JuplendPortfolioElement[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JuplendPortfolioElement {
|
|
27
|
+
data?: {
|
|
28
|
+
link?: string;
|
|
29
|
+
suppliedValue?: number | string;
|
|
30
|
+
borrowedValue?: number | string;
|
|
31
|
+
value?: number | string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface JuplendVaultResponse {
|
|
36
|
+
data?: JuplendVault[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JuplendVault {
|
|
40
|
+
id?: number | string;
|
|
41
|
+
vaultId?: number | string;
|
|
42
|
+
supplyToken?: JuplendToken;
|
|
43
|
+
borrowToken?: JuplendToken;
|
|
44
|
+
liquidationThreshold?: number | string;
|
|
45
|
+
loanToValue?: number | string;
|
|
46
|
+
supplyRate?: number | string;
|
|
47
|
+
borrowRate?: number | string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface JuplendToken {
|
|
51
|
+
symbol?: string;
|
|
52
|
+
asset?: string;
|
|
53
|
+
oraclePrice?: number | string;
|
|
54
|
+
price?: number | string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface JuplendMappedAccount {
|
|
58
|
+
balances: RawBalanceUpdate[];
|
|
59
|
+
risk?: RawRiskUpdate;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface BalanceAccumulator {
|
|
63
|
+
asset: string;
|
|
64
|
+
supplied: BigNumber;
|
|
65
|
+
borrowed: BigNumber;
|
|
66
|
+
supplyAPY?: BigNumber;
|
|
67
|
+
borrowAPY?: BigNumber;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface JuplendAccountOptions {
|
|
71
|
+
walletAddress: string;
|
|
72
|
+
positionId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PORTFOLIO_BASE_URL = "https://api.jup.ag/portfolio/v1";
|
|
76
|
+
const VAULTS_URL = "https://lite-api.jup.ag/lend/v1/borrow/vaults";
|
|
77
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
78
|
+
const VAULT_CACHE_TTL_MS = 60 * 60 * 1_000;
|
|
79
|
+
const LINK_PATTERN = /\/borrow\/([^/]+)\/nfts\/([^/?#]+)/;
|
|
80
|
+
|
|
81
|
+
let vaultCache:
|
|
82
|
+
| {
|
|
83
|
+
loadedAt: number;
|
|
84
|
+
vaults: Map<string, JuplendVault>;
|
|
85
|
+
}
|
|
86
|
+
| undefined;
|
|
87
|
+
let vaultCachePromise: Promise<Map<string, JuplendVault>> | undefined;
|
|
88
|
+
|
|
89
|
+
function requireApiKey(credentials: AccountCredentials): string {
|
|
90
|
+
if (!credentials.apiKey) {
|
|
91
|
+
throw new Error("credentials.apiKey required");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return credentials.apiKey;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getJuplendAccountOptions(
|
|
98
|
+
accountOptions?: Record<string, unknown>,
|
|
99
|
+
): JuplendAccountOptions {
|
|
100
|
+
const walletAddress = accountOptions?.walletAddress;
|
|
101
|
+
if (typeof walletAddress !== "string" || !walletAddress) {
|
|
102
|
+
throw new Error("options.walletAddress required");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const positionId = accountOptions.positionId;
|
|
106
|
+
if (positionId !== undefined && typeof positionId !== "string") {
|
|
107
|
+
throw new Error("options.positionId must be a string");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
walletAddress,
|
|
112
|
+
positionId: positionId || undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toBigNumber(value: number | string | undefined): BigNumber {
|
|
117
|
+
return value === undefined ? new BigNumber(0) : new BigNumber(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeThreshold(value: number | string | undefined): BigNumber {
|
|
121
|
+
const threshold = toBigNumber(value);
|
|
122
|
+
return threshold.gt(1) ? threshold.dividedBy(1000) : threshold;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenAsset(token: JuplendToken | undefined): string | undefined {
|
|
126
|
+
return token?.symbol ?? token?.asset;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function tokenPrice(token: JuplendToken | undefined): BigNumber | undefined {
|
|
130
|
+
const price = toBigNumber(token?.oraclePrice ?? token?.price);
|
|
131
|
+
return price.gt(0) ? price : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractPositionLink(
|
|
135
|
+
link: string | undefined,
|
|
136
|
+
): { vaultId: string; positionId: string } | undefined {
|
|
137
|
+
if (!link) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const match = LINK_PATTERN.exec(link);
|
|
142
|
+
if (!match?.[1] || !match[2]) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
vaultId: match[1],
|
|
148
|
+
positionId: match[2],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getVaultId(vault: JuplendVault): string | undefined {
|
|
153
|
+
const id = vault.id ?? vault.vaultId;
|
|
154
|
+
return id === undefined ? undefined : `${id}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setAccumulator(
|
|
158
|
+
map: Map<string, BalanceAccumulator>,
|
|
159
|
+
asset: string,
|
|
160
|
+
): BalanceAccumulator {
|
|
161
|
+
const existing = map.get(asset);
|
|
162
|
+
if (existing) {
|
|
163
|
+
return existing;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const next: BalanceAccumulator = {
|
|
167
|
+
asset,
|
|
168
|
+
supplied: new BigNumber(0),
|
|
169
|
+
borrowed: new BigNumber(0),
|
|
170
|
+
};
|
|
171
|
+
map.set(asset, next);
|
|
172
|
+
return next;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildBalances(
|
|
176
|
+
balances: Map<string, BalanceAccumulator>,
|
|
177
|
+
receivedAt: number,
|
|
178
|
+
): RawBalanceUpdate[] {
|
|
179
|
+
return [...balances.values()].map((balance) => {
|
|
180
|
+
const netAsset = balance.supplied.minus(balance.borrowed);
|
|
181
|
+
return {
|
|
182
|
+
asset: balance.asset,
|
|
183
|
+
free: "0",
|
|
184
|
+
used: "0",
|
|
185
|
+
total: netAsset.toString(10),
|
|
186
|
+
receivedAt,
|
|
187
|
+
lending: {
|
|
188
|
+
supplied: balance.supplied.toString(10),
|
|
189
|
+
borrowed: balance.borrowed.toString(10),
|
|
190
|
+
interest: "0",
|
|
191
|
+
netAsset: netAsset.toString(10),
|
|
192
|
+
supplyAPY: balance.supplyAPY?.toString(10),
|
|
193
|
+
borrowAPY: balance.borrowAPY?.toString(10),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildRisk(input: {
|
|
200
|
+
totalCollateralUsd: BigNumber;
|
|
201
|
+
totalDebtUsd: BigNumber;
|
|
202
|
+
weightedLiquidationValueUsd: BigNumber;
|
|
203
|
+
receivedAt: number;
|
|
204
|
+
}): RawRiskUpdate | undefined {
|
|
205
|
+
const { totalCollateralUsd, totalDebtUsd, weightedLiquidationValueUsd } =
|
|
206
|
+
input;
|
|
207
|
+
if (totalCollateralUsd.isZero() && totalDebtUsd.isZero()) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const riskRatio = weightedLiquidationValueUsd.isZero()
|
|
212
|
+
? undefined
|
|
213
|
+
: totalDebtUsd.dividedBy(weightedLiquidationValueUsd).toString(10);
|
|
214
|
+
const ltv = totalCollateralUsd.isZero()
|
|
215
|
+
? undefined
|
|
216
|
+
: totalDebtUsd.dividedBy(totalCollateralUsd).toString(10);
|
|
217
|
+
const liquidationThreshold = totalCollateralUsd.isZero()
|
|
218
|
+
? undefined
|
|
219
|
+
: weightedLiquidationValueUsd.dividedBy(totalCollateralUsd).toString(10);
|
|
220
|
+
const healthFactor = riskRatio
|
|
221
|
+
? new BigNumber(1).dividedBy(riskRatio).toString(10)
|
|
222
|
+
: undefined;
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
equity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
|
|
226
|
+
riskRatio,
|
|
227
|
+
receivedAt: input.receivedAt,
|
|
228
|
+
lending: {
|
|
229
|
+
healthFactor,
|
|
230
|
+
ltv,
|
|
231
|
+
liquidationThreshold,
|
|
232
|
+
totalCollateralUSD: totalCollateralUsd.toString(10),
|
|
233
|
+
totalDebtUSD: totalDebtUsd.toString(10),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function readJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
239
|
+
const response = await fetch(url, init);
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`Juplend HTTP ${response.status}: ${response.statusText}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return (await response.json()) as T;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function loadVaults(now: number): Promise<Map<string, JuplendVault>> {
|
|
248
|
+
if (vaultCache && now - vaultCache.loadedAt < VAULT_CACHE_TTL_MS) {
|
|
249
|
+
return vaultCache.vaults;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!vaultCachePromise) {
|
|
253
|
+
vaultCachePromise = readJson<JuplendVaultResponse | JuplendVault[]>(
|
|
254
|
+
VAULTS_URL,
|
|
255
|
+
)
|
|
256
|
+
.then((response) => {
|
|
257
|
+
const rawVaults = Array.isArray(response) ? response : response.data;
|
|
258
|
+
const vaults = new Map<string, JuplendVault>();
|
|
259
|
+
for (const vault of rawVaults ?? []) {
|
|
260
|
+
const id = getVaultId(vault);
|
|
261
|
+
if (id) {
|
|
262
|
+
vaults.set(id, vault);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
vaultCache = { loadedAt: now, vaults };
|
|
266
|
+
return vaults;
|
|
267
|
+
})
|
|
268
|
+
.finally(() => {
|
|
269
|
+
vaultCachePromise = undefined;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
return await vaultCachePromise;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (vaultCache) {
|
|
277
|
+
return vaultCache.vaults;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function loadPortfolio(
|
|
284
|
+
walletAddress: string,
|
|
285
|
+
apiKey: string,
|
|
286
|
+
): Promise<JuplendPortfolioResponse> {
|
|
287
|
+
return readJson<JuplendPortfolioResponse>(
|
|
288
|
+
`${PORTFOLIO_BASE_URL}/positions/${walletAddress}?platforms=jupiter-exchange`,
|
|
289
|
+
{
|
|
290
|
+
headers: {
|
|
291
|
+
"X-API-KEY": apiKey,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function mapAccount(
|
|
298
|
+
portfolio: JuplendPortfolioResponse,
|
|
299
|
+
vaults: Map<string, JuplendVault>,
|
|
300
|
+
receivedAt: number,
|
|
301
|
+
positionId?: string,
|
|
302
|
+
): JuplendMappedAccount {
|
|
303
|
+
const balances = new Map<string, BalanceAccumulator>();
|
|
304
|
+
let totalCollateralUsd = new BigNumber(0);
|
|
305
|
+
let totalDebtUsd = new BigNumber(0);
|
|
306
|
+
let weightedLiquidationValueUsd = new BigNumber(0);
|
|
307
|
+
|
|
308
|
+
for (const element of portfolio.elements ?? []) {
|
|
309
|
+
const positionLink = extractPositionLink(element.data?.link);
|
|
310
|
+
if (!positionLink) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (positionId && positionLink.positionId !== positionId) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const vault = vaults.get(positionLink.vaultId);
|
|
319
|
+
if (!vault) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const suppliedValue = toBigNumber(element.data?.suppliedValue);
|
|
324
|
+
const borrowedValue = toBigNumber(element.data?.borrowedValue);
|
|
325
|
+
const liquidationThreshold = normalizeThreshold(
|
|
326
|
+
vault.liquidationThreshold ?? vault.loanToValue,
|
|
327
|
+
);
|
|
328
|
+
totalCollateralUsd = totalCollateralUsd.plus(suppliedValue);
|
|
329
|
+
totalDebtUsd = totalDebtUsd.plus(borrowedValue);
|
|
330
|
+
weightedLiquidationValueUsd = weightedLiquidationValueUsd.plus(
|
|
331
|
+
suppliedValue.multipliedBy(liquidationThreshold),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const supplyAsset = tokenAsset(vault.supplyToken);
|
|
335
|
+
const supplyPrice = tokenPrice(vault.supplyToken);
|
|
336
|
+
if (supplyAsset && supplyPrice) {
|
|
337
|
+
const accumulator = setAccumulator(balances, supplyAsset);
|
|
338
|
+
accumulator.supplied = accumulator.supplied.plus(
|
|
339
|
+
suppliedValue.dividedBy(supplyPrice),
|
|
340
|
+
);
|
|
341
|
+
accumulator.supplyAPY = toBigNumber(vault.supplyRate);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const borrowAsset = tokenAsset(vault.borrowToken);
|
|
345
|
+
const borrowPrice = tokenPrice(vault.borrowToken);
|
|
346
|
+
if (borrowAsset && borrowPrice) {
|
|
347
|
+
const accumulator = setAccumulator(balances, borrowAsset);
|
|
348
|
+
accumulator.borrowed = accumulator.borrowed.plus(
|
|
349
|
+
borrowedValue.dividedBy(borrowPrice),
|
|
350
|
+
);
|
|
351
|
+
accumulator.borrowAPY = toBigNumber(vault.borrowRate);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
balances: buildBalances(balances, receivedAt),
|
|
357
|
+
risk: buildRisk({
|
|
358
|
+
totalCollateralUsd,
|
|
359
|
+
totalDebtUsd,
|
|
360
|
+
weightedLiquidationValueUsd,
|
|
361
|
+
receivedAt,
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
367
|
+
readonly venue = "juplend" as const;
|
|
368
|
+
readonly readOnly = true;
|
|
369
|
+
readonly notes = [
|
|
370
|
+
"Juplend support is limited to read-only lending account views.",
|
|
371
|
+
"Order and market data managers are not supported for this venue.",
|
|
372
|
+
];
|
|
373
|
+
readonly accountCapabilities: VenueAccountCapabilities = {
|
|
374
|
+
register: "supported",
|
|
375
|
+
snapshot: "supported",
|
|
376
|
+
updates: "polling",
|
|
377
|
+
balances: "supported",
|
|
378
|
+
positions: "unsupported",
|
|
379
|
+
risk: "supported",
|
|
380
|
+
lending: "supported",
|
|
381
|
+
credentialsRequired: true,
|
|
382
|
+
};
|
|
383
|
+
readonly orderCapabilities: VenueOrderCapabilities = {
|
|
384
|
+
supported: false,
|
|
385
|
+
openOrders: "unsupported",
|
|
386
|
+
updates: "unsupported",
|
|
387
|
+
create: "unsupported",
|
|
388
|
+
cancel: "unsupported",
|
|
389
|
+
cancelAll: "unsupported",
|
|
390
|
+
orderTypes: [],
|
|
391
|
+
timeInForce: [],
|
|
392
|
+
postOnly: false,
|
|
393
|
+
reduceOnly: false,
|
|
394
|
+
positionSide: "unsupported",
|
|
395
|
+
clientOrderId: false,
|
|
396
|
+
reason: "read_only",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
async bootstrapAccount(
|
|
400
|
+
credentials: AccountCredentials,
|
|
401
|
+
accountOptions?: Record<string, unknown>,
|
|
402
|
+
): Promise<RawAccountBootstrap> {
|
|
403
|
+
const receivedAt = Date.now();
|
|
404
|
+
const apiKey = requireApiKey(credentials);
|
|
405
|
+
const juplendOptions = getJuplendAccountOptions(accountOptions);
|
|
406
|
+
const [portfolio, vaults] = await Promise.all([
|
|
407
|
+
loadPortfolio(juplendOptions.walletAddress, apiKey),
|
|
408
|
+
loadVaults(receivedAt),
|
|
409
|
+
]);
|
|
410
|
+
const mapped = mapAccount(
|
|
411
|
+
portfolio,
|
|
412
|
+
vaults,
|
|
413
|
+
receivedAt,
|
|
414
|
+
juplendOptions.positionId,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
balances: mapped.balances,
|
|
419
|
+
positions: [],
|
|
420
|
+
risk: mapped.risk,
|
|
421
|
+
receivedAt,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
bootstrapOpenOrders(): Promise<RawOrderUpdate[]> {
|
|
426
|
+
return Promise.resolve([]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
createOrder(
|
|
430
|
+
_credentials: AccountCredentials,
|
|
431
|
+
_request: CreateOrderRequest,
|
|
432
|
+
): Promise<RawOrderUpdate> {
|
|
433
|
+
throw new AcexError(
|
|
434
|
+
"VENUE_NOT_SUPPORTED",
|
|
435
|
+
"Juplend is read-only and does not support createOrder",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
cancelOrder(
|
|
440
|
+
_credentials: AccountCredentials,
|
|
441
|
+
_request: CancelOrderRequest,
|
|
442
|
+
): Promise<RawOrderUpdate> {
|
|
443
|
+
throw new AcexError(
|
|
444
|
+
"VENUE_NOT_SUPPORTED",
|
|
445
|
+
"Juplend is read-only and does not support cancelOrder",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
cancelAllOrders(
|
|
450
|
+
_credentials: AccountCredentials,
|
|
451
|
+
_request: CancelAllOrdersRequest,
|
|
452
|
+
): Promise<RawOrderUpdate[]> {
|
|
453
|
+
throw new AcexError(
|
|
454
|
+
"VENUE_NOT_SUPPORTED",
|
|
455
|
+
"Juplend is read-only and does not support cancelAllOrders",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
createPrivateStream(
|
|
460
|
+
credentials: AccountCredentials,
|
|
461
|
+
callbacks: PrivateStreamCallbacks,
|
|
462
|
+
options: PrivateStreamOptions,
|
|
463
|
+
accountOptions?: Record<string, unknown>,
|
|
464
|
+
): StreamHandle {
|
|
465
|
+
let closed = false;
|
|
466
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
467
|
+
const pollIntervalMs =
|
|
468
|
+
options.juplendPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
469
|
+
|
|
470
|
+
const poll = async (): Promise<void> => {
|
|
471
|
+
try {
|
|
472
|
+
const bootstrap = await this.bootstrapAccount(
|
|
473
|
+
credentials,
|
|
474
|
+
accountOptions,
|
|
475
|
+
);
|
|
476
|
+
if (closed) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
callbacks.onAccountSnapshot(bootstrap);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
callbacks.onError(
|
|
482
|
+
error instanceof Error ? error : new Error("Juplend polling failed"),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const scheduleNextPoll = (): void => {
|
|
488
|
+
if (closed) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
timer = setTimeout(() => {
|
|
493
|
+
void poll().finally(scheduleNextPoll);
|
|
494
|
+
}, pollIntervalMs);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const ready = Promise.resolve().then(() => {
|
|
498
|
+
scheduleNextPoll();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
ready,
|
|
503
|
+
close() {
|
|
504
|
+
closed = true;
|
|
505
|
+
if (timer) {
|
|
506
|
+
clearTimeout(timer);
|
|
507
|
+
timer = undefined;
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function resetJuplendVaultCacheForTests(): void {
|
|
515
|
+
vaultCache = undefined;
|
|
516
|
+
vaultCachePromise = undefined;
|
|
517
|
+
}
|