@imbingox/acex 0.3.0-beta.0 → 0.3.0-beta.2
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 +73 -23
- package/package.json +9 -3
- package/src/adapters/binance/adapter.ts +1 -1
- package/src/adapters/binance/market-catalog.ts +2 -2
- package/src/adapters/binance/private-adapter.ts +14 -4
- package/src/adapters/juplend/private-adapter.ts +483 -0
- package/src/adapters/types.ts +27 -4
- package/src/client/context.ts +16 -11
- package/src/client/private-subscription-coordinator.ts +101 -47
- package/src/client/runtime.ts +43 -20
- 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 +129 -44
- package/src/managers/order-manager.ts +49 -56
- package/src/types/account.ts +30 -10
- package/src/types/client.ts +2 -2
- package/src/types/market.ts +40 -16
- package/src/types/order.ts +8 -7
- package/src/types/shared.ts +43 -7
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { AcexError } from "../../errors.ts";
|
|
3
|
+
import type { AccountCredentials } from "../../types/index.ts";
|
|
4
|
+
import type {
|
|
5
|
+
CancelAllOrdersRequest,
|
|
6
|
+
CancelOrderRequest,
|
|
7
|
+
CreateOrderRequest,
|
|
8
|
+
PrivateStreamCallbacks,
|
|
9
|
+
PrivateStreamOptions,
|
|
10
|
+
PrivateUserDataAdapter,
|
|
11
|
+
RawAccountBootstrap,
|
|
12
|
+
RawBalanceUpdate,
|
|
13
|
+
RawOrderUpdate,
|
|
14
|
+
RawRiskUpdate,
|
|
15
|
+
StreamHandle,
|
|
16
|
+
} from "../types.ts";
|
|
17
|
+
|
|
18
|
+
interface JuplendPortfolioResponse {
|
|
19
|
+
elements?: JuplendPortfolioElement[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface JuplendPortfolioElement {
|
|
23
|
+
data?: {
|
|
24
|
+
link?: string;
|
|
25
|
+
suppliedValue?: number | string;
|
|
26
|
+
borrowedValue?: number | string;
|
|
27
|
+
value?: number | string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface JuplendVaultResponse {
|
|
32
|
+
data?: JuplendVault[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface JuplendVault {
|
|
36
|
+
id?: number | string;
|
|
37
|
+
vaultId?: number | string;
|
|
38
|
+
supplyToken?: JuplendToken;
|
|
39
|
+
borrowToken?: JuplendToken;
|
|
40
|
+
liquidationThreshold?: number | string;
|
|
41
|
+
loanToValue?: number | string;
|
|
42
|
+
supplyRate?: number | string;
|
|
43
|
+
borrowRate?: number | string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface JuplendToken {
|
|
47
|
+
symbol?: string;
|
|
48
|
+
asset?: string;
|
|
49
|
+
oraclePrice?: number | string;
|
|
50
|
+
price?: number | string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface JuplendMappedAccount {
|
|
54
|
+
balances: RawBalanceUpdate[];
|
|
55
|
+
risk?: RawRiskUpdate;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface BalanceAccumulator {
|
|
59
|
+
asset: string;
|
|
60
|
+
supplied: BigNumber;
|
|
61
|
+
borrowed: BigNumber;
|
|
62
|
+
supplyAPY?: BigNumber;
|
|
63
|
+
borrowAPY?: BigNumber;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface JuplendAccountOptions {
|
|
67
|
+
walletAddress: string;
|
|
68
|
+
positionId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const PORTFOLIO_BASE_URL = "https://api.jup.ag/portfolio/v1";
|
|
72
|
+
const VAULTS_URL = "https://lite-api.jup.ag/lend/v1/borrow/vaults";
|
|
73
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
74
|
+
const VAULT_CACHE_TTL_MS = 60 * 60 * 1_000;
|
|
75
|
+
const LINK_PATTERN = /\/borrow\/([^/]+)\/nfts\/([^/?#]+)/;
|
|
76
|
+
|
|
77
|
+
let vaultCache:
|
|
78
|
+
| {
|
|
79
|
+
loadedAt: number;
|
|
80
|
+
vaults: Map<string, JuplendVault>;
|
|
81
|
+
}
|
|
82
|
+
| undefined;
|
|
83
|
+
let vaultCachePromise: Promise<Map<string, JuplendVault>> | undefined;
|
|
84
|
+
|
|
85
|
+
function requireApiKey(credentials: AccountCredentials): string {
|
|
86
|
+
if (!credentials.apiKey) {
|
|
87
|
+
throw new Error("credentials.apiKey required");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return credentials.apiKey;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getJuplendAccountOptions(
|
|
94
|
+
accountOptions?: Record<string, unknown>,
|
|
95
|
+
): JuplendAccountOptions {
|
|
96
|
+
const walletAddress = accountOptions?.walletAddress;
|
|
97
|
+
if (typeof walletAddress !== "string" || !walletAddress) {
|
|
98
|
+
throw new Error("options.walletAddress required");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const positionId = accountOptions.positionId;
|
|
102
|
+
if (positionId !== undefined && typeof positionId !== "string") {
|
|
103
|
+
throw new Error("options.positionId must be a string");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
walletAddress,
|
|
108
|
+
positionId: positionId || undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toBigNumber(value: number | string | undefined): BigNumber {
|
|
113
|
+
return value === undefined ? new BigNumber(0) : new BigNumber(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeThreshold(value: number | string | undefined): BigNumber {
|
|
117
|
+
const threshold = toBigNumber(value);
|
|
118
|
+
return threshold.gt(1) ? threshold.dividedBy(1000) : threshold;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function tokenAsset(token: JuplendToken | undefined): string | undefined {
|
|
122
|
+
return token?.symbol ?? token?.asset;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenPrice(token: JuplendToken | undefined): BigNumber | undefined {
|
|
126
|
+
const price = toBigNumber(token?.oraclePrice ?? token?.price);
|
|
127
|
+
return price.gt(0) ? price : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractPositionLink(
|
|
131
|
+
link: string | undefined,
|
|
132
|
+
): { vaultId: string; positionId: string } | undefined {
|
|
133
|
+
if (!link) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const match = LINK_PATTERN.exec(link);
|
|
138
|
+
if (!match?.[1] || !match[2]) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
vaultId: match[1],
|
|
144
|
+
positionId: match[2],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getVaultId(vault: JuplendVault): string | undefined {
|
|
149
|
+
const id = vault.id ?? vault.vaultId;
|
|
150
|
+
return id === undefined ? undefined : `${id}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setAccumulator(
|
|
154
|
+
map: Map<string, BalanceAccumulator>,
|
|
155
|
+
asset: string,
|
|
156
|
+
): BalanceAccumulator {
|
|
157
|
+
const existing = map.get(asset);
|
|
158
|
+
if (existing) {
|
|
159
|
+
return existing;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const next: BalanceAccumulator = {
|
|
163
|
+
asset,
|
|
164
|
+
supplied: new BigNumber(0),
|
|
165
|
+
borrowed: new BigNumber(0),
|
|
166
|
+
};
|
|
167
|
+
map.set(asset, next);
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildBalances(
|
|
172
|
+
balances: Map<string, BalanceAccumulator>,
|
|
173
|
+
receivedAt: number,
|
|
174
|
+
): RawBalanceUpdate[] {
|
|
175
|
+
return [...balances.values()].map((balance) => {
|
|
176
|
+
const netAsset = balance.supplied.minus(balance.borrowed);
|
|
177
|
+
return {
|
|
178
|
+
asset: balance.asset,
|
|
179
|
+
free: "0",
|
|
180
|
+
used: "0",
|
|
181
|
+
total: netAsset.toString(10),
|
|
182
|
+
receivedAt,
|
|
183
|
+
lending: {
|
|
184
|
+
supplied: balance.supplied.toString(10),
|
|
185
|
+
borrowed: balance.borrowed.toString(10),
|
|
186
|
+
interest: "0",
|
|
187
|
+
netAsset: netAsset.toString(10),
|
|
188
|
+
supplyAPY: balance.supplyAPY?.toString(10),
|
|
189
|
+
borrowAPY: balance.borrowAPY?.toString(10),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildRisk(input: {
|
|
196
|
+
totalCollateralUsd: BigNumber;
|
|
197
|
+
totalDebtUsd: BigNumber;
|
|
198
|
+
weightedLiquidationValueUsd: BigNumber;
|
|
199
|
+
receivedAt: number;
|
|
200
|
+
}): RawRiskUpdate | undefined {
|
|
201
|
+
const { totalCollateralUsd, totalDebtUsd, weightedLiquidationValueUsd } =
|
|
202
|
+
input;
|
|
203
|
+
if (totalCollateralUsd.isZero() && totalDebtUsd.isZero()) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const riskRatio = weightedLiquidationValueUsd.isZero()
|
|
208
|
+
? undefined
|
|
209
|
+
: totalDebtUsd.dividedBy(weightedLiquidationValueUsd).toString(10);
|
|
210
|
+
const ltv = totalCollateralUsd.isZero()
|
|
211
|
+
? undefined
|
|
212
|
+
: totalDebtUsd.dividedBy(totalCollateralUsd).toString(10);
|
|
213
|
+
const liquidationThreshold = totalCollateralUsd.isZero()
|
|
214
|
+
? undefined
|
|
215
|
+
: weightedLiquidationValueUsd.dividedBy(totalCollateralUsd).toString(10);
|
|
216
|
+
const healthFactor = riskRatio
|
|
217
|
+
? new BigNumber(1).dividedBy(riskRatio).toString(10)
|
|
218
|
+
: undefined;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
equity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
|
|
222
|
+
riskRatio,
|
|
223
|
+
receivedAt: input.receivedAt,
|
|
224
|
+
lending: {
|
|
225
|
+
healthFactor,
|
|
226
|
+
ltv,
|
|
227
|
+
liquidationThreshold,
|
|
228
|
+
totalCollateralUSD: totalCollateralUsd.toString(10),
|
|
229
|
+
totalDebtUSD: totalDebtUsd.toString(10),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function readJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
235
|
+
const response = await fetch(url, init);
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(`Juplend HTTP ${response.status}: ${response.statusText}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (await response.json()) as T;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function loadVaults(now: number): Promise<Map<string, JuplendVault>> {
|
|
244
|
+
if (vaultCache && now - vaultCache.loadedAt < VAULT_CACHE_TTL_MS) {
|
|
245
|
+
return vaultCache.vaults;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!vaultCachePromise) {
|
|
249
|
+
vaultCachePromise = readJson<JuplendVaultResponse | JuplendVault[]>(
|
|
250
|
+
VAULTS_URL,
|
|
251
|
+
)
|
|
252
|
+
.then((response) => {
|
|
253
|
+
const rawVaults = Array.isArray(response) ? response : response.data;
|
|
254
|
+
const vaults = new Map<string, JuplendVault>();
|
|
255
|
+
for (const vault of rawVaults ?? []) {
|
|
256
|
+
const id = getVaultId(vault);
|
|
257
|
+
if (id) {
|
|
258
|
+
vaults.set(id, vault);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
vaultCache = { loadedAt: now, vaults };
|
|
262
|
+
return vaults;
|
|
263
|
+
})
|
|
264
|
+
.finally(() => {
|
|
265
|
+
vaultCachePromise = undefined;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
return await vaultCachePromise;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (vaultCache) {
|
|
273
|
+
return vaultCache.vaults;
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function loadPortfolio(
|
|
280
|
+
walletAddress: string,
|
|
281
|
+
apiKey: string,
|
|
282
|
+
): Promise<JuplendPortfolioResponse> {
|
|
283
|
+
return readJson<JuplendPortfolioResponse>(
|
|
284
|
+
`${PORTFOLIO_BASE_URL}/positions/${walletAddress}?platforms=jupiter-exchange`,
|
|
285
|
+
{
|
|
286
|
+
headers: {
|
|
287
|
+
"X-API-KEY": apiKey,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function mapAccount(
|
|
294
|
+
portfolio: JuplendPortfolioResponse,
|
|
295
|
+
vaults: Map<string, JuplendVault>,
|
|
296
|
+
receivedAt: number,
|
|
297
|
+
positionId?: string,
|
|
298
|
+
): JuplendMappedAccount {
|
|
299
|
+
const balances = new Map<string, BalanceAccumulator>();
|
|
300
|
+
let totalCollateralUsd = new BigNumber(0);
|
|
301
|
+
let totalDebtUsd = new BigNumber(0);
|
|
302
|
+
let weightedLiquidationValueUsd = new BigNumber(0);
|
|
303
|
+
|
|
304
|
+
for (const element of portfolio.elements ?? []) {
|
|
305
|
+
const positionLink = extractPositionLink(element.data?.link);
|
|
306
|
+
if (!positionLink) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (positionId && positionLink.positionId !== positionId) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const vault = vaults.get(positionLink.vaultId);
|
|
315
|
+
if (!vault) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const suppliedValue = toBigNumber(element.data?.suppliedValue);
|
|
320
|
+
const borrowedValue = toBigNumber(element.data?.borrowedValue);
|
|
321
|
+
const liquidationThreshold = normalizeThreshold(
|
|
322
|
+
vault.liquidationThreshold ?? vault.loanToValue,
|
|
323
|
+
);
|
|
324
|
+
totalCollateralUsd = totalCollateralUsd.plus(suppliedValue);
|
|
325
|
+
totalDebtUsd = totalDebtUsd.plus(borrowedValue);
|
|
326
|
+
weightedLiquidationValueUsd = weightedLiquidationValueUsd.plus(
|
|
327
|
+
suppliedValue.multipliedBy(liquidationThreshold),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const supplyAsset = tokenAsset(vault.supplyToken);
|
|
331
|
+
const supplyPrice = tokenPrice(vault.supplyToken);
|
|
332
|
+
if (supplyAsset && supplyPrice) {
|
|
333
|
+
const accumulator = setAccumulator(balances, supplyAsset);
|
|
334
|
+
accumulator.supplied = accumulator.supplied.plus(
|
|
335
|
+
suppliedValue.dividedBy(supplyPrice),
|
|
336
|
+
);
|
|
337
|
+
accumulator.supplyAPY = toBigNumber(vault.supplyRate);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const borrowAsset = tokenAsset(vault.borrowToken);
|
|
341
|
+
const borrowPrice = tokenPrice(vault.borrowToken);
|
|
342
|
+
if (borrowAsset && borrowPrice) {
|
|
343
|
+
const accumulator = setAccumulator(balances, borrowAsset);
|
|
344
|
+
accumulator.borrowed = accumulator.borrowed.plus(
|
|
345
|
+
borrowedValue.dividedBy(borrowPrice),
|
|
346
|
+
);
|
|
347
|
+
accumulator.borrowAPY = toBigNumber(vault.borrowRate);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
balances: buildBalances(balances, receivedAt),
|
|
353
|
+
risk: buildRisk({
|
|
354
|
+
totalCollateralUsd,
|
|
355
|
+
totalDebtUsd,
|
|
356
|
+
weightedLiquidationValueUsd,
|
|
357
|
+
receivedAt,
|
|
358
|
+
}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
363
|
+
readonly venue = "juplend" as const;
|
|
364
|
+
|
|
365
|
+
async bootstrapAccount(
|
|
366
|
+
credentials: AccountCredentials,
|
|
367
|
+
accountOptions?: Record<string, unknown>,
|
|
368
|
+
): Promise<RawAccountBootstrap> {
|
|
369
|
+
const receivedAt = Date.now();
|
|
370
|
+
const apiKey = requireApiKey(credentials);
|
|
371
|
+
const juplendOptions = getJuplendAccountOptions(accountOptions);
|
|
372
|
+
const [portfolio, vaults] = await Promise.all([
|
|
373
|
+
loadPortfolio(juplendOptions.walletAddress, apiKey),
|
|
374
|
+
loadVaults(receivedAt),
|
|
375
|
+
]);
|
|
376
|
+
const mapped = mapAccount(
|
|
377
|
+
portfolio,
|
|
378
|
+
vaults,
|
|
379
|
+
receivedAt,
|
|
380
|
+
juplendOptions.positionId,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
balances: mapped.balances,
|
|
385
|
+
positions: [],
|
|
386
|
+
risk: mapped.risk,
|
|
387
|
+
receivedAt,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
bootstrapOpenOrders(): Promise<RawOrderUpdate[]> {
|
|
392
|
+
return Promise.resolve([]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
createOrder(
|
|
396
|
+
_credentials: AccountCredentials,
|
|
397
|
+
_request: CreateOrderRequest,
|
|
398
|
+
): Promise<RawOrderUpdate> {
|
|
399
|
+
throw new AcexError(
|
|
400
|
+
"VENUE_NOT_SUPPORTED",
|
|
401
|
+
"Juplend is read-only and does not support createOrder",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
cancelOrder(
|
|
406
|
+
_credentials: AccountCredentials,
|
|
407
|
+
_request: CancelOrderRequest,
|
|
408
|
+
): Promise<RawOrderUpdate> {
|
|
409
|
+
throw new AcexError(
|
|
410
|
+
"VENUE_NOT_SUPPORTED",
|
|
411
|
+
"Juplend is read-only and does not support cancelOrder",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cancelAllOrders(
|
|
416
|
+
_credentials: AccountCredentials,
|
|
417
|
+
_request: CancelAllOrdersRequest,
|
|
418
|
+
): Promise<RawOrderUpdate[]> {
|
|
419
|
+
throw new AcexError(
|
|
420
|
+
"VENUE_NOT_SUPPORTED",
|
|
421
|
+
"Juplend is read-only and does not support cancelAllOrders",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
createPrivateStream(
|
|
426
|
+
credentials: AccountCredentials,
|
|
427
|
+
callbacks: PrivateStreamCallbacks,
|
|
428
|
+
options: PrivateStreamOptions,
|
|
429
|
+
accountOptions?: Record<string, unknown>,
|
|
430
|
+
): StreamHandle {
|
|
431
|
+
let closed = false;
|
|
432
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
433
|
+
const pollIntervalMs =
|
|
434
|
+
options.juplendPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
435
|
+
|
|
436
|
+
const poll = async (): Promise<void> => {
|
|
437
|
+
try {
|
|
438
|
+
const bootstrap = await this.bootstrapAccount(
|
|
439
|
+
credentials,
|
|
440
|
+
accountOptions,
|
|
441
|
+
);
|
|
442
|
+
if (closed) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
callbacks.onAccountSnapshot(bootstrap);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
callbacks.onError(
|
|
448
|
+
error instanceof Error ? error : new Error("Juplend polling failed"),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const scheduleNextPoll = (): void => {
|
|
454
|
+
if (closed) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
timer = setTimeout(() => {
|
|
459
|
+
void poll().finally(scheduleNextPoll);
|
|
460
|
+
}, pollIntervalMs);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const ready = Promise.resolve().then(() => {
|
|
464
|
+
scheduleNextPoll();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
ready,
|
|
469
|
+
close() {
|
|
470
|
+
closed = true;
|
|
471
|
+
if (timer) {
|
|
472
|
+
clearTimeout(timer);
|
|
473
|
+
timer = undefined;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function resetJuplendVaultCacheForTests(): void {
|
|
481
|
+
vaultCache = undefined;
|
|
482
|
+
vaultCachePromise = undefined;
|
|
483
|
+
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AccountCredentials,
|
|
3
3
|
CreateOrderType,
|
|
4
|
-
Exchange,
|
|
5
4
|
MarketDefinition,
|
|
6
5
|
OrderSide,
|
|
7
6
|
OrderStatus,
|
|
8
7
|
PositionSide,
|
|
8
|
+
Venue,
|
|
9
9
|
} from "../types/index.ts";
|
|
10
10
|
|
|
11
11
|
export interface StreamHandle {
|
|
@@ -68,7 +68,7 @@ export interface FundingRateStreamOptions {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export interface MarketAdapter {
|
|
71
|
-
readonly
|
|
71
|
+
readonly venue: Venue;
|
|
72
72
|
loadMarkets(): Promise<MarketDefinition[]>;
|
|
73
73
|
createL1BookStream(
|
|
74
74
|
market: MarketDefinition,
|
|
@@ -89,6 +89,16 @@ export interface RawBalanceUpdate {
|
|
|
89
89
|
total?: string;
|
|
90
90
|
exchangeTs?: number;
|
|
91
91
|
receivedAt: number;
|
|
92
|
+
lending?: RawLendingBalanceUpdate;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RawLendingBalanceUpdate {
|
|
96
|
+
supplied: string;
|
|
97
|
+
borrowed: string;
|
|
98
|
+
interest: string;
|
|
99
|
+
netAsset: string;
|
|
100
|
+
supplyAPY?: string;
|
|
101
|
+
borrowAPY?: string;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
export interface RawPositionUpdate {
|
|
@@ -106,11 +116,21 @@ export interface RawPositionUpdate {
|
|
|
106
116
|
|
|
107
117
|
export interface RawRiskUpdate {
|
|
108
118
|
equity?: string;
|
|
109
|
-
|
|
119
|
+
riskRatio?: string;
|
|
110
120
|
initialMargin?: string;
|
|
111
121
|
maintenanceMargin?: string;
|
|
112
122
|
exchangeTs?: number;
|
|
113
123
|
receivedAt: number;
|
|
124
|
+
lending?: RawLendingRiskUpdate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RawLendingRiskUpdate {
|
|
128
|
+
marginLevel?: string;
|
|
129
|
+
healthFactor?: string;
|
|
130
|
+
ltv?: string;
|
|
131
|
+
liquidationThreshold?: string;
|
|
132
|
+
totalCollateralUSD?: string;
|
|
133
|
+
totalDebtUSD?: string;
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
export interface RawAccountBootstrap {
|
|
@@ -154,6 +174,7 @@ export interface CreateOrderRequest {
|
|
|
154
174
|
type: CreateOrderType;
|
|
155
175
|
amount: string;
|
|
156
176
|
price?: string;
|
|
177
|
+
postOnly?: boolean;
|
|
157
178
|
clientOrderId?: string;
|
|
158
179
|
reduceOnly?: boolean;
|
|
159
180
|
positionSide?: PositionSide;
|
|
@@ -170,6 +191,7 @@ export interface CancelAllOrdersRequest {
|
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
export interface PrivateStreamCallbacks {
|
|
194
|
+
onAccountSnapshot(snapshot: RawAccountBootstrap): void;
|
|
173
195
|
onAccountUpdate(update: RawAccountUpdate): void;
|
|
174
196
|
onOrderUpdate(update: RawOrderUpdate): void;
|
|
175
197
|
onDisconnected(): void;
|
|
@@ -182,11 +204,12 @@ export interface PrivateStreamOptions {
|
|
|
182
204
|
reconnectDelayMs: number;
|
|
183
205
|
reconnectMaxDelayMs: number;
|
|
184
206
|
listenKeyKeepAliveMs: number;
|
|
207
|
+
juplendPollIntervalMs?: number;
|
|
185
208
|
now?: () => number;
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
export interface PrivateUserDataAdapter {
|
|
189
|
-
readonly
|
|
212
|
+
readonly venue: Venue;
|
|
190
213
|
bootstrapAccount(
|
|
191
214
|
credentials: AccountCredentials,
|
|
192
215
|
accountOptions?: Record<string, unknown>,
|
package/src/client/context.ts
CHANGED
|
@@ -9,15 +9,15 @@ import type {
|
|
|
9
9
|
CancelAllOrdersInput,
|
|
10
10
|
CancelOrderInput,
|
|
11
11
|
CreateOrderInput,
|
|
12
|
-
Exchange,
|
|
13
12
|
HealthEvent,
|
|
14
13
|
PrivateRuntimeReason,
|
|
15
14
|
PrivateRuntimeStatus,
|
|
15
|
+
Venue,
|
|
16
16
|
} from "../types/index.ts";
|
|
17
17
|
|
|
18
18
|
export interface RegisteredAccountRecord {
|
|
19
19
|
accountId: string;
|
|
20
|
-
|
|
20
|
+
venue: Venue;
|
|
21
21
|
credentials?: AccountCredentials;
|
|
22
22
|
options?: Record<string, unknown>;
|
|
23
23
|
}
|
|
@@ -49,7 +49,7 @@ export interface ManagerLifecycle {
|
|
|
49
49
|
|
|
50
50
|
export interface AccountAwareManager {
|
|
51
51
|
onAccountRemoved(accountId: string, now: number): void;
|
|
52
|
-
onCredentialsUpdated(accountId: string,
|
|
52
|
+
onCredentialsUpdated(accountId: string, venue: Venue): void;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface HealthReporter<T> {
|
|
@@ -65,46 +65,51 @@ export interface PrivateSubscriptionState {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export interface PrivateAccountDataConsumer {
|
|
68
|
-
onPrivateAccountPending(accountId: string,
|
|
68
|
+
onPrivateAccountPending(accountId: string, venue: Venue): void;
|
|
69
69
|
onPrivateAccountBootstrap(
|
|
70
70
|
accountId: string,
|
|
71
|
-
|
|
71
|
+
venue: Venue,
|
|
72
72
|
bootstrap: RawAccountBootstrap,
|
|
73
73
|
): void;
|
|
74
74
|
onPrivateAccountUpdate(
|
|
75
75
|
accountId: string,
|
|
76
|
-
|
|
76
|
+
venue: Venue,
|
|
77
77
|
update: RawAccountUpdate,
|
|
78
78
|
): void;
|
|
79
79
|
onPrivateAccountStreamState(
|
|
80
80
|
accountId: string,
|
|
81
|
-
|
|
81
|
+
venue: Venue,
|
|
82
82
|
state: PrivateSubscriptionState,
|
|
83
83
|
): void;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export interface PrivateOrderDataConsumer {
|
|
87
|
-
onPrivateOrderPending(accountId: string,
|
|
87
|
+
onPrivateOrderPending(accountId: string, venue: Venue): void;
|
|
88
88
|
onPrivateOrderBootstrap(
|
|
89
89
|
accountId: string,
|
|
90
|
-
|
|
90
|
+
venue: Venue,
|
|
91
91
|
snapshots: RawOrderUpdate[],
|
|
92
92
|
): void;
|
|
93
93
|
onPrivateOrderUpdate(
|
|
94
94
|
accountId: string,
|
|
95
|
-
|
|
95
|
+
venue: Venue,
|
|
96
96
|
update: RawOrderUpdate,
|
|
97
97
|
): void;
|
|
98
98
|
onPrivateOrderStreamState(
|
|
99
99
|
accountId: string,
|
|
100
|
-
|
|
100
|
+
venue: Venue,
|
|
101
101
|
state: PrivateSubscriptionState,
|
|
102
102
|
): void;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export function hasPrivateCredentials(
|
|
106
106
|
credentials?: AccountCredentials,
|
|
107
|
+
venue?: Venue,
|
|
107
108
|
): boolean {
|
|
109
|
+
if (venue === "juplend") {
|
|
110
|
+
return Boolean(credentials?.apiKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
return Boolean(credentials?.apiKey && credentials.secret);
|
|
109
114
|
}
|
|
110
115
|
|