@runesx/api-client 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runesx/api-client",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A Node.js client for interacting with the RunesX platform API and WebSocket",
5
5
  "main": "src/index.mjs",
6
6
  "type": "module",
@@ -13,23 +13,23 @@
13
13
  "test": "jest"
14
14
  },
15
15
  "dependencies": {
16
- "axios": "^1.13.5",
17
- "bignumber.js": "^9.1.2",
16
+ "axios": "^1.13.6",
17
+ "bignumber.js": "^10.0.2",
18
18
  "socket.io-client": "^4.8.3"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@eslint/js": "^9.11.1",
22
22
  "@semantic-release/changelog": "^6.0.3",
23
23
  "@semantic-release/git": "^10.0.1",
24
- "@semantic-release/npm": "^13.1.4",
24
+ "@semantic-release/npm": "^13.1.5",
25
25
  "dotenv": "^17.3.1",
26
26
  "eslint": "^9.11.1",
27
27
  "eslint-plugin-import": "^2.30.0",
28
28
  "eslint-plugin-n": "^17.24.0",
29
29
  "eslint-plugin-promise": "^7.1.0",
30
- "globals": "^17.3.0",
31
- "jest": "^30.2.0",
32
- "nodemon": "^3.1.11",
30
+ "globals": "^17.4.0",
31
+ "jest": "^30.3.0",
32
+ "nodemon": "^3.1.14",
33
33
  "semantic-release": "^25.0.3"
34
34
  },
35
35
  "files": [
package/src/api.mjs CHANGED
@@ -341,6 +341,73 @@ export function createApi(config) {
341
341
  }
342
342
  }
343
343
 
