@pear-protocol/exchanges-sdk 0.0.6 → 0.0.8-test.0
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 +6 -4
- package/dist/account/account-manager.d.ts +6 -4
- package/dist/account/account-manager.js +22 -5
- package/dist/account/exchange-account.d.ts +2 -2
- package/dist/account/exchange-account.js +2 -2
- package/dist/account/exchanges/base.d.ts +3 -3
- package/dist/account/exchanges/binance/mapper.d.ts +1 -1
- package/dist/account/exchanges/binance/mapper.js +2 -12
- package/dist/account/exchanges/binance/orchestrator.d.ts +2 -4
- package/dist/account/exchanges/binance/orchestrator.js +6 -23
- package/dist/account/exchanges/binance/rest.d.ts +2 -1
- package/dist/account/exchanges/binance/rest.js +4 -1
- package/dist/account/exchanges/bybit/mapper.d.ts +1 -1
- package/dist/account/exchanges/bybit/mapper.js +2 -11
- package/dist/account/exchanges/bybit/orchestrator.d.ts +2 -3
- package/dist/account/exchanges/bybit/orchestrator.js +9 -15
- package/dist/account/exchanges/bybit/rest.d.ts +2 -1
- package/dist/account/exchanges/bybit/rest.js +9 -1
- package/dist/account/exchanges/hyperliquid/const.d.ts +3 -0
- package/dist/account/exchanges/hyperliquid/const.js +3 -0
- package/dist/account/exchanges/hyperliquid/mapper.d.ts +9 -0
- package/dist/account/exchanges/hyperliquid/mapper.js +68 -0
- package/dist/account/exchanges/hyperliquid/orchestrator.d.ts +38 -0
- package/dist/account/exchanges/hyperliquid/orchestrator.js +238 -0
- package/dist/account/exchanges/hyperliquid/rest.d.ts +6 -0
- package/dist/account/exchanges/hyperliquid/rest.js +11 -0
- package/dist/account/exchanges/hyperliquid/types.d.ts +20 -0
- package/dist/account/exchanges/hyperliquid/types.js +8 -0
- package/dist/account/exchanges/index.js +3 -0
- package/dist/account/index.d.ts +2 -2
- package/dist/account/types.d.ts +6 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Unified SDK for connecting to cryptocurrency derivative exchanges. Provides real
|
|
|
8
8
|
|----------|---------------|-------------|
|
|
9
9
|
| Binance | `binanceusdm` | USDM Futures |
|
|
10
10
|
| Bybit | `bybit` | Linear Perpetuals |
|
|
11
|
+
| Hyperliquid | `hyperliquid` | Perpetuals |
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
@@ -67,6 +68,7 @@ Credentials are fetched automatically from the backend when you call `connect`.
|
|
|
67
68
|
```typescript
|
|
68
69
|
await exchanges.account.connect(tradeAccountId, 'binanceusdm');
|
|
69
70
|
await exchanges.account.connect(tradeAccountId, 'bybit');
|
|
71
|
+
await exchanges.account.connect(tradeAccountId, 'hyperliquid');
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
Check connection status:
|
|
@@ -91,8 +93,11 @@ const tracker = exchanges.account.trackBalance((balance) => {
|
|
|
91
93
|
// Per-asset breakdown
|
|
92
94
|
for (const asset of balance.assets) {
|
|
93
95
|
asset.asset; // "USDT", "BTC", etc.
|
|
96
|
+
asset.free; // available amount
|
|
97
|
+
asset.used; // amount in use
|
|
98
|
+
asset.total; // total amount
|
|
99
|
+
asset.walletBalance; // wallet balance
|
|
94
100
|
asset.usdValue; // USD equivalent
|
|
95
|
-
asset.isCollateral; // whether the asset is used as collateral
|
|
96
101
|
}
|
|
97
102
|
});
|
|
98
103
|
|
|
@@ -134,9 +139,6 @@ const tracker = exchanges.account.trackAsset('ETH', (info) => {
|
|
|
134
139
|
info.coin; // "ETH"
|
|
135
140
|
info.leverage; // "20"
|
|
136
141
|
info.marginType; // "cross" | "isolated"
|
|
137
|
-
info.availableToTrade; // [min, max] or null
|
|
138
|
-
info.maxTradeSzs; // [min, max] or null
|
|
139
|
-
info.collateralToken; // "USDT" or null
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
const current = tracker.get();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import PearSDK from '@pear-protocol/core-sdk';
|
|
1
2
|
import { Connector } from '@pear-protocol/types';
|
|
2
3
|
import { Credentials } from '../credentials.js';
|
|
3
|
-
import { BalanceCallback, Tracker, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, ExchangeState } from './types.js';
|
|
4
|
-
import '@pear-protocol/core-sdk';
|
|
4
|
+
import { BalanceCallback, Tracker, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, MarginMode, ExchangeState } from './types.js';
|
|
5
5
|
|
|
6
6
|
declare class AccountManager {
|
|
7
7
|
private credentials;
|
|
8
|
+
private sdk;
|
|
8
9
|
private demo;
|
|
9
10
|
private activeAccount;
|
|
10
11
|
private pendingExchange;
|
|
@@ -12,12 +13,12 @@ declare class AccountManager {
|
|
|
12
13
|
private balanceListeners;
|
|
13
14
|
private positionListeners;
|
|
14
15
|
private assetListeners;
|
|
15
|
-
constructor(credentials: Credentials, demo?: boolean);
|
|
16
|
+
constructor(credentials: Credentials, sdk: PearSDK, demo?: boolean);
|
|
16
17
|
connect(tradeAccountId: string, exchange: Connector): Promise<void>;
|
|
17
18
|
trackBalance(cb: BalanceCallback): Tracker<AccountBalance | null>;
|
|
18
19
|
trackPositions(cb: PositionsCallback): Tracker<AccountPosition[]>;
|
|
19
20
|
trackAsset(coin: string, cb: TrackedAssetCallback): Tracker<TrackedAssetInfo | null>;
|
|
20
|
-
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
21
|
+
setLeverage(symbol: string, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
21
22
|
getLeverage(symbol: string): string | null;
|
|
22
23
|
getState(): Readonly<ExchangeState> | null;
|
|
23
24
|
disconnect(): void;
|
|
@@ -28,6 +29,7 @@ declare class AccountManager {
|
|
|
28
29
|
private disconnectWs;
|
|
29
30
|
private maybeDisconnectWs;
|
|
30
31
|
private ensureWsStarted;
|
|
32
|
+
private resolveCredentials;
|
|
31
33
|
private startWs;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -2,6 +2,7 @@ import { ExchangeAccount } from './exchange-account';
|
|
|
2
2
|
|
|
3
3
|
class AccountManager {
|
|
4
4
|
credentials;
|
|
5
|
+
sdk;
|
|
5
6
|
demo;
|
|
6
7
|
activeAccount = null;
|
|
7
8
|
pendingExchange = null;
|
|
@@ -9,12 +10,13 @@ class AccountManager {
|
|
|
9
10
|
balanceListeners = /* @__PURE__ */ new Map();
|
|
10
11
|
positionListeners = /* @__PURE__ */ new Map();
|
|
11
12
|
assetListeners = /* @__PURE__ */ new Map();
|
|
12
|
-
constructor(credentials, demo = false) {
|
|
13
|
+
constructor(credentials, sdk, demo = false) {
|
|
13
14
|
this.credentials = credentials;
|
|
15
|
+
this.sdk = sdk;
|
|
14
16
|
this.demo = demo;
|
|
15
17
|
}
|
|
16
18
|
async connect(tradeAccountId, exchange) {
|
|
17
|
-
const creds = await this.
|
|
19
|
+
const creds = await this.resolveCredentials(tradeAccountId, exchange);
|
|
18
20
|
this.disconnectWs();
|
|
19
21
|
this.pendingTradeAccountId = tradeAccountId;
|
|
20
22
|
this.pendingExchange = exchange;
|
|
@@ -73,9 +75,9 @@ class AccountManager {
|
|
|
73
75
|
}
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
|
-
async setLeverage(symbol, leverage) {
|
|
78
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
77
79
|
if (!this.activeAccount) throw new Error("No active account");
|
|
78
|
-
await this.activeAccount.setLeverage(symbol, leverage);
|
|
80
|
+
await this.activeAccount.setLeverage(symbol, leverage, marginMode);
|
|
79
81
|
}
|
|
80
82
|
getLeverage(symbol) {
|
|
81
83
|
return this.activeAccount?.getLeverage(symbol) ?? null;
|
|
@@ -119,7 +121,22 @@ class AccountManager {
|
|
|
119
121
|
if (this.activeAccount || !this.pendingExchange || !this.pendingTradeAccountId) return;
|
|
120
122
|
const exchange = this.pendingExchange;
|
|
121
123
|
const tradeAccountId = this.pendingTradeAccountId;
|
|
122
|
-
this.
|
|
124
|
+
this.resolveCredentials(tradeAccountId, exchange).then((creds) => this.startWs(exchange, creds)).catch((err) => console.warn("[AccountManager] Failed to start WS:", err));
|
|
125
|
+
}
|
|
126
|
+
async resolveCredentials(tradeAccountId, exchange) {
|
|
127
|
+
const creds = await this.credentials.getOrFetch(tradeAccountId);
|
|
128
|
+
if (exchange === "hyperliquid") {
|
|
129
|
+
const { accounts } = await this.sdk.info.accounts.list();
|
|
130
|
+
const account = accounts.find((a) => a.id === tradeAccountId);
|
|
131
|
+
if (!account) {
|
|
132
|
+
throw new Error(`Trade account ${tradeAccountId} not found`);
|
|
133
|
+
}
|
|
134
|
+
if (!account.exchangeIdentifier) {
|
|
135
|
+
throw new Error(`Trade account ${tradeAccountId} has no Hyperliquid address`);
|
|
136
|
+
}
|
|
137
|
+
return { ...creds, address: account.exchangeIdentifier };
|
|
138
|
+
}
|
|
139
|
+
return creds;
|
|
123
140
|
}
|
|
124
141
|
async startWs(exchange, creds) {
|
|
125
142
|
const account = new ExchangeAccount({ exchange, demo: this.demo });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Connector } from '@pear-protocol/types';
|
|
2
|
-
import { AccountConfig, SupportedCredentials, BalanceCallback, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, ExchangeState } from './types.js';
|
|
2
|
+
import { AccountConfig, SupportedCredentials, BalanceCallback, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, MarginMode, ExchangeState } from './types.js';
|
|
3
3
|
|
|
4
4
|
declare class ExchangeAccount {
|
|
5
5
|
private exchange;
|
|
@@ -27,7 +27,7 @@ declare class ExchangeAccount {
|
|
|
27
27
|
untrackAsset(id: string): void;
|
|
28
28
|
getTrackedAsset(coin: string): TrackedAssetInfo | null;
|
|
29
29
|
getLeverage(symbol: string): string | null;
|
|
30
|
-
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
30
|
+
setLeverage(symbol: string, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
31
31
|
getState(): Readonly<ExchangeState>;
|
|
32
32
|
getExchange(): Connector;
|
|
33
33
|
private applyStateUpdate;
|
|
@@ -120,9 +120,9 @@ class ExchangeAccount {
|
|
|
120
120
|
}
|
|
121
121
|
return null;
|
|
122
122
|
}
|
|
123
|
-
async setLeverage(symbol, leverage) {
|
|
123
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
124
124
|
if (!this.orchestrator) throw new Error("Not connected");
|
|
125
|
-
await this.orchestrator.setLeverage(symbol, leverage);
|
|
125
|
+
await this.orchestrator.setLeverage(symbol, leverage, marginMode);
|
|
126
126
|
}
|
|
127
127
|
getState() {
|
|
128
128
|
return this.state;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SupportedCredentials, StateUpdate } from '../types.js';
|
|
1
|
+
import { SupportedCredentials, MarginMode, StateUpdate } from '../types.js';
|
|
2
2
|
import '@pear-protocol/types';
|
|
3
3
|
|
|
4
4
|
type StateUpdateHandler = (update: StateUpdate) => void;
|
|
@@ -14,7 +14,7 @@ interface Orchestrator {
|
|
|
14
14
|
isConnected: boolean;
|
|
15
15
|
onAssetTracked: (coin: string) => void;
|
|
16
16
|
onAssetUntracked: (coin: string) => void;
|
|
17
|
-
setLeverage: (symbol: string, leverage: string) => Promise<void>;
|
|
17
|
+
setLeverage: (symbol: string, leverage: string, marginMode?: MarginMode) => Promise<void>;
|
|
18
18
|
trackAsset: (coin: string) => void;
|
|
19
19
|
untrackAsset: (coin: string) => boolean;
|
|
20
20
|
getTrackedCoins: () => ReadonlySet<string>;
|
|
@@ -36,7 +36,7 @@ declare abstract class BaseOrchestrator implements Orchestrator {
|
|
|
36
36
|
abstract get isConnected(): boolean;
|
|
37
37
|
abstract onAssetTracked(coin: string): void;
|
|
38
38
|
abstract onAssetUntracked(coin: string): void;
|
|
39
|
-
abstract setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
39
|
+
abstract setLeverage(symbol: string, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
40
40
|
trackAsset(coin: string): void;
|
|
41
41
|
untrackAsset(coin: string): boolean;
|
|
42
42
|
getTrackedCoins(): ReadonlySet<string>;
|
|
@@ -2,7 +2,7 @@ import { AccountBalance, AccountPosition } from '../../types.js';
|
|
|
2
2
|
import { BinanceRawAccountStatus, BinanceRawPositionRisk } from './types.js';
|
|
3
3
|
import '@pear-protocol/types';
|
|
4
4
|
|
|
5
|
-
declare function mapBalance(raw: BinanceRawAccountStatus
|
|
5
|
+
declare function mapBalance(raw: BinanceRawAccountStatus): AccountBalance;
|
|
6
6
|
declare function mapPositions(raw: BinanceRawPositionRisk[]): Map<string, AccountPosition>;
|
|
7
7
|
declare function mergePositions(incoming: Map<string, AccountPosition>, existing: Map<string, AccountPosition>): Map<string, AccountPosition>;
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../../types';
|
|
2
2
|
import { positionKey } from '../../utils';
|
|
3
3
|
|
|
4
|
-
function mapBalance(raw
|
|
4
|
+
function mapBalance(raw) {
|
|
5
5
|
const free = {};
|
|
6
6
|
const used = {};
|
|
7
7
|
const total = {};
|
|
@@ -11,8 +11,6 @@ function mapBalance(raw, collateralRatios) {
|
|
|
11
11
|
const ab = a.availableBalance ?? "0";
|
|
12
12
|
const mb = a.marginBalance ?? "0";
|
|
13
13
|
const im = a.initialMargin ?? "0";
|
|
14
|
-
const mm = a.maintMargin ?? "0";
|
|
15
|
-
const up = a.unrealizedProfit ?? "0";
|
|
16
14
|
if (parseFloat(wb) === 0 && parseFloat(ab) === 0 && parseFloat(mb) === 0) continue;
|
|
17
15
|
free[a.asset] = ab;
|
|
18
16
|
used[a.asset] = im;
|
|
@@ -23,13 +21,7 @@ function mapBalance(raw, collateralRatios) {
|
|
|
23
21
|
used: im,
|
|
24
22
|
total: mb,
|
|
25
23
|
walletBalance: wb,
|
|
26
|
-
|
|
27
|
-
usdValue: mb,
|
|
28
|
-
initialMargin: im,
|
|
29
|
-
maintenanceMargin: mm,
|
|
30
|
-
isCollateral: a.marginAvailable ?? false,
|
|
31
|
-
collateralRatio: collateralRatios.get(a.asset) ?? "1.0000",
|
|
32
|
-
borrowedAmount: "0"
|
|
24
|
+
usdValue: mb
|
|
33
25
|
});
|
|
34
26
|
}
|
|
35
27
|
const totalMarginBalance = raw.totalMarginBalance ?? "0";
|
|
@@ -38,9 +30,7 @@ function mapBalance(raw, collateralRatios) {
|
|
|
38
30
|
const maintMargin = parseFloat(totalMaintMargin);
|
|
39
31
|
const marginRatio = marginBalance > 0 ? (maintMargin / marginBalance).toFixed(4) : "0.0000";
|
|
40
32
|
return {
|
|
41
|
-
exchange: "binance",
|
|
42
33
|
accountType: raw.multiAssetsMargin ? "multi-asset" : "single-asset",
|
|
43
|
-
marginMode: "cross",
|
|
44
34
|
free,
|
|
45
35
|
used,
|
|
46
36
|
total,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SupportedCredentials } from '../../types.js';
|
|
1
|
+
import { SupportedCredentials, MarginMode } from '../../types.js';
|
|
2
2
|
import { BaseOrchestrator, OrchestratorConfig } from '../base.js';
|
|
3
3
|
import '@pear-protocol/types';
|
|
4
4
|
|
|
@@ -13,8 +13,6 @@ declare class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
13
13
|
private creds;
|
|
14
14
|
private trackedAssetInfos;
|
|
15
15
|
private currentPositions;
|
|
16
|
-
private collateralRatios;
|
|
17
|
-
private multiAssetsMargin;
|
|
18
16
|
private readonly listenKeyRefreshMs;
|
|
19
17
|
private readonly accountPollMs;
|
|
20
18
|
constructor(config: BinanceOrchestratorConfig);
|
|
@@ -23,7 +21,7 @@ declare class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
23
21
|
get isConnected(): boolean;
|
|
24
22
|
onAssetTracked(coin: string): void;
|
|
25
23
|
onAssetUntracked(coin: string): void;
|
|
26
|
-
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
24
|
+
setLeverage(symbol: string, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
27
25
|
private stopAll;
|
|
28
26
|
private pollAccountStatus;
|
|
29
27
|
private handleUserDataEvent;
|
|
@@ -2,7 +2,7 @@ import { positionKey } from '../../utils';
|
|
|
2
2
|
import { BaseOrchestrator } from '../base';
|
|
3
3
|
import { DEFAULT_LISTEN_KEY_REFRESH_MS, DEFAULT_ACCOUNT_POLL_MS } from './const';
|
|
4
4
|
import { mapBalance, mapPositions, mergePositions } from './mapper';
|
|
5
|
-
import { fetchAccountStatus, fetchAllPositionRisk,
|
|
5
|
+
import { fetchAccountStatus, fetchAllPositionRisk, createListenKey, keepAliveListenKey, fetchPositionRisk, setMarginType, setLeverage } from './rest';
|
|
6
6
|
import { BinanceWs } from './ws';
|
|
7
7
|
|
|
8
8
|
class BinanceOrchestrator extends BaseOrchestrator {
|
|
@@ -12,8 +12,6 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
12
12
|
creds = null;
|
|
13
13
|
trackedAssetInfos = /* @__PURE__ */ new Map();
|
|
14
14
|
currentPositions = /* @__PURE__ */ new Map();
|
|
15
|
-
collateralRatios = /* @__PURE__ */ new Map();
|
|
16
|
-
multiAssetsMargin = false;
|
|
17
15
|
listenKeyRefreshMs;
|
|
18
16
|
accountPollMs;
|
|
19
17
|
constructor(config) {
|
|
@@ -33,18 +31,6 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
33
31
|
fetchAllPositionRisk(this.creds, this.demo)
|
|
34
32
|
]);
|
|
35
33
|
if (gen !== this.startGeneration) return;
|
|
36
|
-
this.multiAssetsMargin = snapshot.multiAssetsMargin ?? false;
|
|
37
|
-
if (this.multiAssetsMargin) {
|
|
38
|
-
const indexData = await fetchAssetIndex(this.demo);
|
|
39
|
-
if (gen !== this.startGeneration) return;
|
|
40
|
-
this.collateralRatios.clear();
|
|
41
|
-
for (const item of indexData) {
|
|
42
|
-
const asset = item.symbol.replace(/USD$/, "");
|
|
43
|
-
if (item.bidRate && parseFloat(item.bidRate) > 0) {
|
|
44
|
-
this.collateralRatios.set(asset, item.bidRate);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
34
|
this.handleAccountStatus(snapshot);
|
|
49
35
|
this.handlePositionRisk(positionRisk, false);
|
|
50
36
|
const listenKey = await createListenKey(this.creds.api_key, this.demo);
|
|
@@ -87,8 +73,6 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
87
73
|
this.creds = null;
|
|
88
74
|
this.trackedAssetInfos.clear();
|
|
89
75
|
this.currentPositions.clear();
|
|
90
|
-
this.collateralRatios.clear();
|
|
91
|
-
this.multiAssetsMargin = false;
|
|
92
76
|
}
|
|
93
77
|
get isConnected() {
|
|
94
78
|
return this.ws?.isConnected ?? false;
|
|
@@ -102,10 +86,7 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
102
86
|
const info = {
|
|
103
87
|
coin,
|
|
104
88
|
leverage,
|
|
105
|
-
marginType
|
|
106
|
-
availableToTrade: null,
|
|
107
|
-
maxTradeSzs: null,
|
|
108
|
-
collateralToken: null
|
|
89
|
+
marginType
|
|
109
90
|
};
|
|
110
91
|
this.trackedAssetInfos.set(coin, info);
|
|
111
92
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
@@ -119,8 +100,10 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
119
100
|
this.trackedAssetInfos.delete(coin);
|
|
120
101
|
this.onStateUpdate({ trackedAssets: new Map(this.trackedAssetInfos) });
|
|
121
102
|
}
|
|
122
|
-
async setLeverage(symbol, leverage) {
|
|
103
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
123
104
|
if (!this.creds) throw new Error("Not connected");
|
|
105
|
+
const mode = marginMode ?? "cross";
|
|
106
|
+
await setMarginType(this.creds, symbol, mode === "isolated" ? "ISOLATED" : "CROSSED", this.demo);
|
|
124
107
|
await setLeverage(this.creds, symbol, leverage, this.demo);
|
|
125
108
|
}
|
|
126
109
|
stopAll() {
|
|
@@ -166,7 +149,7 @@ class BinanceOrchestrator extends BaseOrchestrator {
|
|
|
166
149
|
}
|
|
167
150
|
}
|
|
168
151
|
handleAccountStatus(result) {
|
|
169
|
-
const balance = mapBalance(result
|
|
152
|
+
const balance = mapBalance(result);
|
|
170
153
|
this.onStateUpdate({ balance });
|
|
171
154
|
}
|
|
172
155
|
handlePositionRisk(risk, merge) {
|
|
@@ -8,6 +8,7 @@ declare function fetchAccountStatus(creds: BinanceUSDMCredentialsDTO, demo: bool
|
|
|
8
8
|
declare function fetchPositionRisk(creds: BinanceUSDMCredentialsDTO, symbol: string, demo: boolean): Promise<BinanceRawPositionRisk | undefined>;
|
|
9
9
|
declare function fetchAllPositionRisk(creds: BinanceUSDMCredentialsDTO, demo: boolean): Promise<BinanceRawPositionRisk[]>;
|
|
10
10
|
declare function setLeverage(creds: BinanceUSDMCredentialsDTO, symbol: string, leverage: string, demo: boolean): Promise<BinanceRawLeverageResponse>;
|
|
11
|
+
declare function setMarginType(creds: BinanceUSDMCredentialsDTO, symbol: string, marginType: 'ISOLATED' | 'CROSSED', demo: boolean): Promise<void>;
|
|
11
12
|
declare function fetchAssetIndex(demo: boolean): Promise<BinanceAssetIndex[]>;
|
|
12
13
|
|
|
13
|
-
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage };
|
|
14
|
+
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage, setMarginType };
|
|
@@ -69,6 +69,9 @@ async function fetchAllPositionRisk(creds, demo) {
|
|
|
69
69
|
async function setLeverage(creds, symbol, leverage, demo) {
|
|
70
70
|
return signedPost(creds, "/fapi/v1/leverage", { symbol, leverage }, demo);
|
|
71
71
|
}
|
|
72
|
+
async function setMarginType(creds, symbol, marginType, demo) {
|
|
73
|
+
await signedPost(creds, "/fapi/v1/marginType", { symbol, marginType }, demo);
|
|
74
|
+
}
|
|
72
75
|
async function fetchAssetIndex(demo) {
|
|
73
76
|
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
74
77
|
const response = await fetch(`${baseUrl}/fapi/v1/assetIndex`);
|
|
@@ -78,4 +81,4 @@ async function fetchAssetIndex(demo) {
|
|
|
78
81
|
return await response.json();
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage };
|
|
84
|
+
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage, setMarginType };
|
|
@@ -2,7 +2,7 @@ import { AccountBalance, AccountPosition } from '../../types.js';
|
|
|
2
2
|
import { BybitRawWallet, BybitRawCoin, BybitRawPosition } from './types.js';
|
|
3
3
|
import '@pear-protocol/types';
|
|
4
4
|
|
|
5
|
-
declare function buildBalance(raw: BybitRawWallet, coins: Map<string, BybitRawCoin>, marginMode: string
|
|
5
|
+
declare function buildBalance(raw: BybitRawWallet, coins: Map<string, BybitRawCoin>, marginMode: string): AccountBalance;
|
|
6
6
|
declare function mergeRawCoins(existing: Map<string, BybitRawCoin>, incoming: BybitRawCoin[]): Map<string, BybitRawCoin>;
|
|
7
7
|
declare function mapSinglePosition(p: BybitRawPosition, size: string): AccountPosition;
|
|
8
8
|
declare function mapPositions(raw: BybitRawPosition[]): Map<string, AccountPosition>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../../types';
|
|
2
2
|
import { positionKey } from '../../utils';
|
|
3
3
|
|
|
4
|
-
function buildBalance(raw, coins, marginMode
|
|
4
|
+
function buildBalance(raw, coins, marginMode) {
|
|
5
5
|
const free = {};
|
|
6
6
|
const used = {};
|
|
7
7
|
const total = {};
|
|
@@ -11,7 +11,6 @@ function buildBalance(raw, coins, marginMode, collateralRatios) {
|
|
|
11
11
|
const equity = c.equity ?? "0";
|
|
12
12
|
const positionInitialMargin = c.totalPositionIM === "" ? "0" : c.totalPositionIM ?? "0";
|
|
13
13
|
const orderInitialMargin = c.totalOrderIM === "" ? "0" : c.totalOrderIM ?? "0";
|
|
14
|
-
const positionMaintenanceMargin = c.totalPositionMM === "" ? "0" : c.totalPositionMM ?? "0";
|
|
15
14
|
const locked = c.locked ?? "0";
|
|
16
15
|
const equityNum = parseFloat(equity);
|
|
17
16
|
const positionInitialMarginNum = parseFloat(positionInitialMargin);
|
|
@@ -36,19 +35,11 @@ function buildBalance(raw, coins, marginMode, collateralRatios) {
|
|
|
36
35
|
used: coinUsed,
|
|
37
36
|
total: equity,
|
|
38
37
|
walletBalance: c.walletBalance ?? "0",
|
|
39
|
-
|
|
40
|
-
usdValue: c.usdValue ?? "0",
|
|
41
|
-
initialMargin: isPortfolio ? "0" : (positionInitialMarginNum + orderInitialMarginNum).toString(),
|
|
42
|
-
maintenanceMargin: positionMaintenanceMargin,
|
|
43
|
-
isCollateral: (c.marginCollateral ?? false) && (c.collateralSwitch ?? false),
|
|
44
|
-
collateralRatio: collateralRatios.get(c.coin) ?? "1.0000",
|
|
45
|
-
borrowedAmount: c.borrowAmount ?? "0"
|
|
38
|
+
usdValue: c.usdValue ?? "0"
|
|
46
39
|
});
|
|
47
40
|
}
|
|
48
41
|
return {
|
|
49
|
-
exchange: "bybit",
|
|
50
42
|
accountType: "UNIFIED",
|
|
51
|
-
marginMode,
|
|
52
43
|
free,
|
|
53
44
|
used,
|
|
54
45
|
total,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SupportedCredentials } from '../../types.js';
|
|
1
|
+
import { SupportedCredentials, MarginMode } from '../../types.js';
|
|
2
2
|
import { BaseOrchestrator } from '../base.js';
|
|
3
3
|
import '@pear-protocol/types';
|
|
4
4
|
|
|
@@ -8,13 +8,12 @@ declare class BybitOrchestrator extends BaseOrchestrator {
|
|
|
8
8
|
private lastWallet;
|
|
9
9
|
private trackedAssetInfos;
|
|
10
10
|
private marginMode;
|
|
11
|
-
private collateralRatios;
|
|
12
11
|
start(credentials: SupportedCredentials): Promise<void>;
|
|
13
12
|
stop(): void;
|
|
14
13
|
get isConnected(): boolean;
|
|
15
14
|
onAssetTracked(coin: string): void;
|
|
16
15
|
onAssetUntracked(coin: string): void;
|
|
17
|
-
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
16
|
+
setLeverage(symbol: string, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
18
17
|
private stopAll;
|
|
19
18
|
private handleMessage;
|
|
20
19
|
private handleWalletUpdate;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseOrchestrator } from '../base';
|
|
2
2
|
import { mergeRawCoins, buildBalance, mapPositions, mapPositionUpdates } from './mapper';
|
|
3
|
-
import { fetchWalletBalance, fetchPositions, fetchAccountInfo,
|
|
3
|
+
import { fetchWalletBalance, fetchPositions, fetchAccountInfo, fetchPositionLeverage, switchMarginMode } from './rest';
|
|
4
4
|
import { BybitWs } from './ws';
|
|
5
5
|
|
|
6
6
|
class BybitOrchestrator extends BaseOrchestrator {
|
|
@@ -9,7 +9,6 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
9
9
|
lastWallet = null;
|
|
10
10
|
trackedAssetInfos = /* @__PURE__ */ new Map();
|
|
11
11
|
marginMode = "cross";
|
|
12
|
-
collateralRatios = /* @__PURE__ */ new Map();
|
|
13
12
|
async start(credentials) {
|
|
14
13
|
const gen = ++this.startGeneration;
|
|
15
14
|
this.manualClose = false;
|
|
@@ -17,11 +16,10 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
17
16
|
const creds = credentials;
|
|
18
17
|
this.stopAll();
|
|
19
18
|
try {
|
|
20
|
-
const [wallet, positions, accountInfo
|
|
19
|
+
const [wallet, positions, accountInfo] = await Promise.all([
|
|
21
20
|
fetchWalletBalance(creds, this.demo),
|
|
22
21
|
fetchPositions(creds, this.demo),
|
|
23
|
-
fetchAccountInfo(creds, this.demo)
|
|
24
|
-
fetchCollateralRatios(creds, this.demo)
|
|
22
|
+
fetchAccountInfo(creds, this.demo)
|
|
25
23
|
]);
|
|
26
24
|
if (gen !== this.startGeneration) return;
|
|
27
25
|
const MARGIN_MODE_MAP = {
|
|
@@ -31,12 +29,11 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
31
29
|
};
|
|
32
30
|
const rawMode = accountInfo?.marginMode ?? "REGULAR_MARGIN";
|
|
33
31
|
this.marginMode = MARGIN_MODE_MAP[rawMode] ?? "cross";
|
|
34
|
-
this.collateralRatios = ratios;
|
|
35
32
|
if (wallet) {
|
|
36
33
|
this.lastWallet = wallet;
|
|
37
34
|
this.rawCoins = mergeRawCoins(/* @__PURE__ */ new Map(), wallet.coin ?? []);
|
|
38
35
|
this.onStateUpdate({
|
|
39
|
-
balance: buildBalance(wallet, this.rawCoins, this.marginMode
|
|
36
|
+
balance: buildBalance(wallet, this.rawCoins, this.marginMode)
|
|
40
37
|
});
|
|
41
38
|
}
|
|
42
39
|
this.onStateUpdate({ positions: mapPositions(positions) });
|
|
@@ -67,7 +64,6 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
67
64
|
this.rawCoins.clear();
|
|
68
65
|
this.lastWallet = null;
|
|
69
66
|
this.trackedAssetInfos.clear();
|
|
70
|
-
this.collateralRatios.clear();
|
|
71
67
|
this.marginMode = "cross";
|
|
72
68
|
this.credentials = null;
|
|
73
69
|
}
|
|
@@ -83,10 +79,7 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
83
79
|
const info = {
|
|
84
80
|
coin,
|
|
85
81
|
leverage,
|
|
86
|
-
marginType
|
|
87
|
-
availableToTrade: null,
|
|
88
|
-
maxTradeSzs: null,
|
|
89
|
-
collateralToken: null
|
|
82
|
+
marginType
|
|
90
83
|
};
|
|
91
84
|
this.trackedAssetInfos.set(coin, info);
|
|
92
85
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
@@ -100,10 +93,11 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
100
93
|
this.trackedAssetInfos.delete(coin);
|
|
101
94
|
this.onStateUpdate({ trackedAssets: new Map(this.trackedAssetInfos) });
|
|
102
95
|
}
|
|
103
|
-
async setLeverage(symbol, leverage) {
|
|
96
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
104
97
|
const creds = this.credentials;
|
|
105
98
|
if (!creds) throw new Error("Not connected");
|
|
106
|
-
|
|
99
|
+
const mode = marginMode ?? "cross";
|
|
100
|
+
await switchMarginMode(creds, symbol, mode === "isolated" ? 1 : 0, leverage, this.demo);
|
|
107
101
|
}
|
|
108
102
|
stopAll() {
|
|
109
103
|
this.clearReconnectTimer();
|
|
@@ -151,7 +145,7 @@ class BybitOrchestrator extends BaseOrchestrator {
|
|
|
151
145
|
} : raw;
|
|
152
146
|
this.lastWallet = merged;
|
|
153
147
|
this.onStateUpdate({
|
|
154
|
-
balance: buildBalance(merged, this.rawCoins, this.marginMode
|
|
148
|
+
balance: buildBalance(merged, this.rawCoins, this.marginMode)
|
|
155
149
|
});
|
|
156
150
|
}
|
|
157
151
|
}
|
|
@@ -5,7 +5,8 @@ declare function fetchWalletBalance(creds: BybitCredentialsDTO, demo: boolean):
|
|
|
5
5
|
declare function fetchPositions(creds: BybitCredentialsDTO, demo: boolean): Promise<BybitRawPosition[]>;
|
|
6
6
|
declare function fetchPositionLeverage(creds: BybitCredentialsDTO, symbol: string, demo: boolean): Promise<BybitRawPosition | undefined>;
|
|
7
7
|
declare function setLeverage(creds: BybitCredentialsDTO, symbol: string, leverage: string, demo: boolean): Promise<void>;
|
|
8
|
+
declare function switchMarginMode(creds: BybitCredentialsDTO, symbol: string, tradeMode: 0 | 1, leverage: string, demo: boolean): Promise<void>;
|
|
8
9
|
declare function fetchAccountInfo(creds: BybitCredentialsDTO, demo: boolean): Promise<BybitRawAccountInfo>;
|
|
9
10
|
declare function fetchCollateralRatios(creds: BybitCredentialsDTO, demo: boolean): Promise<Map<string, string>>;
|
|
10
11
|
|
|
11
|
-
export { fetchAccountInfo, fetchCollateralRatios, fetchPositionLeverage, fetchPositions, fetchWalletBalance, setLeverage };
|
|
12
|
+
export { fetchAccountInfo, fetchCollateralRatios, fetchPositionLeverage, fetchPositions, fetchWalletBalance, setLeverage, switchMarginMode };
|
|
@@ -87,6 +87,14 @@ async function setLeverage(creds, symbol, leverage, demo) {
|
|
|
87
87
|
demo
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
|
+
async function switchMarginMode(creds, symbol, tradeMode, leverage, demo) {
|
|
91
|
+
await signedPost(
|
|
92
|
+
creds,
|
|
93
|
+
"/v5/position/switch-isolated",
|
|
94
|
+
{ category: "linear", symbol, tradeMode, buyLeverage: leverage, sellLeverage: leverage },
|
|
95
|
+
demo
|
|
96
|
+
);
|
|
97
|
+
}
|
|
90
98
|
async function fetchAccountInfo(creds, demo) {
|
|
91
99
|
return signedGet(creds, "/v5/account/account-info", {}, demo);
|
|
92
100
|
}
|
|
@@ -107,4 +115,4 @@ async function fetchCollateralRatios(creds, demo) {
|
|
|
107
115
|
return ratios;
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
export { fetchAccountInfo, fetchCollateralRatios, fetchPositionLeverage, fetchPositions, fetchWalletBalance, setLeverage };
|
|
118
|
+
export { fetchAccountInfo, fetchCollateralRatios, fetchPositionLeverage, fetchPositions, fetchWalletBalance, setLeverage, switchMarginMode };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AccountBalance, AccountPosition } from '../../types.js';
|
|
2
|
+
import { AggregatedClearinghouseSummary, HyperliquidCoinBalance, AssetPosition } from './types.js';
|
|
3
|
+
import '@pear-protocol/types';
|
|
4
|
+
import '@nktkas/hyperliquid/api/subscription';
|
|
5
|
+
|
|
6
|
+
declare function mapBalance(summary: AggregatedClearinghouseSummary, spotCoinBalances: Map<string, HyperliquidCoinBalance>, accountType: string, availableToTradeByCollateral: Map<string, number>): AccountBalance;
|
|
7
|
+
declare function mapPositions(raw: AssetPosition[]): Map<string, AccountPosition>;
|
|
8
|
+
|
|
9
|
+
export { mapBalance, mapPositions };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { positionKey } from '../../utils';
|
|
2
|
+
import { COLLATERAL_TOKEN_MAP } from './types';
|
|
3
|
+
|
|
4
|
+
const COLLATERAL_COINS = new Set(Object.values(COLLATERAL_TOKEN_MAP));
|
|
5
|
+
function mapBalance(summary, spotCoinBalances, accountType, availableToTradeByCollateral) {
|
|
6
|
+
const free = {};
|
|
7
|
+
const used = {};
|
|
8
|
+
const total = {};
|
|
9
|
+
const assets = [];
|
|
10
|
+
for (const [, coin] of spotCoinBalances) {
|
|
11
|
+
if (!COLLATERAL_COINS.has(coin.coin)) continue;
|
|
12
|
+
const availableOverride = availableToTradeByCollateral.get(coin.coin);
|
|
13
|
+
const coinFree = availableOverride !== void 0 ? availableOverride.toString() : (coin.total - coin.hold).toString();
|
|
14
|
+
const coinUsed = coin.hold.toString();
|
|
15
|
+
const coinTotal = coin.total.toString();
|
|
16
|
+
free[coin.coin] = coinFree;
|
|
17
|
+
used[coin.coin] = coinUsed;
|
|
18
|
+
total[coin.coin] = coinTotal;
|
|
19
|
+
assets.push({
|
|
20
|
+
asset: coin.coin,
|
|
21
|
+
free: coinFree,
|
|
22
|
+
used: coinUsed,
|
|
23
|
+
total: coinTotal,
|
|
24
|
+
walletBalance: coinTotal,
|
|
25
|
+
usdValue: coinTotal
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const walletBalance = summary.accountValue - summary.totalUnrealizedPnl;
|
|
29
|
+
const marginRatio = summary.accountValue > 0 ? (summary.crossMaintenanceMarginUsed / summary.accountValue).toFixed(4) : "0.0000";
|
|
30
|
+
return {
|
|
31
|
+
accountType,
|
|
32
|
+
free,
|
|
33
|
+
used,
|
|
34
|
+
total,
|
|
35
|
+
availableToTrade: summary.withdrawable.toString(),
|
|
36
|
+
totalEquity: summary.accountValue.toString(),
|
|
37
|
+
walletBalance: walletBalance.toString(),
|
|
38
|
+
unrealizedPnl: summary.totalUnrealizedPnl.toString(),
|
|
39
|
+
initialMarginUsed: summary.totalMarginUsed.toString(),
|
|
40
|
+
maintenanceMargin: summary.crossMaintenanceMarginUsed.toString(),
|
|
41
|
+
marginRatio,
|
|
42
|
+
assets,
|
|
43
|
+
timestamp: Date.now()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mapPositions(raw) {
|
|
47
|
+
const positions = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const ap of raw) {
|
|
49
|
+
const pos = ap.position;
|
|
50
|
+
const szi = Number.parseFloat(pos.szi || "0");
|
|
51
|
+
if (szi === 0) continue;
|
|
52
|
+
const coin = pos.coin || "";
|
|
53
|
+
const side = szi > 0 ? "long" : "short";
|
|
54
|
+
positions.set(positionKey(coin, side), {
|
|
55
|
+
symbol: coin,
|
|
56
|
+
side,
|
|
57
|
+
size: szi.toString(),
|
|
58
|
+
entryPrice: pos.entryPx || "0",
|
|
59
|
+
unrealizedPnl: pos.unrealizedPnl || "0",
|
|
60
|
+
leverage: pos.leverage.value.toString(),
|
|
61
|
+
marginType: pos.leverage.type,
|
|
62
|
+
liquidationPrice: pos.liquidationPx || null
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return positions;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { mapBalance, mapPositions };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { InstrumentId } from '@pear-protocol/types';
|
|
2
|
+
import { SupportedCredentials, MarginMode } from '../../types.js';
|
|
3
|
+
import { BaseOrchestrator } from '../base.js';
|
|
4
|
+
|
|
5
|
+
declare class HyperliquidOrchestrator extends BaseOrchestrator {
|
|
6
|
+
private static httpTransport;
|
|
7
|
+
private static wsTransport;
|
|
8
|
+
private infoClient;
|
|
9
|
+
private subsClient;
|
|
10
|
+
private subscriptions;
|
|
11
|
+
private address;
|
|
12
|
+
private activeAssetSubscriptions;
|
|
13
|
+
private trackedAssetData;
|
|
14
|
+
private spotCoinBalances;
|
|
15
|
+
private accountType;
|
|
16
|
+
private symbolToCollateralToken;
|
|
17
|
+
private availableToTradeByCollateral;
|
|
18
|
+
private lastSummary;
|
|
19
|
+
start(credentials: SupportedCredentials): Promise<void>;
|
|
20
|
+
stop(): void;
|
|
21
|
+
get isConnected(): boolean;
|
|
22
|
+
onAssetTracked(coin: string): void;
|
|
23
|
+
onAssetUntracked(coin: string): void;
|
|
24
|
+
setLeverage(id: InstrumentId, leverage: string, marginMode?: MarginMode): Promise<void>;
|
|
25
|
+
protected canUntrackAsset(coin: string): boolean;
|
|
26
|
+
private fetchPerpMetas;
|
|
27
|
+
private subscribeActiveAssetData;
|
|
28
|
+
private stopAll;
|
|
29
|
+
private handleClearinghouseState;
|
|
30
|
+
private handleActiveAssetData;
|
|
31
|
+
private buildTrackedAssetInfo;
|
|
32
|
+
private buildTrackedAssetsMap;
|
|
33
|
+
private buildBalance;
|
|
34
|
+
private handleWebData3;
|
|
35
|
+
private handleSpotState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { HyperliquidOrchestrator };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { HttpTransport, WebSocketTransport, InfoClient, SubscriptionClient } from '@nktkas/hyperliquid';
|
|
2
|
+
import { BaseOrchestrator } from '../base';
|
|
3
|
+
import { LOCKED_TRACKED_COINS } from './const';
|
|
4
|
+
import { mapPositions, mapBalance } from './mapper';
|
|
5
|
+
import { setLeverage } from './rest';
|
|
6
|
+
import { COLLATERAL_TOKEN_MAP } from './types';
|
|
7
|
+
|
|
8
|
+
class HyperliquidOrchestrator extends BaseOrchestrator {
|
|
9
|
+
static httpTransport = new HttpTransport();
|
|
10
|
+
static wsTransport = new WebSocketTransport({
|
|
11
|
+
reconnect: {
|
|
12
|
+
maxRetries: Number.MAX_SAFE_INTEGER
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
infoClient = null;
|
|
16
|
+
subsClient = null;
|
|
17
|
+
subscriptions = [];
|
|
18
|
+
address = null;
|
|
19
|
+
activeAssetSubscriptions = /* @__PURE__ */ new Map();
|
|
20
|
+
trackedAssetData = /* @__PURE__ */ new Map();
|
|
21
|
+
spotCoinBalances = /* @__PURE__ */ new Map();
|
|
22
|
+
accountType = "disabled";
|
|
23
|
+
symbolToCollateralToken = /* @__PURE__ */ new Map();
|
|
24
|
+
availableToTradeByCollateral = /* @__PURE__ */ new Map();
|
|
25
|
+
lastSummary = {
|
|
26
|
+
accountValue: 0,
|
|
27
|
+
totalMarginUsed: 0,
|
|
28
|
+
totalRawUsd: 0,
|
|
29
|
+
crossMaintenanceMarginUsed: 0,
|
|
30
|
+
withdrawable: 0,
|
|
31
|
+
totalUnrealizedPnl: 0
|
|
32
|
+
};
|
|
33
|
+
async start(credentials) {
|
|
34
|
+
const gen = ++this.startGeneration;
|
|
35
|
+
this.manualClose = false;
|
|
36
|
+
this.credentials = credentials;
|
|
37
|
+
const creds = credentials;
|
|
38
|
+
this.address = creds.address;
|
|
39
|
+
await this.stopAll();
|
|
40
|
+
this.trackedAssetData.clear();
|
|
41
|
+
this.spotCoinBalances.clear();
|
|
42
|
+
this.symbolToCollateralToken.clear();
|
|
43
|
+
this.availableToTradeByCollateral.clear();
|
|
44
|
+
try {
|
|
45
|
+
this.infoClient = new InfoClient({ transport: HyperliquidOrchestrator.httpTransport });
|
|
46
|
+
await this.fetchPerpMetas();
|
|
47
|
+
if (gen !== this.startGeneration) return;
|
|
48
|
+
this.subsClient = new SubscriptionClient({ transport: HyperliquidOrchestrator.wsTransport });
|
|
49
|
+
const subs = [];
|
|
50
|
+
subs.push(
|
|
51
|
+
this.subsClient.allDexsClearinghouseState(
|
|
52
|
+
{ user: this.address },
|
|
53
|
+
(data) => this.handleClearinghouseState(data)
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
subs.push(this.subsClient.spotState({ user: this.address }, (data) => this.handleSpotState(data)));
|
|
57
|
+
subs.push(this.subsClient.webData3({ user: this.address }, (data) => this.handleWebData3(data)));
|
|
58
|
+
this.subscriptions = await Promise.all(subs);
|
|
59
|
+
if (gen !== this.startGeneration) return;
|
|
60
|
+
for (const sub of this.subscriptions) {
|
|
61
|
+
sub.failureSignal.addEventListener("abort", () => {
|
|
62
|
+
if (!this.manualClose) {
|
|
63
|
+
this.onError(new Error("Hyperliquid subscription failed after reconnect"));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const coin of LOCKED_TRACKED_COINS) {
|
|
68
|
+
this.trackAsset(coin);
|
|
69
|
+
}
|
|
70
|
+
for (const coin of this.trackedCoins) {
|
|
71
|
+
if (!LOCKED_TRACKED_COINS.includes(coin)) {
|
|
72
|
+
await this.subscribeActiveAssetData(coin);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.reconnectAttempts = 0;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (gen !== this.startGeneration) return;
|
|
78
|
+
await this.stopAll();
|
|
79
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
this.scheduleReconnect();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
stop() {
|
|
84
|
+
this.manualClose = true;
|
|
85
|
+
this.stopAll();
|
|
86
|
+
this.credentials = null;
|
|
87
|
+
this.address = null;
|
|
88
|
+
}
|
|
89
|
+
get isConnected() {
|
|
90
|
+
return this.subsClient !== null && !this.manualClose;
|
|
91
|
+
}
|
|
92
|
+
onAssetTracked(coin) {
|
|
93
|
+
this.subscribeActiveAssetData(coin).catch((err) => {
|
|
94
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
onAssetUntracked(coin) {
|
|
98
|
+
const sub = this.activeAssetSubscriptions.get(coin);
|
|
99
|
+
if (sub) {
|
|
100
|
+
sub.unsubscribe().catch(() => {
|
|
101
|
+
});
|
|
102
|
+
this.activeAssetSubscriptions.delete(coin);
|
|
103
|
+
}
|
|
104
|
+
this.trackedAssetData.delete(coin);
|
|
105
|
+
const trackedAssets = this.buildTrackedAssetsMap();
|
|
106
|
+
this.onStateUpdate({ trackedAssets });
|
|
107
|
+
}
|
|
108
|
+
async setLeverage(id, leverage, marginMode) {
|
|
109
|
+
const creds = this.credentials;
|
|
110
|
+
if (!creds) throw new Error("Not connected");
|
|
111
|
+
const asset = Number.parseInt(id, 10);
|
|
112
|
+
if (Number.isNaN(asset)) throw new Error(`Invalid Hyperliquid asset index: ${id}`);
|
|
113
|
+
const isCross = (marginMode ?? "cross") === "cross";
|
|
114
|
+
await setLeverage(creds, asset, Number.parseFloat(leverage), isCross);
|
|
115
|
+
}
|
|
116
|
+
canUntrackAsset(coin) {
|
|
117
|
+
return !LOCKED_TRACKED_COINS.includes(coin);
|
|
118
|
+
}
|
|
119
|
+
async fetchPerpMetas() {
|
|
120
|
+
if (!this.infoClient) return;
|
|
121
|
+
const allMetas = await this.infoClient.allPerpMetas();
|
|
122
|
+
allMetas.forEach((dexMeta) => {
|
|
123
|
+
const collateralToken = COLLATERAL_TOKEN_MAP[dexMeta.collateralToken];
|
|
124
|
+
dexMeta.universe.forEach((asset) => {
|
|
125
|
+
if (!asset.name) return;
|
|
126
|
+
if (collateralToken && !this.symbolToCollateralToken.has(asset.name)) {
|
|
127
|
+
this.symbolToCollateralToken.set(asset.name, collateralToken);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async subscribeActiveAssetData(coin) {
|
|
133
|
+
if (!this.subsClient || !this.address) return;
|
|
134
|
+
if (this.activeAssetSubscriptions.has(coin)) return;
|
|
135
|
+
const sub = await this.subsClient.activeAssetData(
|
|
136
|
+
{ coin, user: this.address },
|
|
137
|
+
(data) => this.handleActiveAssetData(data)
|
|
138
|
+
);
|
|
139
|
+
sub.failureSignal.addEventListener("abort", () => {
|
|
140
|
+
if (!this.manualClose) {
|
|
141
|
+
this.activeAssetSubscriptions.delete(coin);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.activeAssetSubscriptions.set(coin, sub);
|
|
145
|
+
}
|
|
146
|
+
async stopAll() {
|
|
147
|
+
this.clearReconnectTimer();
|
|
148
|
+
const assetUnsubs = Array.from(this.activeAssetSubscriptions.values()).map((s) => s.unsubscribe().catch(() => {
|
|
149
|
+
}));
|
|
150
|
+
this.activeAssetSubscriptions.clear();
|
|
151
|
+
const unsubs = this.subscriptions.map((s) => s.unsubscribe().catch(() => {
|
|
152
|
+
}));
|
|
153
|
+
this.subscriptions = [];
|
|
154
|
+
await Promise.all([...unsubs, ...assetUnsubs]);
|
|
155
|
+
this.subsClient = null;
|
|
156
|
+
this.infoClient = null;
|
|
157
|
+
}
|
|
158
|
+
handleClearinghouseState(data) {
|
|
159
|
+
const states = (data.clearinghouseStates || []).map(([, s]) => s).filter(Boolean);
|
|
160
|
+
const sumStr = (values) => values.reduce((acc, v) => acc + (Number.parseFloat(v || "0") || 0), 0);
|
|
161
|
+
const allAssetPositions = states.flatMap((s) => s.assetPositions || []);
|
|
162
|
+
const totalUnrealizedPnl = allAssetPositions.reduce(
|
|
163
|
+
(acc, ap) => acc + (Number.parseFloat(ap.position.unrealizedPnl || "0") || 0),
|
|
164
|
+
0
|
|
165
|
+
);
|
|
166
|
+
this.lastSummary = {
|
|
167
|
+
accountValue: sumStr(states.map((s) => s.crossMarginSummary.accountValue)),
|
|
168
|
+
totalMarginUsed: sumStr(states.map((s) => s.crossMarginSummary.totalMarginUsed)),
|
|
169
|
+
totalRawUsd: sumStr(states.map((s) => s.crossMarginSummary.totalRawUsd)),
|
|
170
|
+
crossMaintenanceMarginUsed: sumStr(states.map((s) => s.crossMaintenanceMarginUsed)),
|
|
171
|
+
withdrawable: sumStr(states.map((s) => s.withdrawable)),
|
|
172
|
+
totalUnrealizedPnl
|
|
173
|
+
};
|
|
174
|
+
const balance = this.buildBalance();
|
|
175
|
+
const positions = mapPositions(allAssetPositions);
|
|
176
|
+
const leverageSettings = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const ap of allAssetPositions) {
|
|
178
|
+
const coin = ap.position.coin;
|
|
179
|
+
if (coin && ap.position.leverage?.value) {
|
|
180
|
+
leverageSettings.set(coin, ap.position.leverage.value.toString());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.onStateUpdate({ balance, positions, leverageSettings });
|
|
184
|
+
}
|
|
185
|
+
handleActiveAssetData(data) {
|
|
186
|
+
if (!data.coin) return;
|
|
187
|
+
this.trackedAssetData.set(data.coin, data);
|
|
188
|
+
const collateral = this.symbolToCollateralToken.get(data.coin);
|
|
189
|
+
if (collateral && data.availableToTrade) {
|
|
190
|
+
const long = Number.parseFloat(data.availableToTrade[0] || "0");
|
|
191
|
+
const short = Number.parseFloat(data.availableToTrade[1] || "0");
|
|
192
|
+
const minAvailable = Math.min(long, short);
|
|
193
|
+
this.availableToTradeByCollateral.set(collateral, minAvailable);
|
|
194
|
+
const balance = this.buildBalance();
|
|
195
|
+
this.onStateUpdate({ balance });
|
|
196
|
+
}
|
|
197
|
+
const trackedAssets = /* @__PURE__ */ new Map();
|
|
198
|
+
trackedAssets.set(data.coin, this.buildTrackedAssetInfo(data));
|
|
199
|
+
this.onStateUpdate({ trackedAssets });
|
|
200
|
+
}
|
|
201
|
+
buildTrackedAssetInfo(data) {
|
|
202
|
+
return {
|
|
203
|
+
coin: data.coin,
|
|
204
|
+
leverage: (data.leverage?.value ?? 1).toString(),
|
|
205
|
+
marginType: data.leverage?.type === "isolated" ? "isolated" : "cross"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
buildTrackedAssetsMap() {
|
|
209
|
+
const result = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const [coin, data] of this.trackedAssetData) {
|
|
211
|
+
result.set(coin, this.buildTrackedAssetInfo(data));
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
buildBalance() {
|
|
216
|
+
return mapBalance(this.lastSummary, this.spotCoinBalances, this.accountType, this.availableToTradeByCollateral);
|
|
217
|
+
}
|
|
218
|
+
handleWebData3(data) {
|
|
219
|
+
this.accountType = data.userState.abstraction || "disabled";
|
|
220
|
+
}
|
|
221
|
+
handleSpotState(data) {
|
|
222
|
+
const balances = data?.spotState?.balances;
|
|
223
|
+
if (!balances) return;
|
|
224
|
+
for (const b of balances) {
|
|
225
|
+
const total = Number.parseFloat(b.total || "0");
|
|
226
|
+
const hold = Number.parseFloat(b.hold || "0");
|
|
227
|
+
if (total !== 0 || hold !== 0) {
|
|
228
|
+
this.spotCoinBalances.set(b.coin, { coin: b.coin, total, hold });
|
|
229
|
+
} else {
|
|
230
|
+
this.spotCoinBalances.delete(b.coin);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const balance = this.buildBalance();
|
|
234
|
+
this.onStateUpdate({ balance });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { HyperliquidOrchestrator };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HttpTransport, ExchangeClient } from '@nktkas/hyperliquid';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
|
|
4
|
+
async function setLeverage(creds, asset, leverage, isCross) {
|
|
5
|
+
const transport = new HttpTransport();
|
|
6
|
+
const wallet = privateKeyToAccount(creds.signer_key);
|
|
7
|
+
const exchangeClient = new ExchangeClient({ transport, wallet });
|
|
8
|
+
await exchangeClient.updateLeverage({ asset, leverage, isCross });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { setLeverage };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AllDexsClearinghouseStateEvent } from '@nktkas/hyperliquid/api/subscription';
|
|
2
|
+
|
|
3
|
+
type ClearinghouseState = AllDexsClearinghouseStateEvent['clearinghouseStates'][number][1];
|
|
4
|
+
type AssetPosition = ClearinghouseState['assetPositions'][number];
|
|
5
|
+
interface HyperliquidCoinBalance {
|
|
6
|
+
coin: string;
|
|
7
|
+
total: number;
|
|
8
|
+
hold: number;
|
|
9
|
+
}
|
|
10
|
+
interface AggregatedClearinghouseSummary {
|
|
11
|
+
accountValue: number;
|
|
12
|
+
totalMarginUsed: number;
|
|
13
|
+
totalRawUsd: number;
|
|
14
|
+
crossMaintenanceMarginUsed: number;
|
|
15
|
+
withdrawable: number;
|
|
16
|
+
totalUnrealizedPnl: number;
|
|
17
|
+
}
|
|
18
|
+
declare const COLLATERAL_TOKEN_MAP: Record<number, string>;
|
|
19
|
+
|
|
20
|
+
export { type AggregatedClearinghouseSummary, type AssetPosition, COLLATERAL_TOKEN_MAP, type ClearinghouseState, type HyperliquidCoinBalance };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import './base';
|
|
2
2
|
import { BinanceOrchestrator } from './binance/orchestrator';
|
|
3
3
|
import { BybitOrchestrator } from './bybit/orchestrator';
|
|
4
|
+
import { HyperliquidOrchestrator } from './hyperliquid/orchestrator';
|
|
4
5
|
|
|
5
6
|
function createOrchestrator(exchange, config) {
|
|
6
7
|
switch (exchange) {
|
|
@@ -8,6 +9,8 @@ function createOrchestrator(exchange, config) {
|
|
|
8
9
|
return new BinanceOrchestrator(config);
|
|
9
10
|
case "bybit":
|
|
10
11
|
return new BybitOrchestrator(config);
|
|
12
|
+
case "hyperliquid":
|
|
13
|
+
return new HyperliquidOrchestrator(config);
|
|
11
14
|
default:
|
|
12
15
|
throw new Error(`Unsupported exchange: ${exchange}`);
|
|
13
16
|
}
|
package/dist/account/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { AccountManager } from './account-manager.js';
|
|
2
2
|
export { ExchangeAccount } from './exchange-account.js';
|
|
3
3
|
export { createOrchestrator } from './exchanges/index.js';
|
|
4
|
-
export { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker } from './types.js';
|
|
4
|
+
export { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, HyperliquidAccountCredentials, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker } from './types.js';
|
|
5
5
|
export { Orchestrator, OrchestratorConfig } from './exchanges/base.js';
|
|
6
|
+
import '@pear-protocol/core-sdk';
|
|
6
7
|
import '@pear-protocol/types';
|
|
7
8
|
import '../credentials.js';
|
|
8
|
-
import '@pear-protocol/core-sdk';
|
package/dist/account/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Connector, BinanceUSDMCredentialsDTO, BybitCredentialsDTO } from '@pear-protocol/types';
|
|
1
|
+
import { Connector, BinanceUSDMCredentialsDTO, BybitCredentialsDTO, HyperliquidCredentialsDTO } from '@pear-protocol/types';
|
|
2
2
|
|
|
3
3
|
type MarginMode = 'cross' | 'isolated';
|
|
4
4
|
interface AssetBalance {
|
|
@@ -7,18 +7,10 @@ interface AssetBalance {
|
|
|
7
7
|
used: string;
|
|
8
8
|
total: string;
|
|
9
9
|
walletBalance: string;
|
|
10
|
-
unrealizedPnl: string;
|
|
11
10
|
usdValue: string;
|
|
12
|
-
initialMargin: string;
|
|
13
|
-
maintenanceMargin: string;
|
|
14
|
-
isCollateral: boolean;
|
|
15
|
-
collateralRatio: string;
|
|
16
|
-
borrowedAmount: string;
|
|
17
11
|
}
|
|
18
12
|
interface AccountBalance {
|
|
19
|
-
exchange: string;
|
|
20
13
|
accountType: string;
|
|
21
|
-
marginMode: string;
|
|
22
14
|
free: Record<string, string>;
|
|
23
15
|
used: Record<string, string>;
|
|
24
16
|
total: Record<string, string>;
|
|
@@ -46,9 +38,6 @@ interface TrackedAssetInfo {
|
|
|
46
38
|
coin: string;
|
|
47
39
|
leverage: string;
|
|
48
40
|
marginType: MarginMode;
|
|
49
|
-
availableToTrade: [string, string] | null;
|
|
50
|
-
maxTradeSzs: [string, string] | null;
|
|
51
|
-
collateralToken: string | null;
|
|
52
41
|
}
|
|
53
42
|
interface ExchangeState {
|
|
54
43
|
balance: AccountBalance | null;
|
|
@@ -70,7 +59,10 @@ interface AccountConfig {
|
|
|
70
59
|
demo?: boolean;
|
|
71
60
|
onAuthError?: () => void;
|
|
72
61
|
}
|
|
73
|
-
type
|
|
62
|
+
type HyperliquidAccountCredentials = HyperliquidCredentialsDTO & {
|
|
63
|
+
address: string;
|
|
64
|
+
};
|
|
65
|
+
type SupportedCredentials = BinanceUSDMCredentialsDTO | BybitCredentialsDTO | HyperliquidAccountCredentials;
|
|
74
66
|
interface StateUpdate {
|
|
75
67
|
balance?: AccountBalance;
|
|
76
68
|
positions?: Map<string, AccountPosition>;
|
|
@@ -78,4 +70,4 @@ interface StateUpdate {
|
|
|
78
70
|
trackedAssets?: Map<string, TrackedAssetInfo>;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
export type { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker };
|
|
73
|
+
export type { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, HyperliquidAccountCredentials, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import PearSDK from '@pear-protocol/core-sdk';
|
|
|
2
2
|
import { AccountManager } from './account/account-manager.js';
|
|
3
3
|
export { ExchangeAccount } from './account/exchange-account.js';
|
|
4
4
|
export { createOrchestrator } from './account/exchanges/index.js';
|
|
5
|
-
export { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker } from './account/types.js';
|
|
5
|
+
export { AccountBalance, AccountConfig, AccountPosition, AssetBalance, BalanceCallback, ExchangeState, HyperliquidAccountCredentials, MarginMode, PositionsCallback, StateUpdate, SupportedCredentials, TrackedAssetCallback, TrackedAssetInfo, Tracker } from './account/types.js';
|
|
6
6
|
import { Credentials } from './credentials.js';
|
|
7
7
|
export { Orchestrator, OrchestratorConfig } from './account/exchanges/base.js';
|
|
8
8
|
import '@pear-protocol/types';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ class ExchangesSDK {
|
|
|
10
10
|
constructor(config) {
|
|
11
11
|
this.sdk = config.sdk;
|
|
12
12
|
this.credentials = new Credentials(config.sdk);
|
|
13
|
-
this.account = new AccountManager(this.credentials, config.demo);
|
|
13
|
+
this.account = new AccountManager(this.credentials, config.sdk, config.demo);
|
|
14
14
|
}
|
|
15
15
|
destroy() {
|
|
16
16
|
this.account.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8-test.0",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -22,14 +22,17 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsup",
|
|
24
24
|
"clean": "rm -rf dist .turbo",
|
|
25
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"publish:test": "tsup && bun publish --tag test"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
29
|
+
"@nktkas/hyperliquid": "0.30.2",
|
|
28
30
|
"@pear-protocol/core-sdk": "0.0.8",
|
|
29
|
-
"@pear-protocol/types": "0.0.7"
|
|
31
|
+
"@pear-protocol/types": "0.0.7",
|
|
32
|
+
"viem": "^2.43.5"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
|
-
"@backend/typescript-config": "
|
|
35
|
+
"@backend/typescript-config": "0.0.0",
|
|
33
36
|
"tsup": "8.5.1",
|
|
34
37
|
"typescript": "5.9.3"
|
|
35
38
|
}
|