@temple-digital-group/temple-canton-js 2.0.0-beta.4 → 2.0.0-beta.5
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 +162 -60
- package/dist/canton/helpers.js +2 -2
- package/dist/canton/withdrawals.js +4 -2
- package/dist/websocket/index.d.ts +70 -0
- package/dist/websocket/index.js +295 -0
- package/index.js +3 -0
- package/package.json +4 -2
- package/src/api/tokenStore.ts +30 -30
- package/src/canton/helpers.ts +2 -2
- package/src/canton/index.js +10 -5
- package/src/canton/instrumentCatalog.d.ts +1 -0
- package/src/canton/instrumentCatalog.js +9 -0
- package/src/canton/withdrawals.ts +4 -2
- package/src/config/index.d.ts +1 -0
- package/src/config/index.js +7 -0
- package/src/websocket/index.ts +341 -0
- package/src/websocket/ws.d.ts +24 -0
package/README.md
CHANGED
|
@@ -45,11 +45,11 @@ setWalletAdapter(loop);
|
|
|
45
45
|
|
|
46
46
|
## Supported Instruments
|
|
47
47
|
|
|
48
|
-
| Asset
|
|
49
|
-
|
|
|
50
|
-
| CC
|
|
51
|
-
| USDCx
|
|
52
|
-
| CBTC
|
|
48
|
+
| Asset | Type | Networks |
|
|
49
|
+
| ----- | ----------- | ---------------- |
|
|
50
|
+
| CC | Canton Coin | testnet, mainnet |
|
|
51
|
+
| USDCx | Utility | testnet, mainnet |
|
|
52
|
+
| CBTC | Utility | testnet, mainnet |
|
|
53
53
|
|
|
54
54
|
> **Symbol normalization:** `CC` is automatically normalized to `Amulet` in all API methods that accept a symbol (e.g. `CC/USDCx` becomes `Amulet/USDCx`).
|
|
55
55
|
|
|
@@ -112,19 +112,19 @@ const result = await depositFunds(depositOpts);
|
|
|
112
112
|
|
|
113
113
|
`depositFunds` accepts:
|
|
114
114
|
|
|
115
|
-
| Option | Required | Description
|
|
116
|
-
| ---------------- | -------- |
|
|
117
|
-
| `sender` | Yes | Party allocating the assets
|
|
118
|
-
| `assetId` | Yes | Instrument symbol (`USDCx`, `Amulet`, `CBTC`)
|
|
119
|
-
| `amount` | Yes | Amount to allocate
|
|
120
|
-
| `holdingCids` | Yes | Holding contract IDs that fund the allocation
|
|
121
|
-
| `receiver` | No | Counterparty (defaults to sender)
|
|
122
|
-
| `settlementId` | No | Reference ID for the settlement
|
|
123
|
-
| `transferLegId` | No | Unique ID for this transfer leg
|
|
124
|
-
| `allocateBefore` | No | ISO timestamp; allocation deadline (default: +1h)
|
|
125
|
-
| `settleBefore` | No | ISO timestamp; settlement deadline (default: +2h)
|
|
126
|
-
| `disclosures` | No | Pre-fetched Amulet disclosures (for FE without API access)
|
|
127
|
-
| `userId` | No | User ID (falls back to wallet adapter / config)
|
|
115
|
+
| Option | Required | Description |
|
|
116
|
+
| ---------------- | -------- | ---------------------------------------------------------- |
|
|
117
|
+
| `sender` | Yes | Party allocating the assets |
|
|
118
|
+
| `assetId` | Yes | Instrument symbol (`USDCx`, `Amulet`, `CBTC`) |
|
|
119
|
+
| `amount` | Yes | Amount to allocate |
|
|
120
|
+
| `holdingCids` | Yes | Holding contract IDs that fund the allocation |
|
|
121
|
+
| `receiver` | No | Counterparty (defaults to sender) |
|
|
122
|
+
| `settlementId` | No | Reference ID for the settlement |
|
|
123
|
+
| `transferLegId` | No | Unique ID for this transfer leg |
|
|
124
|
+
| `allocateBefore` | No | ISO timestamp; allocation deadline (default: +1h) |
|
|
125
|
+
| `settleBefore` | No | ISO timestamp; settlement deadline (default: +2h) |
|
|
126
|
+
| `disclosures` | No | Pre-fetched Amulet disclosures (for FE without API access) |
|
|
127
|
+
| `userId` | No | User ID (falls back to wallet adapter / config) |
|
|
128
128
|
|
|
129
129
|
For Amulet deposits, the SDK fetches disclosures automatically. You can also pass them manually via `opts.disclosures`.
|
|
130
130
|
|
|
@@ -228,8 +228,6 @@ Each entry in the returned array contains:
|
|
|
228
228
|
}
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
|
|
232
|
-
|
|
233
231
|
### Merge Holdings
|
|
234
232
|
|
|
235
233
|
```javascript
|
|
@@ -252,6 +250,94 @@ const res = await walletProvider.submitTransaction(cmd);
|
|
|
252
250
|
const counts = await getUtxoCount(partyId, "USDCx", walletProvider);
|
|
253
251
|
```
|
|
254
252
|
|
|
253
|
+
## WebSocket — Real-Time Data
|
|
254
|
+
|
|
255
|
+
Subscribe to live market data and user events via WebSocket. Works in both Node.js and browsers.
|
|
256
|
+
|
|
257
|
+
The server has two types of data:
|
|
258
|
+
|
|
259
|
+
- **Market data** — public channels you explicitly subscribe to (orderbook, trades, ticker, candles, oracle)
|
|
260
|
+
- **User data** — automatically pushed after authentication, no subscribe needed (orders, trades, balances)
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
import {
|
|
264
|
+
subscribeOrderbook,
|
|
265
|
+
subscribeTrades,
|
|
266
|
+
subscribeTicker,
|
|
267
|
+
subscribeCandles,
|
|
268
|
+
subscribeUserOrders,
|
|
269
|
+
subscribeUserTrades,
|
|
270
|
+
subscribeUserBalances,
|
|
271
|
+
disconnectWebSocket,
|
|
272
|
+
} from "@temple-digital-group/temple-canton-js";
|
|
273
|
+
|
|
274
|
+
// Market data — sends a subscribe message to the server
|
|
275
|
+
const unsub = subscribeOrderbook("Amulet/USDCx", (data) => {
|
|
276
|
+
console.log("Orderbook update:", data);
|
|
277
|
+
});
|
|
278
|
+
subscribeTrades("Amulet/USDCx", (data) => console.log("Trade:", data));
|
|
279
|
+
subscribeTicker("CBTC/USDCx", (data) => console.log("Ticker:", data));
|
|
280
|
+
subscribeCandles("Amulet/USDCx", 60, (data) => console.log("1m candle:", data));
|
|
281
|
+
|
|
282
|
+
// User data — auto-delivered after auth, no subscribe message needed
|
|
283
|
+
subscribeUserOrders((data) => console.log("Order update:", data));
|
|
284
|
+
subscribeUserTrades((data) => console.log("Trade fill:", data));
|
|
285
|
+
subscribeUserBalances((data) => console.log("Balance update:", data));
|
|
286
|
+
|
|
287
|
+
// Unsubscribe from a specific channel
|
|
288
|
+
unsub();
|
|
289
|
+
|
|
290
|
+
// Disconnect everything
|
|
291
|
+
disconnectWebSocket();
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Market Data Channels
|
|
295
|
+
|
|
296
|
+
These require an explicit subscribe message. The SDK handles this automatically.
|
|
297
|
+
|
|
298
|
+
| Function | Channel | Example |
|
|
299
|
+
| ------------------------------------------- | -------------------------------- | ------------------------- |
|
|
300
|
+
| `subscribeOrderbook(symbol, cb)` | `orderbook:{symbol}` | `orderbook:Amulet/USDCx` |
|
|
301
|
+
| `subscribeTrades(symbol, cb)` | `trades:{symbol}` | `trades:Amulet/USDCx` |
|
|
302
|
+
| `subscribeTicker(symbol, cb)` | `ticker:{symbol}` | `ticker:CBTC/USDCx` |
|
|
303
|
+
| `subscribeCandles(symbol, granularity, cb)` | `candles:{symbol}:{granularity}` | `candles:Amulet/USDCx:60`|
|
|
304
|
+
| `subscribeOracle(symbol, cb)` | `oracle:{symbol}` | `oracle:cc` |
|
|
305
|
+
| `subscribeOracleVolume(symbol, cb)` | `oracle_volume:{symbol}` | `oracle_volume:cc` |
|
|
306
|
+
|
|
307
|
+
**Candle granularity values:** `60` (1m), `300` (5m), `900` (15m), `3600` (1h), `14400` (4h), `86400` (1d)
|
|
308
|
+
|
|
309
|
+
### User Data Events
|
|
310
|
+
|
|
311
|
+
Pushed automatically by the server after authentication. No subscribe message is sent — you just register a local handler. Requires `API_KEY` (Node.js) or cookie auth (browser).
|
|
312
|
+
|
|
313
|
+
| Function | Server Event | Description |
|
|
314
|
+
| -------------------------- | -------------- | -------------------------------------------------- |
|
|
315
|
+
| `subscribeUserOrders(cb)` | `user_order` | Order lifecycle updates (created, filled, cancelled)|
|
|
316
|
+
| `subscribeUserTrades(cb)` | `user_trade` | Trade fill confirmations |
|
|
317
|
+
| `subscribeUserBalances(cb)`| `user_balance` | Balance changes |
|
|
318
|
+
|
|
319
|
+
### Advanced Usage
|
|
320
|
+
|
|
321
|
+
Use the `TempleWebSocket` class directly for full control:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
import { TempleWebSocket } from "@temple-digital-group/temple-canton-js";
|
|
325
|
+
|
|
326
|
+
const ws = new TempleWebSocket();
|
|
327
|
+
ws.onConnect = () => console.log("Connected");
|
|
328
|
+
ws.onDisconnect = (code, reason) => console.log("Disconnected:", code, reason);
|
|
329
|
+
ws.onAuth = (success, userId) => console.log("Auth:", success, userId);
|
|
330
|
+
ws.onError = (err) => console.error("WS error:", err);
|
|
331
|
+
ws.autoReconnect = true; // default — reconnects with exponential backoff
|
|
332
|
+
ws.connect();
|
|
333
|
+
|
|
334
|
+
// Market data — sends subscribe to server
|
|
335
|
+
const unsub = ws.subscribe("orderbook:Amulet/USDCx", (data) => { ... });
|
|
336
|
+
|
|
337
|
+
// User data — no subscribe message, just local handler
|
|
338
|
+
const unsubOrder = ws.onUserEvent("user_order", (data) => { ... });
|
|
339
|
+
```
|
|
340
|
+
|
|
255
341
|
## API Reference
|
|
256
342
|
|
|
257
343
|
> Functions marked with **W** support Loop Wallet via the wallet adapter.
|
|
@@ -272,36 +358,36 @@ const counts = await getUtxoCount(partyId, "USDCx", walletProvider);
|
|
|
272
358
|
|
|
273
359
|
### Onboarding & Delegation
|
|
274
360
|
|
|
275
|
-
| Function
|
|
276
|
-
|
|
|
277
|
-
| `isUserOnboarded(party)`
|
|
278
|
-
| `onboardUser(party)`
|
|
279
|
-
| `withdrawDelegation(delegationId?, user?)` | **W**
|
|
361
|
+
| Function | Provider | Description |
|
|
362
|
+
| ------------------------------------------ | -------- | ------------------------------------------------------ |
|
|
363
|
+
| `isUserOnboarded(party)` | **W** | Check if user has a delegation contract on ledger |
|
|
364
|
+
| `onboardUser(party)` | **W** | Create the delegation contract needed for trading |
|
|
365
|
+
| `withdrawDelegation(delegationId?, user?)` | **W** | Archive the delegation contract (user must re-onboard) |
|
|
280
366
|
|
|
281
367
|
### Deposits & Withdrawals
|
|
282
368
|
|
|
283
|
-
| Function
|
|
284
|
-
|
|
|
285
|
-
| `prepareDepositHoldings(amount, assetId)` | **W**
|
|
286
|
-
| `depositFunds(opts)`
|
|
287
|
-
| `withdrawFunds({ asset_id, amount })`
|
|
288
|
-
| `emergencyWithdrawFunds(opts)`
|
|
369
|
+
| Function | Provider | Description |
|
|
370
|
+
| ----------------------------------------- | -------- | ----------------------------------------------------- |
|
|
371
|
+
| `prepareDepositHoldings(amount, assetId)` | **W** | Resolve holdings for a deposit amount |
|
|
372
|
+
| `depositFunds(opts)` | **W** | Deposit funds into the user's trading balance |
|
|
373
|
+
| `withdrawFunds({ asset_id, amount })` | **W** | Withdraw available trading balance back to wallet |
|
|
374
|
+
| `emergencyWithdrawFunds(opts)` | **W** | Cancel all orders and withdraw everything immediately |
|
|
289
375
|
|
|
290
376
|
### Holdings
|
|
291
377
|
|
|
292
|
-
| Function | Provider | Description
|
|
293
|
-
| ----------------------------------------------------------------- | -------- |
|
|
294
|
-
| `getUserBalances(party?, provider?)` | **W** | Get all balances grouped by asset (Amulet, locked, and utility)
|
|
295
|
-
| `getAmuletHoldingsForParty(party, returnCommand, provider)`
|
|
296
|
-
| `getLockedAmuletHoldingsForParty(party, returnCommand, provider)
|
|
297
|
-
| `getUtilityHoldingsForParty(party, returnCommand, provider)`
|
|
298
|
-
| `getUtxoCount(party, assetId, provider)`
|
|
378
|
+
| Function | Provider | Description |
|
|
379
|
+
| ----------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------- |
|
|
380
|
+
| `getUserBalances(party?, provider?)` | **W** | Get all balances grouped by asset (Amulet, locked, and utility) |
|
|
381
|
+
| `getAmuletHoldingsForParty(party, returnCommand, provider)` | **W** | Get Amulet holdings |
|
|
382
|
+
| `getLockedAmuletHoldingsForParty(party, returnCommand, provider)` | **W** | Get locked Amulet holdings |
|
|
383
|
+
| `getUtilityHoldingsForParty(party, returnCommand, provider)` | **W** | Get utility token holdings |
|
|
384
|
+
| `getUtxoCount(party, assetId, provider)` | **W** | Get UTXO summary: counts, largest unlocked amount, and total unlocked balance |
|
|
299
385
|
|
|
300
386
|
### Holding Operations
|
|
301
387
|
|
|
302
|
-
| Function | Provider | Description
|
|
303
|
-
| ------------------------------------------------------------------------------------------ | -------- |
|
|
304
|
-
| `mergeAmuletHoldingsForParty(party, returnCommand, provider, maxUtxos, amuletDisclosures)` | **W** | Merge Amulet holdings into one
|
|
388
|
+
| Function | Provider | Description |
|
|
389
|
+
| ------------------------------------------------------------------------------------------ | -------- | ------------------------------- |
|
|
390
|
+
| `mergeAmuletHoldingsForParty(party, returnCommand, provider, maxUtxos, amuletDisclosures)` | **W** | Merge Amulet holdings into one |
|
|
305
391
|
| `mergeUtilityHoldingsForParty(party, utilityAsset, returnCommand, provider, maxUtxos)` | **W** | Merge utility holdings into one |
|
|
306
392
|
|
|
307
393
|
### Temple REST API
|
|
@@ -310,10 +396,10 @@ const counts = await getUtxoCount(partyId, "USDCx", walletProvider);
|
|
|
310
396
|
|
|
311
397
|
#### Auth
|
|
312
398
|
|
|
313
|
-
| Function
|
|
314
|
-
|
|
|
315
|
-
| `setApiKey(key)`
|
|
316
|
-
| `getUserId()`
|
|
399
|
+
| Function | Description |
|
|
400
|
+
| ---------------- | ------------------------------------------- |
|
|
401
|
+
| `setApiKey(key)` | Set the API key for REST API authentication |
|
|
402
|
+
| `getUserId()` | Get the stored user ID |
|
|
317
403
|
|
|
318
404
|
#### Market Data
|
|
319
405
|
|
|
@@ -327,24 +413,40 @@ const counts = await getUtxoCount(partyId, "USDCx", walletProvider);
|
|
|
327
413
|
|
|
328
414
|
#### Trading
|
|
329
415
|
|
|
330
|
-
| Function
|
|
331
|
-
|
|
|
332
|
-
| `createOrderRequest(opts)`
|
|
333
|
-
| `cancelOrder(orderId)`
|
|
334
|
-
| `cancelAllOrders(options?)`
|
|
335
|
-
| `getTradingBalance()`
|
|
336
|
-
| `getActiveOrders(options?)`
|
|
416
|
+
| Function | Description |
|
|
417
|
+
| --------------------------- | ------------------------------------------------------ |
|
|
418
|
+
| `createOrderRequest(opts)` | Place a buy/sell order via the trading backend |
|
|
419
|
+
| `cancelOrder(orderId)` | Cancel a specific order |
|
|
420
|
+
| `cancelAllOrders(options?)` | Cancel all orders (options: `symbol` filter) |
|
|
421
|
+
| `getTradingBalance()` | Get user's trading balance (unlocked/locked/in-flight) |
|
|
422
|
+
| `getActiveOrders(options?)` | Get active orders (options: `symbol`, `limit`) |
|
|
337
423
|
|
|
338
424
|
#### Withdrawals
|
|
339
425
|
|
|
340
|
-
| Function
|
|
341
|
-
|
|
|
342
|
-
| `createWithdrawalRequest(assetId, amount)`
|
|
343
|
-
| `getWithdrawalRequestStatus(requestId)`
|
|
426
|
+
| Function | Description |
|
|
427
|
+
| ------------------------------------------ | ------------------------------------------ |
|
|
428
|
+
| `createWithdrawalRequest(assetId, amount)` | Submit a withdrawal request to the backend |
|
|
429
|
+
| `getWithdrawalRequestStatus(requestId)` | Poll withdrawal status until ready |
|
|
344
430
|
|
|
345
431
|
#### Disclosures & Delegation
|
|
346
432
|
|
|
347
|
-
| Function | Description
|
|
348
|
-
| ------------------------- |
|
|
349
|
-
| `getDisclosures(partyId)` | Get Amulet disclosure data (factory ID, choice context, disclosed contracts)
|
|
350
|
-
| `getDelegation()` | Get the user's delegation contract from the API
|
|
433
|
+
| Function | Description |
|
|
434
|
+
| ------------------------- | ---------------------------------------------------------------------------- |
|
|
435
|
+
| `getDisclosures(partyId)` | Get Amulet disclosure data (factory ID, choice context, disclosed contracts) |
|
|
436
|
+
| `getDelegation()` | Get the user's delegation contract from the API |
|
|
437
|
+
|
|
438
|
+
### WebSocket
|
|
439
|
+
|
|
440
|
+
| Function | Description |
|
|
441
|
+
| ------------------------------------------- | -------------------------------------------------------------- |
|
|
442
|
+
| `createWebSocket()` | Get or create the shared WS instance (auto-connects) |
|
|
443
|
+
| `disconnectWebSocket()` | Disconnect and destroy the shared WS instance |
|
|
444
|
+
| `subscribeOrderbook(symbol, cb)` | Subscribe to orderbook updates |
|
|
445
|
+
| `subscribeTrades(symbol, cb)` | Subscribe to trade updates |
|
|
446
|
+
| `subscribeTicker(symbol, cb)` | Subscribe to ticker updates |
|
|
447
|
+
| `subscribeCandles(symbol, granularity, cb)` | Subscribe to candle updates |
|
|
448
|
+
| `subscribeOracle(symbol, cb)` | Subscribe to oracle price updates |
|
|
449
|
+
| `subscribeOracleVolume(symbol, cb)` | Subscribe to oracle volume updates |
|
|
450
|
+
| `subscribeUserOrders(cb)` | Listen to user order events (auto-pushed, no subscribe needed) |
|
|
451
|
+
| `subscribeUserTrades(cb)` | Listen to user trade events (auto-pushed, no subscribe needed) |
|
|
452
|
+
| `subscribeUserBalances(cb)` | Listen to user balance events (auto-pushed, no subscribe needed)|
|
package/dist/canton/helpers.js
CHANGED
|
@@ -2,7 +2,7 @@ import config from "../../src/config/index.js";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { getJWTToken } from "../../src/auth0/index.js";
|
|
4
4
|
import { getAdapterProvider } from "../../src/canton/walletAdapter.js";
|
|
5
|
-
import { instrumentCatalog } from "../../src/canton/instrumentCatalog.js";
|
|
5
|
+
import { instrumentCatalog, normalizeAssetId } from "../../src/canton/instrumentCatalog.js";
|
|
6
6
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
7
7
|
export const DEFAULT_AMULET_CONTEXT_KEYS = {
|
|
8
8
|
amuletRules: "amulet-rules",
|
|
@@ -98,7 +98,7 @@ export function resolveProvider(provider) {
|
|
|
98
98
|
export function resolveInstrumentDefinition(assetId) {
|
|
99
99
|
if (!assetId || typeof assetId !== "string")
|
|
100
100
|
return null;
|
|
101
|
-
return instrumentCatalog[assetId
|
|
101
|
+
return instrumentCatalog[normalizeAssetId(assetId)] || null;
|
|
102
102
|
}
|
|
103
103
|
/** Get the registrar for a utility instrument from the catalog. */
|
|
104
104
|
export function getInstrumentRegistrar(assetId) {
|
|
@@ -5,6 +5,7 @@ import { getUserId } from "../api/tokenStore.js";
|
|
|
5
5
|
import { getAdapterPartyId, getWalletAdapter, submitCommand } from "../../src/canton/walletAdapter.js";
|
|
6
6
|
import { randomUUID, shouldUseLedgerForMetadata, normalizeContractId, resolveInstrumentDefinition, getInstrumentRegistrar, dedupeDisclosedContracts, buildHeaders, DEFAULT_AMULET_CONTEXT_KEYS, DEFAULT_UTILITY_CONTEXT_KEYS, } from "./helpers.js";
|
|
7
7
|
import { resolveAmuletContext, resolveUtilityInstrumentConfiguration, resolveUtilityAllocationFactory, } from "../../src/canton/index.js";
|
|
8
|
+
import { normalizeAssetId } from "../../src/canton/instrumentCatalog.js";
|
|
8
9
|
// ─── Withdrawal Functions ────────────────────────────────────────────────────
|
|
9
10
|
/**
|
|
10
11
|
* Finalize a withdrawal by exercising the Allocation_Withdraw choice.
|
|
@@ -260,7 +261,8 @@ export async function finalizeWithdrawFunds(opts, returnCommand = false) {
|
|
|
260
261
|
* 3. Exercises Allocation_Withdraw on the allocation contract to release holdings back to the user.
|
|
261
262
|
*/
|
|
262
263
|
export async function withdrawFunds(opts, returnCommand = false) {
|
|
263
|
-
const {
|
|
264
|
+
const { amount, pollIntervalMs = 2000, maxPollAttempts = 30 } = opts || {};
|
|
265
|
+
const asset_id = normalizeAssetId(opts?.asset_id);
|
|
264
266
|
const sender = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
|
|
265
267
|
if (!sender) {
|
|
266
268
|
const msg = "withdrawFunds: sender party is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID.";
|
|
@@ -309,7 +311,7 @@ export async function withdrawFunds(opts, returnCommand = false) {
|
|
|
309
311
|
}
|
|
310
312
|
// 3. Exercise Allocation_Withdraw to release held funds back to the user
|
|
311
313
|
console.log(`withdrawFunds: exercising Allocation_Withdraw on ${allocationCid}...`);
|
|
312
|
-
const assetId = asset_id
|
|
314
|
+
const assetId = asset_id;
|
|
313
315
|
const withdrawResult = await finalizeWithdrawFunds({ allocationId: allocationCid, sender, assetId }, returnCommand);
|
|
314
316
|
if (withdrawResult?.error) {
|
|
315
317
|
console.error(`withdrawFunds: Allocation_Withdraw failed for request ${requestId}: ${withdrawResult.error}`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type MessageCallback = (data: unknown) => void;
|
|
2
|
+
export type Granularity = 60 | 300 | 900 | 3600 | 14400 | 86400;
|
|
3
|
+
export declare class TempleWebSocket {
|
|
4
|
+
private ws;
|
|
5
|
+
/** Channel subscriptions — require a subscribe message to the server */
|
|
6
|
+
private subscriptions;
|
|
7
|
+
/** User event subscriptions — auto-delivered after auth, no subscribe message needed */
|
|
8
|
+
private userEventSubscribers;
|
|
9
|
+
private reconnectTimer;
|
|
10
|
+
private pingInterval;
|
|
11
|
+
private reconnectAttempts;
|
|
12
|
+
private maxReconnectDelay;
|
|
13
|
+
private authenticated;
|
|
14
|
+
/** Whether to automatically reconnect on close/error */
|
|
15
|
+
autoReconnect: boolean;
|
|
16
|
+
/** Event callbacks */
|
|
17
|
+
onConnect: (() => void) | null;
|
|
18
|
+
onDisconnect: ((code: number, reason: string) => void) | null;
|
|
19
|
+
onError: ((error: unknown) => void) | null;
|
|
20
|
+
onAuth: ((success: boolean, userId?: number) => void) | null;
|
|
21
|
+
get connected(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Connect to the Temple WebSocket server.
|
|
24
|
+
* Auth is handled automatically via header (Node.js) or message (browser).
|
|
25
|
+
*/
|
|
26
|
+
connect(): void;
|
|
27
|
+
/** Disconnect and stop auto-reconnect. */
|
|
28
|
+
disconnect(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to a market data channel. Sends a subscribe message to the server.
|
|
31
|
+
* Returns an unsubscribe function.
|
|
32
|
+
*
|
|
33
|
+
* @param channel - Channel pattern (e.g. "orderbook:Amulet/USDCx", "trades:Amulet/USDCx")
|
|
34
|
+
* @param callback - Called with parsed message data for this channel
|
|
35
|
+
* @returns Unsubscribe function
|
|
36
|
+
*/
|
|
37
|
+
subscribe(channel: string, callback: MessageCallback): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Unsubscribe a callback (or all callbacks) from a market data channel.
|
|
40
|
+
*/
|
|
41
|
+
unsubscribe(channel: string, callback?: MessageCallback): void;
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to user events (auto-delivered after auth, no subscribe message sent).
|
|
44
|
+
* The server pushes these automatically once authenticated.
|
|
45
|
+
* Returns an unsubscribe function.
|
|
46
|
+
*
|
|
47
|
+
* @param event - Event name (e.g. "trade", "order", "balance")
|
|
48
|
+
* @param callback - Called with the event data
|
|
49
|
+
* @returns Unsubscribe function
|
|
50
|
+
*/
|
|
51
|
+
onUserEvent(event: string, callback: MessageCallback): () => void;
|
|
52
|
+
private send;
|
|
53
|
+
private handleMessage;
|
|
54
|
+
private scheduleReconnect;
|
|
55
|
+
private cleanup;
|
|
56
|
+
}
|
|
57
|
+
/** Get or create the shared WebSocket instance. Auto-connects if not already. */
|
|
58
|
+
export declare function createWebSocket(): TempleWebSocket;
|
|
59
|
+
/** Disconnect and destroy the shared WebSocket instance. */
|
|
60
|
+
export declare function disconnectWebSocket(): void;
|
|
61
|
+
export declare function subscribeOrderbook(symbol: string, cb: MessageCallback): () => void;
|
|
62
|
+
export declare function subscribeTrades(symbol: string, cb: MessageCallback): () => void;
|
|
63
|
+
export declare function subscribeTicker(symbol: string, cb: MessageCallback): () => void;
|
|
64
|
+
export declare function subscribeCandles(symbol: string, granularity: Granularity, cb: MessageCallback): () => void;
|
|
65
|
+
export declare function subscribeOracle(symbol: string, cb: MessageCallback): () => void;
|
|
66
|
+
export declare function subscribeOracleVolume(symbol: string, cb: MessageCallback): () => void;
|
|
67
|
+
export declare function subscribeUserOrders(cb: MessageCallback): () => void;
|
|
68
|
+
export declare function subscribeUserTrades(cb: MessageCallback): () => void;
|
|
69
|
+
export declare function subscribeUserBalances(cb: MessageCallback): () => void;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import config from "../../src/config/index.js";
|
|
2
|
+
import NodeWebSocket from "ws";
|
|
3
|
+
// Environment-aware WebSocket constructor
|
|
4
|
+
const WS = typeof WebSocket !== "undefined" ? WebSocket : NodeWebSocket;
|
|
5
|
+
// Whether we're in Node.js (can set headers on upgrade) vs browser (must auth via message)
|
|
6
|
+
const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
7
|
+
/** Normalize CC → Amulet in symbol strings */
|
|
8
|
+
function normalizeSymbol(symbol) {
|
|
9
|
+
return symbol.replace(/\bCC\b/g, "Amulet");
|
|
10
|
+
}
|
|
11
|
+
export class TempleWebSocket {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ws = null;
|
|
14
|
+
/** Channel subscriptions — require a subscribe message to the server */
|
|
15
|
+
this.subscriptions = new Map();
|
|
16
|
+
/** User event subscriptions — auto-delivered after auth, no subscribe message needed */
|
|
17
|
+
this.userEventSubscribers = new Map();
|
|
18
|
+
this.reconnectTimer = null;
|
|
19
|
+
this.pingInterval = null;
|
|
20
|
+
this.reconnectAttempts = 0;
|
|
21
|
+
this.maxReconnectDelay = 30000;
|
|
22
|
+
this.authenticated = false;
|
|
23
|
+
/** Whether to automatically reconnect on close/error */
|
|
24
|
+
this.autoReconnect = true;
|
|
25
|
+
/** Event callbacks */
|
|
26
|
+
this.onConnect = null;
|
|
27
|
+
this.onDisconnect = null;
|
|
28
|
+
this.onError = null;
|
|
29
|
+
this.onAuth = null;
|
|
30
|
+
}
|
|
31
|
+
get connected() {
|
|
32
|
+
return this.ws?.readyState === WS.OPEN;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Connect to the Temple WebSocket server.
|
|
36
|
+
* Auth is handled automatically via header (Node.js) or message (browser).
|
|
37
|
+
*/
|
|
38
|
+
connect() {
|
|
39
|
+
if (this.ws && (this.ws.readyState === WS.OPEN || this.ws.readyState === WS.CONNECTING)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const url = config.WS_URL;
|
|
43
|
+
const apiKey = config.API_KEY;
|
|
44
|
+
if (isNode && apiKey) {
|
|
45
|
+
// Node.js: auth via HTTP upgrade header
|
|
46
|
+
this.ws = new WS(url, { headers: { "X-API-Key": apiKey } });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.ws = new WS(url);
|
|
50
|
+
}
|
|
51
|
+
this.ws.onopen = () => {
|
|
52
|
+
this.reconnectAttempts = 0;
|
|
53
|
+
// Browser: auth via message after connect
|
|
54
|
+
if (!isNode && apiKey) {
|
|
55
|
+
this.send({ type: "auth", api_key: apiKey });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.authenticated = true;
|
|
59
|
+
}
|
|
60
|
+
// Re-subscribe to all active channels (market data only, not user events)
|
|
61
|
+
const channels = [...this.subscriptions.keys()];
|
|
62
|
+
if (channels.length > 0) {
|
|
63
|
+
this.send({ type: "subscribe", channels });
|
|
64
|
+
}
|
|
65
|
+
// Keepalive ping every 30s
|
|
66
|
+
this.pingInterval = setInterval(() => {
|
|
67
|
+
if (this.connected) {
|
|
68
|
+
this.send({ type: "ping" });
|
|
69
|
+
}
|
|
70
|
+
}, 30000);
|
|
71
|
+
this.onConnect?.();
|
|
72
|
+
};
|
|
73
|
+
this.ws.onmessage = (event) => {
|
|
74
|
+
this.handleMessage(typeof event.data === "string" ? event.data : String(event.data));
|
|
75
|
+
};
|
|
76
|
+
this.ws.onclose = (event) => {
|
|
77
|
+
this.cleanup();
|
|
78
|
+
this.onDisconnect?.(event.code, event.reason);
|
|
79
|
+
if (this.autoReconnect) {
|
|
80
|
+
this.scheduleReconnect();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
this.ws.onerror = (event) => {
|
|
84
|
+
this.onError?.(event);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Disconnect and stop auto-reconnect. */
|
|
88
|
+
disconnect() {
|
|
89
|
+
this.autoReconnect = false;
|
|
90
|
+
this.cleanup();
|
|
91
|
+
if (this.ws) {
|
|
92
|
+
this.ws.close(1000, "client disconnect");
|
|
93
|
+
this.ws = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Subscribe to a market data channel. Sends a subscribe message to the server.
|
|
98
|
+
* Returns an unsubscribe function.
|
|
99
|
+
*
|
|
100
|
+
* @param channel - Channel pattern (e.g. "orderbook:Amulet/USDCx", "trades:Amulet/USDCx")
|
|
101
|
+
* @param callback - Called with parsed message data for this channel
|
|
102
|
+
* @returns Unsubscribe function
|
|
103
|
+
*/
|
|
104
|
+
subscribe(channel, callback) {
|
|
105
|
+
if (!this.subscriptions.has(channel)) {
|
|
106
|
+
this.subscriptions.set(channel, new Set());
|
|
107
|
+
// Send subscribe if already connected
|
|
108
|
+
if (this.connected) {
|
|
109
|
+
this.send({ type: "subscribe", channels: [channel] });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.subscriptions.get(channel).add(callback);
|
|
113
|
+
// Return unsubscribe function
|
|
114
|
+
return () => this.unsubscribe(channel, callback);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Unsubscribe a callback (or all callbacks) from a market data channel.
|
|
118
|
+
*/
|
|
119
|
+
unsubscribe(channel, callback) {
|
|
120
|
+
const callbacks = this.subscriptions.get(channel);
|
|
121
|
+
if (!callbacks)
|
|
122
|
+
return;
|
|
123
|
+
if (callback) {
|
|
124
|
+
callbacks.delete(callback);
|
|
125
|
+
}
|
|
126
|
+
if (!callback || callbacks.size === 0) {
|
|
127
|
+
this.subscriptions.delete(channel);
|
|
128
|
+
if (this.connected) {
|
|
129
|
+
this.send({ type: "unsubscribe", channels: [channel] });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Subscribe to user events (auto-delivered after auth, no subscribe message sent).
|
|
135
|
+
* The server pushes these automatically once authenticated.
|
|
136
|
+
* Returns an unsubscribe function.
|
|
137
|
+
*
|
|
138
|
+
* @param event - Event name (e.g. "trade", "order", "balance")
|
|
139
|
+
* @param callback - Called with the event data
|
|
140
|
+
* @returns Unsubscribe function
|
|
141
|
+
*/
|
|
142
|
+
onUserEvent(event, callback) {
|
|
143
|
+
if (!this.userEventSubscribers.has(event)) {
|
|
144
|
+
this.userEventSubscribers.set(event, new Set());
|
|
145
|
+
}
|
|
146
|
+
this.userEventSubscribers.get(event).add(callback);
|
|
147
|
+
return () => {
|
|
148
|
+
const handlers = this.userEventSubscribers.get(event);
|
|
149
|
+
if (handlers) {
|
|
150
|
+
handlers.delete(callback);
|
|
151
|
+
if (handlers.size === 0) {
|
|
152
|
+
this.userEventSubscribers.delete(event);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
send(msg) {
|
|
158
|
+
if (this.ws && this.ws.readyState === WS.OPEN) {
|
|
159
|
+
this.ws.send(JSON.stringify(msg));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
handleMessage(raw) {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(raw);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const type = parsed.type;
|
|
171
|
+
// Handle auth response
|
|
172
|
+
if (type === "authenticated") {
|
|
173
|
+
this.authenticated = true;
|
|
174
|
+
this.onAuth?.(true, parsed.user_id);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (type === "auth_expired") {
|
|
178
|
+
this.authenticated = false;
|
|
179
|
+
this.onAuth?.(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Handle pong
|
|
183
|
+
if (type === "pong")
|
|
184
|
+
return;
|
|
185
|
+
// Handle subscribed/unsubscribed confirmations
|
|
186
|
+
if (type === "subscribed" || type === "unsubscribed")
|
|
187
|
+
return;
|
|
188
|
+
// Handle server errors
|
|
189
|
+
if (type === "error") {
|
|
190
|
+
this.onError?.(parsed);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Route user data (auto-pushed after auth, keyed by event name)
|
|
194
|
+
if (type === "user_data") {
|
|
195
|
+
const event = parsed.event;
|
|
196
|
+
if (event && this.userEventSubscribers.has(event)) {
|
|
197
|
+
const handlers = this.userEventSubscribers.get(event);
|
|
198
|
+
for (const cb of handlers) {
|
|
199
|
+
try {
|
|
200
|
+
cb(parsed.data ?? parsed);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Don't let one bad callback kill the others
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Route channel data (market data from subscriptions)
|
|
210
|
+
if (type === "data") {
|
|
211
|
+
const channel = parsed.channel;
|
|
212
|
+
if (channel && this.subscriptions.has(channel)) {
|
|
213
|
+
const callbacks = this.subscriptions.get(channel);
|
|
214
|
+
for (const cb of callbacks) {
|
|
215
|
+
try {
|
|
216
|
+
cb(parsed.data ?? parsed);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Don't let one bad callback kill the others
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
scheduleReconnect() {
|
|
227
|
+
if (this.reconnectTimer)
|
|
228
|
+
return;
|
|
229
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
|
|
230
|
+
this.reconnectAttempts++;
|
|
231
|
+
this.reconnectTimer = setTimeout(() => {
|
|
232
|
+
this.reconnectTimer = null;
|
|
233
|
+
this.connect();
|
|
234
|
+
}, delay);
|
|
235
|
+
}
|
|
236
|
+
cleanup() {
|
|
237
|
+
if (this.pingInterval) {
|
|
238
|
+
clearInterval(this.pingInterval);
|
|
239
|
+
this.pingInterval = null;
|
|
240
|
+
}
|
|
241
|
+
if (this.reconnectTimer) {
|
|
242
|
+
clearTimeout(this.reconnectTimer);
|
|
243
|
+
this.reconnectTimer = null;
|
|
244
|
+
}
|
|
245
|
+
this.authenticated = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ─── Singleton & Convenience API ─────────────────────────────────────────────
|
|
249
|
+
let instance = null;
|
|
250
|
+
/** Get or create the shared WebSocket instance. Auto-connects if not already. */
|
|
251
|
+
export function createWebSocket() {
|
|
252
|
+
if (!instance) {
|
|
253
|
+
instance = new TempleWebSocket();
|
|
254
|
+
}
|
|
255
|
+
if (!instance.connected) {
|
|
256
|
+
instance.connect();
|
|
257
|
+
}
|
|
258
|
+
return instance;
|
|
259
|
+
}
|
|
260
|
+
/** Disconnect and destroy the shared WebSocket instance. */
|
|
261
|
+
export function disconnectWebSocket() {
|
|
262
|
+
if (instance) {
|
|
263
|
+
instance.disconnect();
|
|
264
|
+
instance = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ─── Market Data Subscriptions ───────────────────────────────────────────────
|
|
268
|
+
export function subscribeOrderbook(symbol, cb) {
|
|
269
|
+
return createWebSocket().subscribe(`orderbook:${normalizeSymbol(symbol)}`, cb);
|
|
270
|
+
}
|
|
271
|
+
export function subscribeTrades(symbol, cb) {
|
|
272
|
+
return createWebSocket().subscribe(`trades:${normalizeSymbol(symbol)}`, cb);
|
|
273
|
+
}
|
|
274
|
+
export function subscribeTicker(symbol, cb) {
|
|
275
|
+
return createWebSocket().subscribe(`ticker:${normalizeSymbol(symbol)}`, cb);
|
|
276
|
+
}
|
|
277
|
+
export function subscribeCandles(symbol, granularity, cb) {
|
|
278
|
+
return createWebSocket().subscribe(`candles:${normalizeSymbol(symbol)}:${granularity}`, cb);
|
|
279
|
+
}
|
|
280
|
+
export function subscribeOracle(symbol, cb) {
|
|
281
|
+
return createWebSocket().subscribe(`oracle:${symbol}`, cb);
|
|
282
|
+
}
|
|
283
|
+
export function subscribeOracleVolume(symbol, cb) {
|
|
284
|
+
return createWebSocket().subscribe(`oracle_volume:${symbol}`, cb);
|
|
285
|
+
}
|
|
286
|
+
// ─── User Data Subscriptions (auto-delivered after auth) ─────────────────────
|
|
287
|
+
export function subscribeUserOrders(cb) {
|
|
288
|
+
return createWebSocket().onUserEvent("user_order", cb);
|
|
289
|
+
}
|
|
290
|
+
export function subscribeUserTrades(cb) {
|
|
291
|
+
return createWebSocket().onUserEvent("user_trade", cb);
|
|
292
|
+
}
|
|
293
|
+
export function subscribeUserBalances(cb) {
|
|
294
|
+
return createWebSocket().onUserEvent("user_balance", cb);
|
|
295
|
+
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@temple-digital-group/temple-canton-js",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.5",
|
|
4
4
|
"description": "JavaScript library for interacting with Temple Canton blockchain",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"axios": "^1.13.1",
|
|
41
|
-
"dotenv": "^17.2.3"
|
|
41
|
+
"dotenv": "^17.2.3",
|
|
42
|
+
"ws": "^8.19.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^25.3.3",
|
|
46
|
+
"@types/ws": "^8.18.0",
|
|
45
47
|
"typescript": "^5.9.3"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/src/api/tokenStore.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import config from "../../src/config/index.js";
|
|
2
|
-
import type { AuthHeaders } from "./types.js";
|
|
3
|
-
|
|
4
|
-
let userId: string | null = null;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get the stored user ID, or null if not set.
|
|
8
|
-
*/
|
|
9
|
-
export function getUserId(): string | null {
|
|
10
|
-
return userId;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Set the user ID directly (e.g. from config at init time).
|
|
15
|
-
*/
|
|
16
|
-
export function setUserId(id: string): void {
|
|
17
|
-
userId = id;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get the authorization header using the API key from config.
|
|
22
|
-
* Returns null if no API key is configured.
|
|
23
|
-
*/
|
|
24
|
-
export function getAuthHeader(): AuthHeaders | null {
|
|
25
|
-
const apiKey = config.API_KEY;
|
|
26
|
-
if (apiKey) {
|
|
27
|
-
return { "X-API-Key": apiKey };
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
1
|
+
import config from "../../src/config/index.js";
|
|
2
|
+
import type { AuthHeaders } from "./types.js";
|
|
3
|
+
|
|
4
|
+
let userId: string | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the stored user ID, or null if not set.
|
|
8
|
+
*/
|
|
9
|
+
export function getUserId(): string | null {
|
|
10
|
+
return userId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set the user ID directly (e.g. from config at init time).
|
|
15
|
+
*/
|
|
16
|
+
export function setUserId(id: string): void {
|
|
17
|
+
userId = id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the authorization header using the API key from config.
|
|
22
|
+
* Returns null if no API key is configured.
|
|
23
|
+
*/
|
|
24
|
+
export function getAuthHeader(): AuthHeaders | null {
|
|
25
|
+
const apiKey = config.API_KEY;
|
|
26
|
+
if (apiKey) {
|
|
27
|
+
return { "X-API-Key": apiKey };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
package/src/canton/helpers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import config from "../../src/config/index.js";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { getJWTToken } from "../../src/auth0/index.js";
|
|
4
4
|
import { getAdapterProvider } from "../../src/canton/walletAdapter.js";
|
|
5
|
-
import { instrumentCatalog } from "../../src/canton/instrumentCatalog.js";
|
|
5
|
+
import { instrumentCatalog, normalizeAssetId } from "../../src/canton/instrumentCatalog.js";
|
|
6
6
|
|
|
7
7
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -151,7 +151,7 @@ export function resolveProvider(provider: unknown): unknown {
|
|
|
151
151
|
/** Resolve an instrument definition from the catalog. */
|
|
152
152
|
export function resolveInstrumentDefinition(assetId: string): Record<string, unknown> | null {
|
|
153
153
|
if (!assetId || typeof assetId !== "string") return null;
|
|
154
|
-
return (instrumentCatalog as Record<string, Record<string, unknown>>)[assetId
|
|
154
|
+
return (instrumentCatalog as Record<string, Record<string, unknown>>)[normalizeAssetId(assetId)] || null;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/** Get the registrar for a utility instrument from the catalog. */
|
package/src/canton/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
// Re-export helpers that are part of the public API
|
|
28
28
|
export { resolveInstrumentDefinition, getInstrumentRegistrar };
|
|
29
|
+
export { normalizeAssetId } from "./instrumentCatalog.js";
|
|
29
30
|
|
|
30
31
|
// Import JSON schemas
|
|
31
32
|
import createOrderProposalAmuletSchema from "./request_schemas/create_order_proposal_amulet.json" with { type: "json" };
|
|
@@ -44,7 +45,7 @@ import unlockAmuletSchema from "./request_schemas/unlock_amulet.json" with { typ
|
|
|
44
45
|
import createUtilityCredentialSchema from "./request_schemas/create_utility_credential.json" with { type: "json" };
|
|
45
46
|
|
|
46
47
|
// Import instrument catalog
|
|
47
|
-
import { RegistrarInternalScheme, instrumentCatalog, instrumentIdToSymbol, supportedTradingPairs, supportedSymbols } from "./instrumentCatalog.js";
|
|
48
|
+
import { RegistrarInternalScheme, instrumentCatalog, instrumentIdToSymbol, supportedTradingPairs, supportedSymbols, normalizeAssetId } from "./instrumentCatalog.js";
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -827,6 +828,8 @@ function replaceTemplateVariables(schema, variables) {
|
|
|
827
828
|
* @returns {boolean|null} True if Amulet context is needed, false if not, null if invalid symbol
|
|
828
829
|
*/
|
|
829
830
|
export function checkAmuletContext(baseAsset, quoteAsset, side) {
|
|
831
|
+
baseAsset = normalizeAssetId(baseAsset);
|
|
832
|
+
quoteAsset = normalizeAssetId(quoteAsset);
|
|
830
833
|
const symbolPair = supportedSymbols[`${baseAsset}/${quoteAsset}`];
|
|
831
834
|
if (!symbolPair) {
|
|
832
835
|
return null; // Invalid symbol
|
|
@@ -1596,6 +1599,7 @@ export async function getUtilityHoldingsForParty(party, returnCommand = false, p
|
|
|
1596
1599
|
* @returns {Promise<{total: number, unlocked: number, locked: number, largestUnlocked: number, unlockedBalance: number}>} UTXO summary
|
|
1597
1600
|
*/
|
|
1598
1601
|
export async function getUtxoCount(party, assetId, provider = null) {
|
|
1602
|
+
assetId = normalizeAssetId(assetId);
|
|
1599
1603
|
provider = resolveProvider(provider);
|
|
1600
1604
|
const isAmulet = assetId === "Amulet";
|
|
1601
1605
|
const holdings = isAmulet ? await getAmuletHoldingsForParty(party, false, provider) : await getUtilityHoldingsForParty(party, false, provider);
|
|
@@ -1770,6 +1774,7 @@ export async function getUtilitySenderCredentials(sender, returnCommand = false)
|
|
|
1770
1774
|
* @returns {Promise<Object>} Holding data with contractId, quantity, dso/registrar, or an error object
|
|
1771
1775
|
*/
|
|
1772
1776
|
export async function getCandidateHoldingForOrderCreation(party, isAmulet, requiredQuantity, expectedAssetId = null, returnCommand = false, provider = null) {
|
|
1777
|
+
if (expectedAssetId) expectedAssetId = normalizeAssetId(expectedAssetId);
|
|
1773
1778
|
provider = resolveProvider(provider);
|
|
1774
1779
|
const holdings = isAmulet ? await getAmuletHoldingsForParty(party, false, provider) : await getUtilityHoldingsForParty(party, false, provider);
|
|
1775
1780
|
|
|
@@ -2166,6 +2171,7 @@ export async function mergeAmuletHoldingsForParty(party, returnCommand = false,
|
|
|
2166
2171
|
* @returns {Promise<Object|null>} Merge result data, or {command, endpoint} when returnCommand is true, or null on failure
|
|
2167
2172
|
*/
|
|
2168
2173
|
export async function mergeUtilityHoldingsForParty(party, utilityAsset, returnCommand = false, provider = null, maxUtxos = null) {
|
|
2174
|
+
utilityAsset = normalizeAssetId(utilityAsset);
|
|
2169
2175
|
provider = resolveProvider(provider);
|
|
2170
2176
|
if (provider) {
|
|
2171
2177
|
returnCommand = true;
|
|
@@ -2651,9 +2657,7 @@ export async function prepareDepositHoldings(amount, symbol) {
|
|
|
2651
2657
|
return { error: "prepareDepositHoldings: party ID is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID." };
|
|
2652
2658
|
}
|
|
2653
2659
|
|
|
2654
|
-
|
|
2655
|
-
let assetId = symbol?.trim();
|
|
2656
|
-
if (assetId === "CC") assetId = "Amulet";
|
|
2660
|
+
let assetId = normalizeAssetId(symbol);
|
|
2657
2661
|
|
|
2658
2662
|
if (!instrumentCatalog[assetId]) {
|
|
2659
2663
|
return { error: `prepareDepositHoldings: unsupported symbol "${symbol}"` };
|
|
@@ -2760,7 +2764,8 @@ export async function prepareDepositHoldings(amount, symbol) {
|
|
|
2760
2764
|
* @returns {Promise<Object|null>} Ledger API response (or { error } on failure).
|
|
2761
2765
|
*/
|
|
2762
2766
|
export async function depositFunds(opts, returnCommand = false) {
|
|
2763
|
-
const { sender, receiver,
|
|
2767
|
+
const { sender, receiver, amount, holdingCids = [], settlementId, transferLegId, allocateBefore, settleBefore, disclosures, userId } = opts;
|
|
2768
|
+
const assetId = normalizeAssetId(opts.assetId);
|
|
2764
2769
|
|
|
2765
2770
|
if (!sender || !assetId || !amount) {
|
|
2766
2771
|
const msg = "depositFunds: sender, assetId, and amount are required";
|
|
@@ -3,3 +3,4 @@ export const instrumentCatalog: Record<string, Record<string, unknown>>;
|
|
|
3
3
|
export const instrumentIdToSymbol: Record<string, string>;
|
|
4
4
|
export const supportedTradingPairs: Array<{ baseAsset: string; quoteAsset: string }>;
|
|
5
5
|
export const supportedSymbols: Record<string, unknown>;
|
|
6
|
+
export function normalizeAssetId(symbol: string): string;
|
|
@@ -172,3 +172,12 @@ export const supportedSymbols = supportedTradingPairs.reduce((acc, pair) => {
|
|
|
172
172
|
|
|
173
173
|
// Add CC as an alias for Amulet so users can pass "CC/USDCx"
|
|
174
174
|
supportedSymbols["CC/USDCx"] = supportedSymbols["Amulet/USDCx"];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Normalize asset aliases to their canonical names.
|
|
178
|
+
* "CC" (any case) → "Amulet". Also works inside trading pairs: "CC/USDCx" → "Amulet/USDCx".
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeAssetId(symbol) {
|
|
181
|
+
if (!symbol || typeof symbol !== "string") return symbol;
|
|
182
|
+
return symbol.trim().replace(/\bcc\b/gi, "Amulet");
|
|
183
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resolveUtilityInstrumentConfiguration,
|
|
26
26
|
resolveUtilityAllocationFactory,
|
|
27
27
|
} from "../../src/canton/index.js";
|
|
28
|
+
import { normalizeAssetId } from "../../src/canton/instrumentCatalog.js";
|
|
28
29
|
|
|
29
30
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
30
31
|
|
|
@@ -333,7 +334,8 @@ export async function withdrawFunds(
|
|
|
333
334
|
opts: WithdrawFundsOpts,
|
|
334
335
|
returnCommand = false,
|
|
335
336
|
): Promise<Record<string, unknown>> {
|
|
336
|
-
const {
|
|
337
|
+
const { amount, pollIntervalMs = 2000, maxPollAttempts = 30 } = opts || {};
|
|
338
|
+
const asset_id = normalizeAssetId(opts?.asset_id);
|
|
337
339
|
|
|
338
340
|
const sender = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
|
|
339
341
|
if (!sender) {
|
|
@@ -393,7 +395,7 @@ export async function withdrawFunds(
|
|
|
393
395
|
|
|
394
396
|
// 3. Exercise Allocation_Withdraw to release held funds back to the user
|
|
395
397
|
console.log(`withdrawFunds: exercising Allocation_Withdraw on ${allocationCid}...`);
|
|
396
|
-
const assetId = asset_id
|
|
398
|
+
const assetId = asset_id;
|
|
397
399
|
const withdrawResult = await finalizeWithdrawFunds({ allocationId: allocationCid, sender, assetId }, returnCommand);
|
|
398
400
|
|
|
399
401
|
if (withdrawResult?.error) {
|
package/src/config/index.d.ts
CHANGED
package/src/config/index.js
CHANGED
|
@@ -123,6 +123,13 @@ const config = {
|
|
|
123
123
|
get API_PASSWORD() { return getConfigValue("API_PASSWORD"); },
|
|
124
124
|
get API_KEY() { return getConfigValue("API_KEY"); },
|
|
125
125
|
|
|
126
|
+
// WebSocket
|
|
127
|
+
get WS_URL() {
|
|
128
|
+
const network = getConfigValue("NETWORK") || NETWORK_MAINNET;
|
|
129
|
+
if (network === NETWORK_TESTNET) return "wss://ws-dev.templedigitalgroup.com/v1/stream";
|
|
130
|
+
return "wss://ws.templedigitalgroup.com/v1/stream";
|
|
131
|
+
},
|
|
132
|
+
|
|
126
133
|
// Validator connection settings
|
|
127
134
|
get VALIDATOR_API_URL() { return getConfigValue("VALIDATOR_API_URL"); },
|
|
128
135
|
get VALIDATOR_SCAN_API_URL() { return getConfigValue("VALIDATOR_SCAN_API_URL"); },
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import config from "../../src/config/index.js";
|
|
2
|
+
import NodeWebSocket from "ws";
|
|
3
|
+
|
|
4
|
+
// Environment-aware WebSocket constructor
|
|
5
|
+
const WS: typeof globalThis.WebSocket =
|
|
6
|
+
typeof WebSocket !== "undefined" ? WebSocket : (NodeWebSocket as unknown as typeof WebSocket);
|
|
7
|
+
|
|
8
|
+
// Whether we're in Node.js (can set headers on upgrade) vs browser (must auth via message)
|
|
9
|
+
const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
10
|
+
|
|
11
|
+
type MessageCallback = (data: unknown) => void;
|
|
12
|
+
|
|
13
|
+
export type Granularity = 60 | 300 | 900 | 3600 | 14400 | 86400;
|
|
14
|
+
|
|
15
|
+
/** Normalize CC → Amulet in symbol strings */
|
|
16
|
+
function normalizeSymbol(symbol: string): string {
|
|
17
|
+
return symbol.replace(/\bCC\b/g, "Amulet");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TempleWebSocket {
|
|
21
|
+
private ws: InstanceType<typeof WS> | null = null;
|
|
22
|
+
/** Channel subscriptions — require a subscribe message to the server */
|
|
23
|
+
private subscriptions: Map<string, Set<MessageCallback>> = new Map();
|
|
24
|
+
/** User event subscriptions — auto-delivered after auth, no subscribe message needed */
|
|
25
|
+
private userEventSubscribers: Map<string, Set<MessageCallback>> = new Map();
|
|
26
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
+
private pingInterval: ReturnType<typeof setInterval> | null = null;
|
|
28
|
+
private reconnectAttempts = 0;
|
|
29
|
+
private maxReconnectDelay = 30_000;
|
|
30
|
+
private authenticated = false;
|
|
31
|
+
|
|
32
|
+
/** Whether to automatically reconnect on close/error */
|
|
33
|
+
autoReconnect = true;
|
|
34
|
+
|
|
35
|
+
/** Event callbacks */
|
|
36
|
+
onConnect: (() => void) | null = null;
|
|
37
|
+
onDisconnect: ((code: number, reason: string) => void) | null = null;
|
|
38
|
+
onError: ((error: unknown) => void) | null = null;
|
|
39
|
+
onAuth: ((success: boolean, userId?: number) => void) | null = null;
|
|
40
|
+
|
|
41
|
+
get connected(): boolean {
|
|
42
|
+
return this.ws?.readyState === WS.OPEN;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Connect to the Temple WebSocket server.
|
|
47
|
+
* Auth is handled automatically via header (Node.js) or message (browser).
|
|
48
|
+
*/
|
|
49
|
+
connect(): void {
|
|
50
|
+
if (this.ws && (this.ws.readyState === WS.OPEN || this.ws.readyState === WS.CONNECTING)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = config.WS_URL;
|
|
55
|
+
const apiKey = config.API_KEY;
|
|
56
|
+
|
|
57
|
+
if (isNode && apiKey) {
|
|
58
|
+
// Node.js: auth via HTTP upgrade header
|
|
59
|
+
this.ws = new WS(url, { headers: { "X-API-Key": apiKey } } as unknown as string[]);
|
|
60
|
+
} else {
|
|
61
|
+
this.ws = new WS(url);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.ws.onopen = () => {
|
|
65
|
+
this.reconnectAttempts = 0;
|
|
66
|
+
|
|
67
|
+
// Browser: auth via message after connect
|
|
68
|
+
if (!isNode && apiKey) {
|
|
69
|
+
this.send({ type: "auth", api_key: apiKey });
|
|
70
|
+
} else {
|
|
71
|
+
this.authenticated = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Re-subscribe to all active channels (market data only, not user events)
|
|
75
|
+
const channels = [...this.subscriptions.keys()];
|
|
76
|
+
if (channels.length > 0) {
|
|
77
|
+
this.send({ type: "subscribe", channels });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Keepalive ping every 30s
|
|
81
|
+
this.pingInterval = setInterval(() => {
|
|
82
|
+
if (this.connected) {
|
|
83
|
+
this.send({ type: "ping" });
|
|
84
|
+
}
|
|
85
|
+
}, 30_000);
|
|
86
|
+
|
|
87
|
+
this.onConnect?.();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.ws.onmessage = (event: MessageEvent) => {
|
|
91
|
+
this.handleMessage(typeof event.data === "string" ? event.data : String(event.data));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.ws.onclose = (event: CloseEvent) => {
|
|
95
|
+
this.cleanup();
|
|
96
|
+
this.onDisconnect?.(event.code, event.reason);
|
|
97
|
+
if (this.autoReconnect) {
|
|
98
|
+
this.scheduleReconnect();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.ws.onerror = (event: Event) => {
|
|
103
|
+
this.onError?.(event);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Disconnect and stop auto-reconnect. */
|
|
108
|
+
disconnect(): void {
|
|
109
|
+
this.autoReconnect = false;
|
|
110
|
+
this.cleanup();
|
|
111
|
+
if (this.ws) {
|
|
112
|
+
this.ws.close(1000, "client disconnect");
|
|
113
|
+
this.ws = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to a market data channel. Sends a subscribe message to the server.
|
|
119
|
+
* Returns an unsubscribe function.
|
|
120
|
+
*
|
|
121
|
+
* @param channel - Channel pattern (e.g. "orderbook:Amulet/USDCx", "trades:Amulet/USDCx")
|
|
122
|
+
* @param callback - Called with parsed message data for this channel
|
|
123
|
+
* @returns Unsubscribe function
|
|
124
|
+
*/
|
|
125
|
+
subscribe(channel: string, callback: MessageCallback): () => void {
|
|
126
|
+
if (!this.subscriptions.has(channel)) {
|
|
127
|
+
this.subscriptions.set(channel, new Set());
|
|
128
|
+
// Send subscribe if already connected
|
|
129
|
+
if (this.connected) {
|
|
130
|
+
this.send({ type: "subscribe", channels: [channel] });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.subscriptions.get(channel)!.add(callback);
|
|
134
|
+
|
|
135
|
+
// Return unsubscribe function
|
|
136
|
+
return () => this.unsubscribe(channel, callback);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Unsubscribe a callback (or all callbacks) from a market data channel.
|
|
141
|
+
*/
|
|
142
|
+
unsubscribe(channel: string, callback?: MessageCallback): void {
|
|
143
|
+
const callbacks = this.subscriptions.get(channel);
|
|
144
|
+
if (!callbacks) return;
|
|
145
|
+
|
|
146
|
+
if (callback) {
|
|
147
|
+
callbacks.delete(callback);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!callback || callbacks.size === 0) {
|
|
151
|
+
this.subscriptions.delete(channel);
|
|
152
|
+
if (this.connected) {
|
|
153
|
+
this.send({ type: "unsubscribe", channels: [channel] });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Subscribe to user events (auto-delivered after auth, no subscribe message sent).
|
|
160
|
+
* The server pushes these automatically once authenticated.
|
|
161
|
+
* Returns an unsubscribe function.
|
|
162
|
+
*
|
|
163
|
+
* @param event - Event name (e.g. "trade", "order", "balance")
|
|
164
|
+
* @param callback - Called with the event data
|
|
165
|
+
* @returns Unsubscribe function
|
|
166
|
+
*/
|
|
167
|
+
onUserEvent(event: string, callback: MessageCallback): () => void {
|
|
168
|
+
if (!this.userEventSubscribers.has(event)) {
|
|
169
|
+
this.userEventSubscribers.set(event, new Set());
|
|
170
|
+
}
|
|
171
|
+
this.userEventSubscribers.get(event)!.add(callback);
|
|
172
|
+
|
|
173
|
+
return () => {
|
|
174
|
+
const handlers = this.userEventSubscribers.get(event);
|
|
175
|
+
if (handlers) {
|
|
176
|
+
handlers.delete(callback);
|
|
177
|
+
if (handlers.size === 0) {
|
|
178
|
+
this.userEventSubscribers.delete(event);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private send(msg: Record<string, unknown>): void {
|
|
185
|
+
if (this.ws && this.ws.readyState === WS.OPEN) {
|
|
186
|
+
this.ws.send(JSON.stringify(msg));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private handleMessage(raw: string): void {
|
|
191
|
+
let parsed: Record<string, unknown>;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(raw);
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const type = parsed.type as string | undefined;
|
|
199
|
+
|
|
200
|
+
// Handle auth response
|
|
201
|
+
if (type === "authenticated") {
|
|
202
|
+
this.authenticated = true;
|
|
203
|
+
this.onAuth?.(true, parsed.user_id as number | undefined);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (type === "auth_expired") {
|
|
207
|
+
this.authenticated = false;
|
|
208
|
+
this.onAuth?.(false);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Handle pong
|
|
213
|
+
if (type === "pong") return;
|
|
214
|
+
|
|
215
|
+
// Handle subscribed/unsubscribed confirmations
|
|
216
|
+
if (type === "subscribed" || type === "unsubscribed") return;
|
|
217
|
+
|
|
218
|
+
// Handle server errors
|
|
219
|
+
if (type === "error") {
|
|
220
|
+
this.onError?.(parsed);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Route user data (auto-pushed after auth, keyed by event name)
|
|
225
|
+
if (type === "user_data") {
|
|
226
|
+
const event = parsed.event as string | undefined;
|
|
227
|
+
if (event && this.userEventSubscribers.has(event)) {
|
|
228
|
+
const handlers = this.userEventSubscribers.get(event)!;
|
|
229
|
+
for (const cb of handlers) {
|
|
230
|
+
try {
|
|
231
|
+
cb(parsed.data ?? parsed);
|
|
232
|
+
} catch {
|
|
233
|
+
// Don't let one bad callback kill the others
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Route channel data (market data from subscriptions)
|
|
241
|
+
if (type === "data") {
|
|
242
|
+
const channel = parsed.channel as string | undefined;
|
|
243
|
+
if (channel && this.subscriptions.has(channel)) {
|
|
244
|
+
const callbacks = this.subscriptions.get(channel)!;
|
|
245
|
+
for (const cb of callbacks) {
|
|
246
|
+
try {
|
|
247
|
+
cb(parsed.data ?? parsed);
|
|
248
|
+
} catch {
|
|
249
|
+
// Don't let one bad callback kill the others
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private scheduleReconnect(): void {
|
|
258
|
+
if (this.reconnectTimer) return;
|
|
259
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
|
|
260
|
+
this.reconnectAttempts++;
|
|
261
|
+
this.reconnectTimer = setTimeout(() => {
|
|
262
|
+
this.reconnectTimer = null;
|
|
263
|
+
this.connect();
|
|
264
|
+
}, delay);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private cleanup(): void {
|
|
268
|
+
if (this.pingInterval) {
|
|
269
|
+
clearInterval(this.pingInterval);
|
|
270
|
+
this.pingInterval = null;
|
|
271
|
+
}
|
|
272
|
+
if (this.reconnectTimer) {
|
|
273
|
+
clearTimeout(this.reconnectTimer);
|
|
274
|
+
this.reconnectTimer = null;
|
|
275
|
+
}
|
|
276
|
+
this.authenticated = false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Singleton & Convenience API ─────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
let instance: TempleWebSocket | null = null;
|
|
283
|
+
|
|
284
|
+
/** Get or create the shared WebSocket instance. Auto-connects if not already. */
|
|
285
|
+
export function createWebSocket(): TempleWebSocket {
|
|
286
|
+
if (!instance) {
|
|
287
|
+
instance = new TempleWebSocket();
|
|
288
|
+
}
|
|
289
|
+
if (!instance.connected) {
|
|
290
|
+
instance.connect();
|
|
291
|
+
}
|
|
292
|
+
return instance;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Disconnect and destroy the shared WebSocket instance. */
|
|
296
|
+
export function disconnectWebSocket(): void {
|
|
297
|
+
if (instance) {
|
|
298
|
+
instance.disconnect();
|
|
299
|
+
instance = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Market Data Subscriptions ───────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
export function subscribeOrderbook(symbol: string, cb: MessageCallback): () => void {
|
|
306
|
+
return createWebSocket().subscribe(`orderbook:${normalizeSymbol(symbol)}`, cb);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function subscribeTrades(symbol: string, cb: MessageCallback): () => void {
|
|
310
|
+
return createWebSocket().subscribe(`trades:${normalizeSymbol(symbol)}`, cb);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function subscribeTicker(symbol: string, cb: MessageCallback): () => void {
|
|
314
|
+
return createWebSocket().subscribe(`ticker:${normalizeSymbol(symbol)}`, cb);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function subscribeCandles(symbol: string, granularity: Granularity, cb: MessageCallback): () => void {
|
|
318
|
+
return createWebSocket().subscribe(`candles:${normalizeSymbol(symbol)}:${granularity}`, cb);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function subscribeOracle(symbol: string, cb: MessageCallback): () => void {
|
|
322
|
+
return createWebSocket().subscribe(`oracle:${symbol}`, cb);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function subscribeOracleVolume(symbol: string, cb: MessageCallback): () => void {
|
|
326
|
+
return createWebSocket().subscribe(`oracle_volume:${symbol}`, cb);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── User Data Subscriptions (auto-delivered after auth) ─────────────────────
|
|
330
|
+
|
|
331
|
+
export function subscribeUserOrders(cb: MessageCallback): () => void {
|
|
332
|
+
return createWebSocket().onUserEvent("user_order", cb);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function subscribeUserTrades(cb: MessageCallback): () => void {
|
|
336
|
+
return createWebSocket().onUserEvent("user_trade", cb);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function subscribeUserBalances(cb: MessageCallback): () => void {
|
|
340
|
+
return createWebSocket().onUserEvent("user_balance", cb);
|
|
341
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare module "ws" {
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
class WebSocket extends EventEmitter {
|
|
5
|
+
static readonly CONNECTING: 0;
|
|
6
|
+
static readonly OPEN: 1;
|
|
7
|
+
static readonly CLOSING: 2;
|
|
8
|
+
static readonly CLOSED: 3;
|
|
9
|
+
|
|
10
|
+
readonly readyState: number;
|
|
11
|
+
|
|
12
|
+
constructor(address: string, options?: { headers?: Record<string, string> });
|
|
13
|
+
|
|
14
|
+
close(code?: number, reason?: string): void;
|
|
15
|
+
send(data: string | Buffer): void;
|
|
16
|
+
|
|
17
|
+
onopen: ((event: { target: WebSocket }) => void) | null;
|
|
18
|
+
onclose: ((event: { code: number; reason: string; target: WebSocket }) => void) | null;
|
|
19
|
+
onmessage: ((event: { data: unknown; target: WebSocket }) => void) | null;
|
|
20
|
+
onerror: ((event: { error: unknown; target: WebSocket }) => void) | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default WebSocket;
|
|
24
|
+
}
|