344
+ // ---- Orders endpoints ----
345
+
346
+ async function placeOrder({ pair, side, price, quantity, timeInForce, inverted, idempotencyKey }) {
347
+ try {
348
+ const key = idempotencyKey || randomUUID();
349
+ const body = { pair, side, price, quantity, timeInForce };
350
+ if (inverted !== undefined && inverted !== null) {
351
+ body.inverted = inverted;
352
+ }
353
+ const response = await api.post('/orders', body, {
354
+ headers: { 'x-idempotency-key': key },
355
+ });
356
+ return response.data;
357
+ } catch (error) {
358
+ throw new Error(error.response?.data?.error || 'Failed to place order');
359
+ }
360
+ }
361
+
362
+ async function cancelOrder(orderId, { idempotencyKey } = {}) {
363
+ try {
364
+ const headers = {};
365
+ if (idempotencyKey) {
366
+ headers['x-idempotency-key'] = idempotencyKey;
367
+ }
368
+ const response = await api.delete(`/orders/${encodeURIComponent(orderId)}`, { headers });
369
+ return response.data;
370
+ } catch (error) {
371
+ throw new Error(error.response?.data?.error || 'Failed to cancel order');
372
+ }
373
+ }
374
+
375
+ async function getOrders({ pair, status, limit, offset } = {}) {
376
+ try {
377
+ const params = {};
378
+ if (pair) { params.pair = pair; }
379
+ if (status) { params.status = status; }
380
+ if (limit) { params.limit = limit; }
381
+ if (offset !== undefined && offset !== null) { params.offset = offset; }
382
+ const response = await api.get('/orders', { params });
383
+ return response.data;
384
+ } catch (error) {
385
+ throw new Error(error.response?.data?.error || 'Failed to fetch orders');
386
+ }
387
+ }
388
+
389
+ async function getOrderBookApi(pair, { levels } = {}) {
390
+ try {
391
+ const params = {};
392
+ if (levels) { params.levels = levels; }
393
+ const response = await api.get(`/orders/book/${encodeURIComponent(pair)}`, { params });
394
+ return response.data;
395
+ } catch (error) {
396
+ throw new Error(error.response?.data?.error || 'Failed to fetch order book');
397
+ }
398
+ }
399
+
400
+ async function getTrades(pair, { limit } = {}) {
401
+ try {
402
+ const params = {};
403
+ if (limit) { params.limit = limit; }
404
+ const response = await api.get(`/orders/trades/${encodeURIComponent(pair)}`, { params });
405
+ return response.data;
406
+ } catch (error) {
407
+ throw new Error(error.response?.data?.error || 'Failed to fetch trades');
408
+ }
409
+ }
410
+
344
411
  async function deleteYardMessage(messageId) {
345
412
  try {
346
413
  const response = await api.delete(`/yard/messages/${encodeURIComponent(messageId)}`);
@@ -385,5 +452,11 @@ export function createApi(config) {
385
452
  getTransactionHistory,
386
453
  getUserOperations,
387
454
  deleteYardMessage,
455
+ // Orders
456
+ placeOrder,
457
+ cancelOrder,
458
+ getOrders,
459
+ getOrderBookApi,
460
+ getTrades,
388
461
  };
389
462
  }
package/src/index.mjs CHANGED
@@ -7,6 +7,9 @@ import { getCoins, getCoinByTicker } from './store/coinStore.mjs';
7
7
  import { getChains, getChainByName } from './store/chainStore.mjs';
8
8
  import { getWallets as getWalletsStore, getWalletByTicker } from './store/walletStore.mjs';
9
9
  import { getUserShares, getUserShareByPoolId } from './store/userSharesStore.mjs';
10
+ import { getAllOrderBooks, getOrderBook, getOrderBookPairs, getUserOrders } from './store/orderbookStore.mjs';
11
+ import { getMarkets, getMarketByCoinKey, getMarketByCoins } from './store/marketStore.mjs';
12
+ import { getClobFees } from './store/exchangeConfigStore.mjs';
10
13
  import { waitForStores } from './waitForStores.mjs';
11
14
  import { estimateLiquidityFrontend, checkRunesLiquidityFrontend, calculateShareAmounts, estimateDepositShares } from './utils/liquidityUtils.mjs';
12
15
  import { estimateSwap } from './utils/swapUtils.mjs';
@@ -23,9 +26,9 @@ export function createRunesXClient(options = {}) {
23
26
  throw new Error('API_KEY is required');
24
27
  }
25
28
  socketHandler = setupSocket(config);
26
- const { pools, coins, chains, wallets, userShares } = await waitForStores(socketHandler.socket);
29
+ const { pools, coins, chains, wallets, userShares, orderbooks } = await waitForStores(socketHandler.socket);
27
30
  initialized = true;
28
- return { pools, coins, chains, wallets, userShares };
31
+ return { pools, coins, chains, wallets, userShares, orderbooks };
29
32
  }
30
33
 
