@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.
Files changed (48) hide show
  1. package/README.md +187 -0
  2. package/dist/account/account-manager.d.ts +34 -0
  3. package/dist/account/account-manager.js +140 -0
  4. package/dist/account/exchange-account.d.ts +36 -0
  5. package/dist/account/exchange-account.js +185 -0
  6. package/dist/account/exchanges/base.d.ts +49 -0
  7. package/dist/account/exchanges/base.js +63 -0
  8. package/dist/account/exchanges/binance/const.d.ts +10 -0
  9. package/dist/account/exchanges/binance/const.js +10 -0
  10. package/dist/account/exchanges/binance/mapper.d.ts +9 -0
  11. package/dist/account/exchanges/binance/mapper.js +96 -0
  12. package/dist/account/exchanges/binance/orchestrator.d.ts +35 -0
  13. package/dist/account/exchanges/binance/orchestrator.js +232 -0
  14. package/dist/account/exchanges/binance/rest.d.ts +13 -0
  15. package/dist/account/exchanges/binance/rest.js +81 -0
  16. package/dist/account/exchanges/binance/types.d.ts +77 -0
  17. package/dist/account/exchanges/binance/types.js +1 -0
  18. package/dist/account/exchanges/binance/ws.d.ts +21 -0
  19. package/dist/account/exchanges/binance/ws.js +85 -0
  20. package/dist/account/exchanges/bybit/const.d.ts +7 -0
  21. package/dist/account/exchanges/bybit/const.js +7 -0
  22. package/dist/account/exchanges/bybit/mapper.d.ts +11 -0
  23. package/dist/account/exchanges/bybit/mapper.js +106 -0
  24. package/dist/account/exchanges/bybit/orchestrator.d.ts +23 -0
  25. package/dist/account/exchanges/bybit/orchestrator.js +159 -0
  26. package/dist/account/exchanges/bybit/rest.d.ts +11 -0
  27. package/dist/account/exchanges/bybit/rest.js +110 -0
  28. package/dist/account/exchanges/bybit/types.d.ts +59 -0
  29. package/dist/account/exchanges/bybit/types.js +1 -0
  30. package/dist/account/exchanges/bybit/ws.d.ts +18 -0
  31. package/dist/account/exchanges/bybit/ws.js +74 -0
  32. package/dist/account/exchanges/index.d.ts +8 -0
  33. package/dist/account/exchanges/index.js +16 -0
  34. package/dist/account/index.d.ts +8 -0
  35. package/dist/account/index.js +4 -0
  36. package/dist/account/types.d.ts +81 -0
  37. package/dist/account/types.js +1 -0
  38. package/dist/account/utils.d.ts +7 -0
  39. package/dist/account/utils.js +15 -0
  40. package/dist/credentials.d.ts +28 -0
  41. package/dist/credentials.js +46 -0
  42. package/dist/index.d.ts +22 -0
  43. package/dist/index.js +21 -0
  44. package/dist/utils/hmac.d.ts +4 -0
  45. package/dist/utils/hmac.js +17 -0
  46. package/dist/utils/ws.d.ts +7 -0
  47. package/dist/utils/ws.js +25 -0
  48. 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 };