@pear-protocol/exchanges-sdk 0.0.1
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 +187 -0
- package/dist/account/account-manager.d.ts +34 -0
- package/dist/account/account-manager.js +140 -0
- package/dist/account/exchange-account.d.ts +36 -0
- package/dist/account/exchange-account.js +185 -0
- package/dist/account/exchanges/base.d.ts +49 -0
- package/dist/account/exchanges/base.js +63 -0
- package/dist/account/exchanges/binance/const.d.ts +10 -0
- package/dist/account/exchanges/binance/const.js +10 -0
- package/dist/account/exchanges/binance/mapper.d.ts +9 -0
- package/dist/account/exchanges/binance/mapper.js +96 -0
- package/dist/account/exchanges/binance/orchestrator.d.ts +35 -0
- package/dist/account/exchanges/binance/orchestrator.js +232 -0
- package/dist/account/exchanges/binance/rest.d.ts +13 -0
- package/dist/account/exchanges/binance/rest.js +81 -0
- package/dist/account/exchanges/binance/types.d.ts +77 -0
- package/dist/account/exchanges/binance/types.js +1 -0
- package/dist/account/exchanges/binance/ws.d.ts +21 -0
- package/dist/account/exchanges/binance/ws.js +85 -0
- package/dist/account/exchanges/bybit/const.d.ts +7 -0
- package/dist/account/exchanges/bybit/const.js +7 -0
- package/dist/account/exchanges/bybit/mapper.d.ts +11 -0
- package/dist/account/exchanges/bybit/mapper.js +106 -0
- package/dist/account/exchanges/bybit/orchestrator.d.ts +23 -0
- package/dist/account/exchanges/bybit/orchestrator.js +159 -0
- package/dist/account/exchanges/bybit/rest.d.ts +11 -0
- package/dist/account/exchanges/bybit/rest.js +110 -0
- package/dist/account/exchanges/bybit/types.d.ts +59 -0
- package/dist/account/exchanges/bybit/types.js +1 -0
- package/dist/account/exchanges/bybit/ws.d.ts +18 -0
- package/dist/account/exchanges/bybit/ws.js +74 -0
- package/dist/account/exchanges/index.d.ts +8 -0
- package/dist/account/exchanges/index.js +16 -0
- package/dist/account/index.d.ts +8 -0
- package/dist/account/index.js +4 -0
- package/dist/account/types.d.ts +81 -0
- package/dist/account/types.js +1 -0
- package/dist/account/utils.d.ts +7 -0
- package/dist/account/utils.js +15 -0
- package/dist/credentials.d.ts +28 -0
- package/dist/credentials.js +46 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +21 -0
- package/dist/utils/hmac.d.ts +4 -0
- package/dist/utils/hmac.js +17 -0
- package/dist/utils/ws.d.ts +7 -0
- package/dist/utils/ws.js +25 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @backend/exchanges-sdk
|
|
2
|
+
|
|
3
|
+
Unified SDK for connecting to cryptocurrency derivative exchanges. Provides real-time account state tracking including balances, positions, and leverage management through a single, exchange-agnostic interface.
|
|
4
|
+
|
|
5
|
+
## Supported Exchanges
|
|
6
|
+
|
|
7
|
+
| Exchange | Connector Name | Market Type |
|
|
8
|
+
|----------|---------------|-------------|
|
|
9
|
+
| Binance | `binanceusdm` | USDM Futures |
|
|
10
|
+
| Bybit | `bybit` | Linear Perpetuals |
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@backend/exchanges-sdk": "workspace:*"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires `@pear-protocol/core-sdk` and `@backend/shared` as peer dependencies.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import PearSDK from '@pear-protocol/core-sdk';
|
|
28
|
+
import { ExchangesSDK } from '@backend/exchanges-sdk';
|
|
29
|
+
|
|
30
|
+
const sdk = new PearSDK({ /* ... */ });
|
|
31
|
+
const exchanges = new ExchangesSDK({ sdk });
|
|
32
|
+
|
|
33
|
+
await exchanges.account.connect(tradeAccountId, 'binanceusdm');
|
|
34
|
+
|
|
35
|
+
const balanceTracker = exchanges.account.trackBalance((balance) => {
|
|
36
|
+
console.log(balance.totalEquity);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const positionsTracker = exchanges.account.trackPositions((positions) => {
|
|
40
|
+
for (const pos of positions) {
|
|
41
|
+
console.log(pos.symbol, pos.side, pos.size);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Cleanup
|
|
46
|
+
balanceTracker.untrack();
|
|
47
|
+
positionsTracker.untrack();
|
|
48
|
+
exchanges.destroy();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Initialization
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Production
|
|
57
|
+
const exchanges = new ExchangesSDK({ sdk });
|
|
58
|
+
|
|
59
|
+
// Testnet / demo
|
|
60
|
+
const exchanges = new ExchangesSDK({ sdk, demo: true });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Connecting to an Exchange
|
|
64
|
+
|
|
65
|
+
Credentials are fetched automatically from the backend when you call `connect`.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
await exchanges.account.connect(tradeAccountId, 'binanceusdm');
|
|
69
|
+
await exchanges.account.connect(tradeAccountId, 'bybit');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Check connection status:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
exchanges.account.isConnected; // WebSocket is active
|
|
76
|
+
exchanges.account.isInitialized; // First state snapshot received
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Tracking Balance
|
|
80
|
+
|
|
81
|
+
Subscribe to real-time balance updates. The callback fires on every balance change.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const tracker = exchanges.account.trackBalance((balance) => {
|
|
85
|
+
balance.totalEquity; // total account equity
|
|
86
|
+
balance.walletBalance; // wallet balance excluding unrealized PnL
|
|
87
|
+
balance.unrealizedPnl; // total unrealized PnL
|
|
88
|
+
balance.availableToTrade; // available margin for new trades
|
|
89
|
+
balance.marginRatio; // current margin ratio
|
|
90
|
+
|
|
91
|
+
// Per-asset breakdown
|
|
92
|
+
for (const asset of balance.assets) {
|
|
93
|
+
asset.asset; // "USDT", "BTC", etc.
|
|
94
|
+
asset.usdValue; // USD equivalent
|
|
95
|
+
asset.isCollateral; // whether the asset is used as collateral
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Read the latest value without waiting for a callback
|
|
100
|
+
const current = tracker.get();
|
|
101
|
+
|
|
102
|
+
// Stop receiving updates
|
|
103
|
+
tracker.untrack();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Tracking Positions
|
|
107
|
+
|
|
108
|
+
Subscribe to real-time position updates. The callback fires with the full list of open positions on every change.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const tracker = exchanges.account.trackPositions((positions) => {
|
|
112
|
+
for (const pos of positions) {
|
|
113
|
+
pos.symbol; // "BTCUSDT"
|
|
114
|
+
pos.side; // "long" | "short" | "both"
|
|
115
|
+
pos.size; // "0.5"
|
|
116
|
+
pos.entryPrice; // "65000.00"
|
|
117
|
+
pos.unrealizedPnl; // "120.50"
|
|
118
|
+
pos.leverage; // "10"
|
|
119
|
+
pos.marginType; // "cross" | "isolated"
|
|
120
|
+
pos.liquidationPrice; // "58000.00" or null
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const current = tracker.get();
|
|
125
|
+
tracker.untrack();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Tracking a Specific Asset
|
|
129
|
+
|
|
130
|
+
Retrieve leverage, margin type, and trade size constraints for a specific coin. Useful when preparing to place a trade.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const tracker = exchanges.account.trackAsset('ETH', (info) => {
|
|
134
|
+
info.coin; // "ETH"
|
|
135
|
+
info.leverage; // "20"
|
|
136
|
+
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
|
+
});
|
|
141
|
+
|
|
142
|
+
const current = tracker.get();
|
|
143
|
+
tracker.untrack();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Managing Leverage
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
await exchanges.account.setLeverage('BTCUSDT', '10');
|
|
150
|
+
|
|
151
|
+
// Read cached leverage (returns null if not yet fetched)
|
|
152
|
+
const leverage = exchanges.account.getLeverage('BTCUSDT');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Reading Full State
|
|
156
|
+
|
|
157
|
+
Access a read-only snapshot of the complete account state at any time.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const state = exchanges.account.getState();
|
|
161
|
+
if (state) {
|
|
162
|
+
state.balance; // latest balance or null
|
|
163
|
+
state.positions; // Map of all open positions
|
|
164
|
+
state.leverageSettings; // Map of symbol to leverage
|
|
165
|
+
state.trackedAssets; // Map of coin to asset info
|
|
166
|
+
state.lastUpdated; // timestamp of last update
|
|
167
|
+
state.initialized; // true after first snapshot
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Disconnecting
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Disconnect but keep credentials cached for reconnection
|
|
175
|
+
exchanges.account.disconnect();
|
|
176
|
+
|
|
177
|
+
// Full teardown: disconnect and clear all credentials
|
|
178
|
+
exchanges.destroy();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Behavior Notes
|
|
182
|
+
|
|
183
|
+
- The WebSocket connection starts lazily when the first tracker is registered and stops when all trackers are removed.
|
|
184
|
+
- On unexpected disconnection, the SDK reconnects automatically with exponential backoff (1s to 30s).
|
|
185
|
+
- Reconnection is suppressed after explicit `disconnect()` or `destroy()` calls.
|
|
186
|
+
- Credentials are held in memory only and never persisted to disk.
|
|
187
|
+
- All numeric values (balances, sizes, prices, PnL) are returned as strings to preserve decimal precision.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { Credentials } from '../credentials.js';
|
|
3
|
+
import { BalanceCallback, Tracker, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, ExchangeState } from './types.js';
|
|
4
|
+
import '@pear-protocol/core-sdk';
|
|
5
|
+
|
|
6
|
+
declare class AccountManager {
|
|
7
|
+
private credentials;
|
|
8
|
+
private demo;
|
|
9
|
+
private activeAccount;
|
|
10
|
+
private pendingExchange;
|
|
11
|
+
private pendingTradeAccountId;
|
|
12
|
+
private balanceListeners;
|
|
13
|
+
private positionListeners;
|
|
14
|
+
private assetListeners;
|
|
15
|
+
constructor(credentials: Credentials, demo?: boolean);
|
|
16
|
+
connect(tradeAccountId: string, exchange: Connector): Promise<void>;
|
|
17
|
+
trackBalance(cb: BalanceCallback): Tracker<AccountBalance | null>;
|
|
18
|
+
trackPositions(cb: PositionsCallback): Tracker<AccountPosition[]>;
|
|
19
|
+
trackAsset(coin: string, cb: TrackedAssetCallback): Tracker<TrackedAssetInfo | null>;
|
|
20
|
+
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
21
|
+
getLeverage(symbol: string): string | null;
|
|
22
|
+
getState(): Readonly<ExchangeState> | null;
|
|
23
|
+
disconnect(): void;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
get isConnected(): boolean;
|
|
26
|
+
get isInitialized(): boolean;
|
|
27
|
+
private get hasListeners();
|
|
28
|
+
private disconnectWs;
|
|
29
|
+
private maybeDisconnectWs;
|
|
30
|
+
private ensureWsStarted;
|
|
31
|
+
private startWs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { AccountManager };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ExchangeAccount } from './exchange-account';
|
|
2
|
+
|
|
3
|
+
class AccountManager {
|
|
4
|
+
credentials;
|
|
5
|
+
demo;
|
|
6
|
+
activeAccount = null;
|
|
7
|
+
pendingExchange = null;
|
|
8
|
+
pendingTradeAccountId = null;
|
|
9
|
+
balanceListeners = /* @__PURE__ */ new Map();
|
|
10
|
+
positionListeners = /* @__PURE__ */ new Map();
|
|
11
|
+
assetListeners = /* @__PURE__ */ new Map();
|
|
12
|
+
constructor(credentials, demo = false) {
|
|
13
|
+
this.credentials = credentials;
|
|
14
|
+
this.demo = demo;
|
|
15
|
+
}
|
|
16
|
+
async connect(tradeAccountId, exchange) {
|
|
17
|
+
const creds = await this.credentials.getOrFetch(tradeAccountId);
|
|
18
|
+
this.disconnectWs();
|
|
19
|
+
this.pendingTradeAccountId = tradeAccountId;
|
|
20
|
+
this.pendingExchange = exchange;
|
|
21
|
+
if (this.hasListeners) {
|
|
22
|
+
await this.startWs(exchange, creds);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
trackBalance(cb) {
|
|
26
|
+
const id = Math.random().toString(36).slice(2);
|
|
27
|
+
this.balanceListeners.set(id, cb);
|
|
28
|
+
if (this.activeAccount) {
|
|
29
|
+
this.activeAccount.trackBalance(cb);
|
|
30
|
+
} else {
|
|
31
|
+
this.ensureWsStarted();
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
get: () => this.activeAccount?.getBalance() ?? null,
|
|
35
|
+
untrack: () => {
|
|
36
|
+
this.balanceListeners.delete(id);
|
|
37
|
+
this.activeAccount?.untrackBalance(id);
|
|
38
|
+
this.maybeDisconnectWs();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
trackPositions(cb) {
|
|
43
|
+
const id = Math.random().toString(36).slice(2);
|
|
44
|
+
this.positionListeners.set(id, cb);
|
|
45
|
+
if (this.activeAccount) {
|
|
46
|
+
this.activeAccount.trackPositions(cb);
|
|
47
|
+
} else {
|
|
48
|
+
this.ensureWsStarted();
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
get: () => this.activeAccount?.getPositions() ?? [],
|
|
52
|
+
untrack: () => {
|
|
53
|
+
this.positionListeners.delete(id);
|
|
54
|
+
this.activeAccount?.untrackPositions(id);
|
|
55
|
+
this.maybeDisconnectWs();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
trackAsset(coin, cb) {
|
|
60
|
+
const id = Math.random().toString(36).slice(2);
|
|
61
|
+
this.assetListeners.set(id, { coin, cb });
|
|
62
|
+
if (this.activeAccount) {
|
|
63
|
+
this.activeAccount.trackAsset(coin, cb);
|
|
64
|
+
} else {
|
|
65
|
+
this.ensureWsStarted();
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
get: () => this.activeAccount?.getTrackedAsset(coin) ?? null,
|
|
69
|
+
untrack: () => {
|
|
70
|
+
this.assetListeners.delete(id);
|
|
71
|
+
this.activeAccount?.untrackAsset(id);
|
|
72
|
+
this.maybeDisconnectWs();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async setLeverage(symbol, leverage) {
|
|
77
|
+
if (!this.activeAccount) throw new Error("No active account");
|
|
78
|
+
await this.activeAccount.setLeverage(symbol, leverage);
|
|
79
|
+
}
|
|
80
|
+
getLeverage(symbol) {
|
|
81
|
+
return this.activeAccount?.getLeverage(symbol) ?? null;
|
|
82
|
+
}
|
|
83
|
+
getState() {
|
|
84
|
+
return this.activeAccount?.getState() ?? null;
|
|
85
|
+
}
|
|
86
|
+
disconnect() {
|
|
87
|
+
this.disconnectWs();
|
|
88
|
+
this.pendingExchange = null;
|
|
89
|
+
this.pendingTradeAccountId = null;
|
|
90
|
+
this.balanceListeners.clear();
|
|
91
|
+
this.positionListeners.clear();
|
|
92
|
+
this.assetListeners.clear();
|
|
93
|
+
}
|
|
94
|
+
destroy() {
|
|
95
|
+
this.disconnect();
|
|
96
|
+
this.credentials.clear();
|
|
97
|
+
}
|
|
98
|
+
get isConnected() {
|
|
99
|
+
return this.activeAccount?.isConnected ?? false;
|
|
100
|
+
}
|
|
101
|
+
get isInitialized() {
|
|
102
|
+
return this.activeAccount?.isInitialized ?? false;
|
|
103
|
+
}
|
|
104
|
+
get hasListeners() {
|
|
105
|
+
return this.balanceListeners.size > 0 || this.positionListeners.size > 0 || this.assetListeners.size > 0;
|
|
106
|
+
}
|
|
107
|
+
disconnectWs() {
|
|
108
|
+
if (this.activeAccount) {
|
|
109
|
+
this.activeAccount.destroy();
|
|
110
|
+
this.activeAccount = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
maybeDisconnectWs() {
|
|
114
|
+
if (!this.hasListeners) {
|
|
115
|
+
this.disconnectWs();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
ensureWsStarted() {
|
|
119
|
+
if (this.activeAccount || !this.pendingExchange || !this.pendingTradeAccountId) return;
|
|
120
|
+
const exchange = this.pendingExchange;
|
|
121
|
+
const tradeAccountId = this.pendingTradeAccountId;
|
|
122
|
+
this.credentials.getOrFetch(tradeAccountId).then((creds) => this.startWs(exchange, creds)).catch((err) => console.warn("[AccountManager] Failed to start WS:", err));
|
|
123
|
+
}
|
|
124
|
+
async startWs(exchange, creds) {
|
|
125
|
+
const account = new ExchangeAccount({ exchange, demo: this.demo });
|
|
126
|
+
await account.connect(creds);
|
|
127
|
+
this.activeAccount = account;
|
|
128
|
+
for (const cb of this.balanceListeners.values()) {
|
|
129
|
+
account.trackBalance(cb);
|
|
130
|
+
}
|
|
131
|
+
for (const cb of this.positionListeners.values()) {
|
|
132
|
+
account.trackPositions(cb);
|
|
133
|
+
}
|
|
134
|
+
for (const { coin, cb } of this.assetListeners.values()) {
|
|
135
|
+
account.trackAsset(coin, cb);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { AccountManager };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { AccountConfig, SupportedCredentials, BalanceCallback, AccountBalance, PositionsCallback, AccountPosition, TrackedAssetCallback, TrackedAssetInfo, ExchangeState } from './types.js';
|
|
3
|
+
|
|
4
|
+
declare class ExchangeAccount {
|
|
5
|
+
private exchange;
|
|
6
|
+
private demo;
|
|
7
|
+
private orchestrator;
|
|
8
|
+
private state;
|
|
9
|
+
private balanceListeners;
|
|
10
|
+
private positionListeners;
|
|
11
|
+
private assetListeners;
|
|
12
|
+
private onAuthError?;
|
|
13
|
+
constructor(config: AccountConfig);
|
|
14
|
+
connect(credentials: SupportedCredentials): Promise<void>;
|
|
15
|
+
disconnect(): void;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
get isConnected(): boolean;
|
|
18
|
+
get isInitialized(): boolean;
|
|
19
|
+
get hasListeners(): boolean;
|
|
20
|
+
trackBalance(cb: BalanceCallback): string;
|
|
21
|
+
untrackBalance(id: string): void;
|
|
22
|
+
getBalance(): AccountBalance | null;
|
|
23
|
+
trackPositions(cb: PositionsCallback): string;
|
|
24
|
+
untrackPositions(id: string): void;
|
|
25
|
+
getPositions(): AccountPosition[];
|
|
26
|
+
trackAsset(coin: string, cb: TrackedAssetCallback): string;
|
|
27
|
+
untrackAsset(id: string): void;
|
|
28
|
+
getTrackedAsset(coin: string): TrackedAssetInfo | null;
|
|
29
|
+
getLeverage(symbol: string): string | null;
|
|
30
|
+
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
31
|
+
getState(): Readonly<ExchangeState>;
|
|
32
|
+
getExchange(): Connector;
|
|
33
|
+
private applyStateUpdate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { ExchangeAccount };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createOrchestrator } from './exchanges';
|
|
2
|
+
import { createEmptyState } from './utils';
|
|
3
|
+
|
|
4
|
+
class ExchangeAccount {
|
|
5
|
+
exchange;
|
|
6
|
+
demo;
|
|
7
|
+
orchestrator = null;
|
|
8
|
+
state;
|
|
9
|
+
balanceListeners = /* @__PURE__ */ new Map();
|
|
10
|
+
positionListeners = /* @__PURE__ */ new Map();
|
|
11
|
+
assetListeners = /* @__PURE__ */ new Map();
|
|
12
|
+
onAuthError;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.exchange = config.exchange;
|
|
15
|
+
this.demo = config.demo ?? false;
|
|
16
|
+
this.onAuthError = config.onAuthError;
|
|
17
|
+
this.state = createEmptyState();
|
|
18
|
+
}
|
|
19
|
+
async connect(credentials) {
|
|
20
|
+
this.disconnect();
|
|
21
|
+
this.state = createEmptyState();
|
|
22
|
+
this.orchestrator = createOrchestrator(this.exchange, {
|
|
23
|
+
onError: (error) => {
|
|
24
|
+
console.warn(`[ExchangeAccount] ${this.exchange} error:`, error.message);
|
|
25
|
+
if (error.message.includes("auth") || error.message.includes("login")) {
|
|
26
|
+
this.onAuthError?.();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
onStateUpdate: (update) => this.applyStateUpdate(update),
|
|
30
|
+
demo: this.demo
|
|
31
|
+
});
|
|
32
|
+
await this.orchestrator.start(credentials);
|
|
33
|
+
}
|
|
34
|
+
disconnect() {
|
|
35
|
+
if (this.orchestrator) {
|
|
36
|
+
this.orchestrator.stop();
|
|
37
|
+
this.orchestrator = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
destroy() {
|
|
41
|
+
this.disconnect();
|
|
42
|
+
this.balanceListeners.clear();
|
|
43
|
+
this.positionListeners.clear();
|
|
44
|
+
this.assetListeners.clear();
|
|
45
|
+
this.state = createEmptyState();
|
|
46
|
+
}
|
|
47
|
+
get isConnected() {
|
|
48
|
+
return this.orchestrator?.isConnected ?? false;
|
|
49
|
+
}
|
|
50
|
+
get isInitialized() {
|
|
51
|
+
return this.state.initialized;
|
|
52
|
+
}
|
|
53
|
+
get hasListeners() {
|
|
54
|
+
return this.balanceListeners.size > 0 || this.positionListeners.size > 0 || this.assetListeners.size > 0;
|
|
55
|
+
}
|
|
56
|
+
trackBalance(cb) {
|
|
57
|
+
const id = Math.random().toString(36).slice(2);
|
|
58
|
+
this.balanceListeners.set(id, cb);
|
|
59
|
+
if (this.state.balance) {
|
|
60
|
+
try {
|
|
61
|
+
cb(this.state.balance);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
untrackBalance(id) {
|
|
68
|
+
this.balanceListeners.delete(id);
|
|
69
|
+
}
|
|
70
|
+
getBalance() {
|
|
71
|
+
return this.state.balance;
|
|
72
|
+
}
|
|
73
|
+
trackPositions(cb) {
|
|
74
|
+
const id = Math.random().toString(36).slice(2);
|
|
75
|
+
this.positionListeners.set(id, cb);
|
|
76
|
+
if (this.state.initialized) {
|
|
77
|
+
try {
|
|
78
|
+
cb(Array.from(this.state.positions.values()));
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
84
|
+
untrackPositions(id) {
|
|
85
|
+
this.positionListeners.delete(id);
|
|
86
|
+
}
|
|
87
|
+
getPositions() {
|
|
88
|
+
return Array.from(this.state.positions.values());
|
|
89
|
+
}
|
|
90
|
+
trackAsset(coin, cb) {
|
|
91
|
+
const id = Math.random().toString(36).slice(2);
|
|
92
|
+
this.assetListeners.set(id, { coin, cb });
|
|
93
|
+
this.orchestrator?.trackAsset(coin);
|
|
94
|
+
const existing = this.state.trackedAssets.get(coin);
|
|
95
|
+
if (existing) {
|
|
96
|
+
try {
|
|
97
|
+
cb(existing);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
untrackAsset(id) {
|
|
104
|
+
const entry = this.assetListeners.get(id);
|
|
105
|
+
if (!entry) return;
|
|
106
|
+
this.assetListeners.delete(id);
|
|
107
|
+
const hasOtherListeners = Array.from(this.assetListeners.values()).some((l) => l.coin === entry.coin);
|
|
108
|
+
if (!hasOtherListeners) {
|
|
109
|
+
this.orchestrator?.untrackAsset(entry.coin);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
getTrackedAsset(coin) {
|
|
113
|
+
return this.state.trackedAssets.get(coin) ?? null;
|
|
114
|
+
}
|
|
115
|
+
getLeverage(symbol) {
|
|
116
|
+
const setting = this.state.leverageSettings.get(symbol);
|
|
117
|
+
if (setting !== void 0) return setting;
|
|
118
|
+
for (const pos of this.state.positions.values()) {
|
|
119
|
+
if (pos.symbol === symbol) return pos.leverage;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
async setLeverage(symbol, leverage) {
|
|
124
|
+
if (!this.orchestrator) throw new Error("Not connected");
|
|
125
|
+
await this.orchestrator.setLeverage(symbol, leverage);
|
|
126
|
+
}
|
|
127
|
+
getState() {
|
|
128
|
+
return this.state;
|
|
129
|
+
}
|
|
130
|
+
getExchange() {
|
|
131
|
+
return this.exchange;
|
|
132
|
+
}
|
|
133
|
+
// --- private ---
|
|
134
|
+
applyStateUpdate(update) {
|
|
135
|
+
if (update.balance) {
|
|
136
|
+
this.state.balance = update.balance;
|
|
137
|
+
for (const cb of this.balanceListeners.values()) {
|
|
138
|
+
try {
|
|
139
|
+
cb(update.balance);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (update.positions) {
|
|
145
|
+
for (const [key, pos] of update.positions) {
|
|
146
|
+
if (pos.size === "0") {
|
|
147
|
+
this.state.positions.delete(key);
|
|
148
|
+
} else {
|
|
149
|
+
this.state.positions.set(key, pos);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const positions = Array.from(this.state.positions.values());
|
|
153
|
+
for (const cb of this.positionListeners.values()) {
|
|
154
|
+
try {
|
|
155
|
+
cb(positions);
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (update.leverageSettings) {
|
|
161
|
+
for (const [symbol, lev] of update.leverageSettings) {
|
|
162
|
+
this.state.leverageSettings.set(symbol, lev);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (update.trackedAssets) {
|
|
166
|
+
for (const [coin, info] of update.trackedAssets) {
|
|
167
|
+
this.state.trackedAssets.set(coin, info);
|
|
168
|
+
for (const listener of this.assetListeners.values()) {
|
|
169
|
+
if (listener.coin === coin) {
|
|
170
|
+
try {
|
|
171
|
+
listener.cb(info);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.state.lastUpdated = Date.now();
|
|
179
|
+
if (!this.state.initialized) {
|
|
180
|
+
this.state.initialized = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { ExchangeAccount };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SupportedCredentials, StateUpdate } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
type StateUpdateHandler = (update: StateUpdate) => void;
|
|
5
|
+
type ErrorHandler = (error: Error) => void;
|
|
6
|
+
type OrchestratorConfig = {
|
|
7
|
+
onStateUpdate: StateUpdateHandler;
|
|
8
|
+
onError: ErrorHandler;
|
|
9
|
+
demo: boolean;
|
|
10
|
+
};
|
|
11
|
+
interface Orchestrator {
|
|
12
|
+
start: (credentials: SupportedCredentials) => Promise<void>;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
isConnected: boolean;
|
|
15
|
+
onAssetTracked: (coin: string) => void;
|
|
16
|
+
onAssetUntracked: (coin: string) => void;
|
|
17
|
+
setLeverage: (symbol: string, leverage: string) => Promise<void>;
|
|
18
|
+
trackAsset: (coin: string) => void;
|
|
19
|
+
untrackAsset: (coin: string) => boolean;
|
|
20
|
+
getTrackedCoins: () => ReadonlySet<string>;
|
|
21
|
+
}
|
|
22
|
+
declare abstract class BaseOrchestrator implements Orchestrator {
|
|
23
|
+
protected onStateUpdate: StateUpdateHandler;
|
|
24
|
+
protected onError: ErrorHandler;
|
|
25
|
+
protected demo: boolean;
|
|
26
|
+
protected credentials: SupportedCredentials | null;
|
|
27
|
+
protected manualClose: boolean;
|
|
28
|
+
protected reconnectAttempts: number;
|
|
29
|
+
protected reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
30
|
+
protected trackedCoins: Set<string>;
|
|
31
|
+
protected startGeneration: number;
|
|
32
|
+
private starting;
|
|
33
|
+
constructor(config: OrchestratorConfig);
|
|
34
|
+
abstract start(credentials: SupportedCredentials): Promise<void>;
|
|
35
|
+
abstract stop(): void;
|
|
36
|
+
abstract get isConnected(): boolean;
|
|
37
|
+
abstract onAssetTracked(coin: string): void;
|
|
38
|
+
abstract onAssetUntracked(coin: string): void;
|
|
39
|
+
abstract setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
40
|
+
trackAsset(coin: string): void;
|
|
41
|
+
untrackAsset(coin: string): boolean;
|
|
42
|
+
getTrackedCoins(): ReadonlySet<string>;
|
|
43
|
+
protected canUntrackAsset(_coin: string): boolean;
|
|
44
|
+
protected scheduleReconnect(): void;
|
|
45
|
+
protected clearReconnectTimer(): void;
|
|
46
|
+
private guardedStart;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { BaseOrchestrator, type Orchestrator, type OrchestratorConfig };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { reconnectDelay, clearTimer } from '../../utils/ws';
|
|
2
|
+
|
|
3
|
+
class BaseOrchestrator {
|
|
4
|
+
onStateUpdate;
|
|
5
|
+
onError;
|
|
6
|
+
demo;
|
|
7
|
+
credentials = null;
|
|
8
|
+
manualClose = false;
|
|
9
|
+
reconnectAttempts = 0;
|
|
10
|
+
reconnectTimer = null;
|
|
11
|
+
trackedCoins = /* @__PURE__ */ new Set();
|
|
12
|
+
startGeneration = 0;
|
|
13
|
+
starting = false;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.onStateUpdate = config.onStateUpdate;
|
|
16
|
+
this.onError = config.onError;
|
|
17
|
+
this.demo = config.demo;
|
|
18
|
+
}
|
|
19
|
+
trackAsset(coin) {
|
|
20
|
+
if (this.trackedCoins.has(coin)) return;
|
|
21
|
+
this.trackedCoins.add(coin);
|
|
22
|
+
if (this.isConnected) {
|
|
23
|
+
this.onAssetTracked(coin);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
untrackAsset(coin) {
|
|
27
|
+
if (!this.trackedCoins.has(coin)) return true;
|
|
28
|
+
if (!this.canUntrackAsset(coin)) return false;
|
|
29
|
+
this.trackedCoins.delete(coin);
|
|
30
|
+
if (this.isConnected) {
|
|
31
|
+
this.onAssetUntracked(coin);
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
getTrackedCoins() {
|
|
36
|
+
return this.trackedCoins;
|
|
37
|
+
}
|
|
38
|
+
canUntrackAsset(_coin) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
scheduleReconnect() {
|
|
42
|
+
if (this.manualClose || !this.credentials) return;
|
|
43
|
+
this.reconnectAttempts += 1;
|
|
44
|
+
const delay = reconnectDelay(this.reconnectAttempts);
|
|
45
|
+
this.reconnectTimer = setTimeout(() => {
|
|
46
|
+
if (this.credentials && !this.starting) {
|
|
47
|
+
this.guardedStart(this.credentials);
|
|
48
|
+
}
|
|
49
|
+
}, delay);
|
|
50
|
+
}
|
|
51
|
+
clearReconnectTimer() {
|
|
52
|
+
this.reconnectTimer = clearTimer(this.reconnectTimer);
|
|
53
|
+
}
|
|
54
|
+
guardedStart(credentials) {
|
|
55
|
+
if (this.starting) return;
|
|
56
|
+
this.starting = true;
|
|
57
|
+
this.start(credentials).catch((err) => this.onError(err instanceof Error ? err : new Error(String(err)))).finally(() => {
|
|
58
|
+
this.starting = false;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { BaseOrchestrator };
|