31
34
  function ensureInitialized() {
@@ -54,6 +57,12 @@ export function createRunesXClient(options = {}) {
54
57
  getWalletByTicker,
55
58
  getUserShares,
56
59
  getUserShareByPoolId,
60
+ getAllOrderBooks,
61
+ getOrderBook,
62
+ getOrderBookPairs,
63
+ getUserOrders,
64
+ getMarkets,
65
+ getMarketByCoins,
57
66
 
58
67
  // ---- Event callbacks ----
59
68
  on: (event, callback) => {
@@ -133,13 +142,20 @@ export function createRunesXClient(options = {}) {
133
142
  getVolumePool: api.getVolumePool,
134
143
  getBucketsPools: api.getBucketsPools,
135
144
 
145
+ // ---- Orders API (auth required for place/cancel/getOrders, public for book/trades) ----
146
+ placeOrder: api.placeOrder,
147
+ cancelOrder: api.cancelOrder,
148
+ getOrders: api.getOrders,
149
+ getOrderBookApi: api.getOrderBookApi,
150
+ getTrades: api.getTrades,
151
+
136
152
  // ---- Yard (chat) API ----
137
153
  getYardMessages: api.getYardMessages,
138
154
  deleteYardMessage: api.deleteYardMessage,
139
155
 
140
156
  // ---- Client-side estimation utilities ----
141
157
  estimateSwap: (inputCoin, outputCoin, amountIn, maxHops = 6, algorithm = 'dfs') =>
142
- estimateSwap(inputCoin, outputCoin, amountIn, getPools(), getCoins(), maxHops, algorithm),
158
+ estimateSwap(inputCoin, outputCoin, amountIn, getPools(), getCoins(), maxHops, algorithm, getAllOrderBooks(), getUserOrders(), getClobFees(), getMarketByCoinKey()),
143
159
  estimateLiquidityFrontend,
144
160
  estimateDepositShares: ({ pool, amountA, amountB, slippagePercent } = {}) =>
145
161
  estimateDepositShares({ pool, amountA, amountB, slippagePercent }),
package/src/socket.mjs CHANGED
@@ -6,6 +6,9 @@ import { setInitialCoins, updateCoin, resetCoins } from './store/coinStore.mjs';
6
6
  import { setInitialChains, updateChain, resetChains } from './store/chainStore.mjs';
7
7
  import { setInitialWallets, updateWallet, resetWallets } from './store/walletStore.mjs';
8
8
  import { setInitialUserShares, updateUserShare, resetUserShares } from './store/userSharesStore.mjs';
9
+ import { setInitialOrderBooks, updateOrderBook, resetOrderBooks, setUserOrders, updateUserOrder } from './store/orderbookStore.mjs';
10
+ import { setInitialMarkets, addOrUpdateMarket, resetMarkets } from './store/marketStore.mjs';
11
+ import { setExchangeConfig } from './store/exchangeConfigStore.mjs';
9
12
 
10
13
  export function setupSocket(config) {
11
14
  const socket = io(config.socketUrl, {
@@ -53,6 +56,8 @@ export function setupSocket(config) {
53
56
  resetChains();
54
57
  resetWallets();
55
58
  resetUserShares();
59
+ resetOrderBooks();
60
+ resetMarkets();
56
61
  }
57
62
  emit('connect_error', err);
58
63
  });
@@ -65,6 +70,7 @@ export function setupSocket(config) {
65
70
  resetChains();
66
71
  resetWallets();
67
72
  resetUserShares();
73
+ resetMarkets();
68
74
  emit('disconnect', reason);
69
75
  });
70
76
 
@@ -86,6 +92,7 @@ export function setupSocket(config) {
86
92
  resetChains();
87
93
  resetWallets();
88
94
  resetUserShares();
95
+ resetOrderBooks();
89
96
  }
90
97
  emit('reconnect_error', err);
91
98
  });
@@ -97,6 +104,26 @@ export function setupSocket(config) {
97
104
 
98
105
  // ---- Public room events ----
99
106
 
107
+ socket.on('exchange_config', (config) => {
108
+ setExchangeConfig(config);
109
+ emit('exchange_config', config);
110
+ });
111
+
112
+ socket.on('markets_initial', (data) => {
113
+ setInitialMarkets(data);
114
+ emit('markets_initial', data);
115
+ });
116
+
117
+ socket.on('market_created', (data) => {
118
+ addOrUpdateMarket(data);
119
+ emit('market_created', data);
120
+ });
121
+
122
+ socket.on('market_updated', (data) => {
123
+ addOrUpdateMarket(data);
124
+ emit('market_updated', data);
125
+ });
126
+
100
127
  socket.on('pools_updated', ({ pools, isInitial }) => {
101
128
  if (isInitial) {
102
129
  setInitialPools(pools);
@@ -140,6 +167,32 @@ export function setupSocket(config) {
140
167
  emit('status_updated', data);
141
168
  });
142
169
 
170
+ socket.on('orderbooks_initial', ({ books, isInitial }) => {
171
+ if (isInitial) {
172
+ setInitialOrderBooks(books);
173
+ }
174
+ emit('orderbooks_initial', { books, isInitial });
175
+ });
176
+
177
+ socket.on('orderbook_updated', ({ pair, bids, asks }) => {
178
+ updateOrderBook(pair, bids, asks);
179
+ emit('orderbook_updated', { pair, bids, asks });
180
+ });
181
+
182
+ socket.on('user_orders_initial', (orders) => {
183
+ setUserOrders(orders);
184
+ emit('user_orders_initial', orders);
185
+ });
186
+
187
+ socket.on('order_updated', (data) => {
188
+ updateUserOrder(data);
189
+ emit('order_updated', data);
190
+ });
191
+
192
+ socket.on('clob_trade', (trade) => {
193
+ emit('clob_trade', trade);
194
+ });
195
+
143
196
  socket.on('volumeUpdate', (data) => {
144
197
  emit('volumeUpdate', data);
145
198
  });
@@ -1,4 +1,4 @@
1
- import { BigNumber } from 'bignumber.js';
1
+ import { SafeBigNumber as BigNumber } from '../utils/safeBigNumber.mjs';
2
2
 
3
3
  const coinStore = {
4
4
  coins: new Map(), // Store coin data by coin ID
@@ -0,0 +1,48 @@
1
+ // src/store/exchangeConfigStore.mjs
2
+ // Exchange configuration received from the backend via socket.
3
+ // Keeps fee rates, depth limits, etc. in sync without hardcoding.
4
+
5
+ const exchangeConfigStore = {
6
+ clobFees: {
7
+ takerFeeRate: '0.002', // sensible default until backend config arrives
8
+ makerFeeRate: '0.001',
9
+ maxFillBatch: 50,
10
+ maxFillTotal: 500,
11
+ },
12
+ };
13
+
14
+ const setExchangeConfig = (config) => {
15
+ const { clobFees } = config;
16
+ if (clobFees) {
17
+ if (clobFees.takerFeeRate !== null && clobFees.takerFeeRate !== undefined) {
18
+ exchangeConfigStore.clobFees.takerFeeRate = clobFees.takerFeeRate;
19
+ }
20
+ if (clobFees.makerFeeRate !== null && clobFees.makerFeeRate !== undefined) {
21
+ exchangeConfigStore.clobFees.makerFeeRate = clobFees.makerFeeRate;
22
+ }
23
+ if (clobFees.maxFillBatch !== null && clobFees.maxFillBatch !== undefined) {
24
+ exchangeConfigStore.clobFees.maxFillBatch = clobFees.maxFillBatch;
25
+ }
26
+ if (clobFees.maxFillTotal !== null && clobFees.maxFillTotal !== undefined) {
27
+ exchangeConfigStore.clobFees.maxFillTotal = clobFees.maxFillTotal;
28
+ }
29
+ }
30
+ };
31
+
32
+ const getClobFees = () => exchangeConfigStore.clobFees;
33
+
34
+ const resetExchangeConfig = () => {
35
+ exchangeConfigStore.clobFees = {
36
+ takerFeeRate: '0.002',
37
+ makerFeeRate: '0.001',
38
+ maxFillBatch: 50,
39
+ maxFillTotal: 500,
40
+ };
41
+ };
42
+
43
+ export {
44
+ exchangeConfigStore,
45
+ setExchangeConfig,
46
+ getClobFees,
47
+ resetExchangeConfig,
48
+ };
@@ -0,0 +1,57 @@
1
+ // src/store/marketStore.mjs
2
+ // Stores admin-defined markets received from the backend via socket.
3
+ // Markets define the canonical pair direction (e.g., "RUNES-DOG" not "DOG-RUNES").
4
+
5
+ let markets = [];
6
+ let byCoinKey = {};
7
+
8
+ function buildCoinKey(coinIdA, coinIdB) {
9
+ return coinIdA < coinIdB ? `${coinIdA}|${coinIdB}` : `${coinIdB}|${coinIdA}`;
10
+ }
11
+
12
+ function rebuildLookup() {
13
+ const lookup = {};
14
+ for (const m of markets) {
15
+ const key = buildCoinKey(m.baseCoinId, m.quoteCoinId);
16
+ lookup[key] = m;
17
+ }
18
+ byCoinKey = lookup;
19
+ }
20
+
21
+ export function setInitialMarkets(list) {
22
+ markets = list || [];
23
+ rebuildLookup();
24
+ }
25
+
26
+ export function addOrUpdateMarket(market) {
27
+ const idx = markets.findIndex((m) => m.id === market.id);
28
+ if (idx >= 0) {
29
+ markets[idx] = { ...markets[idx], ...market };
30
+ } else {
31
+ markets.push(market);
32
+ }
33
+ rebuildLookup();
34
+ }
35
+
36
+ export function getMarkets() {
37
+ return markets;
38
+ }
39
+
40
+ export function getMarketByCoinKey() {
41
+ return byCoinKey;
42
+ }
43
+
44
+ /**
45
+ * Look up a market by two coin IDs (order-independent).
46
+ * @param {string} coinIdA
47
+ * @param {string} coinIdB
48
+ * @returns {Object|undefined}
49
+ */
50
+ export function getMarketByCoins(coinIdA, coinIdB) {
51
+ return byCoinKey[buildCoinKey(coinIdA, coinIdB)];
52
+ }
53
+
54
+ export function resetMarkets() {
55
+ markets = [];
56
+ byCoinKey = {};
57
+ }
@@ -0,0 +1,99 @@
1
+ const orderbookStore = {
2
+ books: new Map(), // pair -> { bids: [[price, qty], ...], asks: [[price, qty], ...] }
3
+ isInitialReceived: false,
4
+ pendingUpdates: [],
5
+ userOrders: [], // User's open CLOB orders (across all pairs)
6
+ };
7
+
8
+ // Initialize all order books with initial data
9
+ const setInitialOrderBooks = (books) => {
10
+ orderbookStore.books.clear();
11
+ for (const [pair, depth] of Object.entries(books)) {
12
+ orderbookStore.books.set(pair, {
13
+ bids: depth.bids || [],
14
+ asks: depth.asks || [],
15
+ });
16
+ }
17
+ orderbookStore.isInitialReceived = true;
18
+
19
+ // Process buffered updates
20
+ if (orderbookStore.pendingUpdates.length > 0) {
21
+ orderbookStore.pendingUpdates.forEach(({ pair, bids, asks }) => {
22
+ updateOrderBook(pair, bids, asks);
23
+ });
24
+ orderbookStore.pendingUpdates = [];
25
+ }
26
+ };
27
+
28
+ // Update a single order book
29
+ const updateOrderBook = (pair, bids, asks) => {
30
+ if (!orderbookStore.isInitialReceived) {
31
+ orderbookStore.pendingUpdates.push({ pair, bids, asks });
32
+ return;
33
+ }
34
+ orderbookStore.books.set(pair, {
35
+ bids: bids || [],
36
+ asks: asks || [],
37
+ });
38
+ };
39
+
40
+ // Get all order books as a plain object { [pair]: { bids, asks } }
41
+ const getAllOrderBooks = () => {
42
+ const result = {};
43
+ for (const [pair, depth] of orderbookStore.books) {
44
+ result[pair] = depth;
45
+ }
46
+ return result;
47
+ };
48
+
49
+ // Get a specific order book by pair
50
+ const getOrderBook = (pair) => {
51
+ return orderbookStore.books.get(pair) || { bids: [], asks: [] };
52
+ };
53
+
54
+ // Get all pairs with active order books
55
+ const getOrderBookPairs = () => {
56
+ return [...orderbookStore.books.keys()];
57
+ };
58
+
59
+ // Set user's open orders (received on connect)
60
+ const setUserOrders = (orders) => {
61
+ orderbookStore.userOrders = orders || [];
62
+ };
63
+
64
+ // Update a single user order (from order_updated event)
65
+ const updateUserOrder = (data) => {
66
+ if (data.refresh) {return;} // caller should refetch
67
+ const order = data.order;
68
+ if (!order) {return;}
69
+ const idx = orderbookStore.userOrders.findIndex((o) => o.id === order.id);
70
+ if (idx >= 0) {
71
+ orderbookStore.userOrders[idx] = { ...orderbookStore.userOrders[idx], ...order };
72
+ } else {
73
+ orderbookStore.userOrders.unshift(order);
74
+ }
75
+ };
76
+
77
+ // Get user's open orders
78
+ const getUserOrders = () => orderbookStore.userOrders;
79
+
80
+ // Reset store on disconnect or error
81
+ const resetOrderBooks = () => {
82
+ orderbookStore.books.clear();
83
+ orderbookStore.isInitialReceived = false;
84
+ orderbookStore.pendingUpdates = [];
85
+ orderbookStore.userOrders = [];
86
+ };
87
+
88
+ export {
89
+ orderbookStore,
90
+ setInitialOrderBooks,
91
+ updateOrderBook,
92
+ getAllOrderBooks,
93
+ getOrderBook,
94
+ getOrderBookPairs,
95
+ setUserOrders,
96
+ updateUserOrder,
97
+ getUserOrders,
98
+ resetOrderBooks,
99
+ };
@@ -1,4 +1,4 @@
1
- import { BigNumber } from 'bignumber.js';
1
+ import { SafeBigNumber as BigNumber } from '../utils/safeBigNumber.mjs';
2
2
 
3
3
  const poolStore = {
4
4
  pools: new Map(), // Store pool data by pool ID
@@ -1,5 +1,5 @@
1
1
  // src/store/userSharesStore.mjs
2
- import { BigNumber } from 'bignumber.js';
2
+ import { SafeBigNumber as BigNumber } from '../utils/safeBigNumber.mjs';
3
3
 
4
4
  const userSharesStore = {
5
5
  userShares: new Map(),
@@ -1,4 +1,4 @@
1
- import { BigNumber } from 'bignumber.js';
1
+ import { SafeBigNumber as BigNumber } from '../utils/safeBigNumber.mjs';
2
2
 
3
3
  const walletStore = {
4
4
  wallets: new Map(), // Store wallet data by ticker
@@ -1,6 +1,5 @@
1
1
  // src/utils/liquidityUtils.mjs
2
- import { BigNumber } from 'bignumber.js';
3
-
2
+ import { SafeBigNumber as BigNumber } from './safeBigNumber.mjs';
4
3
  import { getRunesPriceUSD, getTokenPriceInRunes } from './swapUtils.mjs';
5
4
 
6
5
  export function normalizeTokenPairFrontend(coinA, coinB, pools) {
@@ -111,7 +110,7 @@ export function getPoolRatioFrontend(pool) {
111
110
  return reserveADecimal.div(reserveBDecimal);
112
111
  }
113
112
 
114
- export function estimateLiquidityFrontend({ coinA, coinB, amountA, amountB, pools, coins }) {
113
+ export function estimateLiquidityFrontend({ coinA, coinB, amountA, amountB, pools }) {
115
114
  if ((amountA === null && amountB === null) || (amountA !== null && amountB !== null)) {
116
115
  throw new Error('Provide either amountA or amountB, but not both or neither');
117
116
  }
@@ -1,9 +1,10 @@
1
1
  // src/utils/priceUtils.mjs
2
- import { BigNumber } from 'bignumber.js';
3
2
 
4
3
  import { getPools } from '../store/poolStore.mjs';
5
4
  import { getCoinByTicker } from '../store/coinStore.mjs';
6
5
 
6
+ import { SafeBigNumber as BigNumber } from './safeBigNumber.mjs';
7
+
7
8
  export function createPriceUtils() {
8
9
  const getRunesPriceUSD = () => {
9
10
  const pools = getPools();
@@ -0,0 +1,11 @@
1
+ // src/utils/safeBigNumber.mjs
2
+ // Isolated BigNumber constructor aligned with the backend's SafeBigNumber
3
+ // (runesx-api/src/utils/safeBigNumber.mjs). Uses the same DECIMAL_PLACES=40
4
+ // and EXPONENTIAL_AT=[-100, 100] configuration to ensure estimation arithmetic
5
+ // matches backend financial calculations.
6
+ import BigNumber from 'bignumber.js';
7
+
8
+ export const SafeBigNumber = BigNumber.clone({
9
+ DECIMAL_PLACES: 40,
10
+ EXPONENTIAL_AT: [-100, 100],
11
+ });