@runesx/api-client 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runesx/api-client",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
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,24 +13,24 @@
13
13
  "test": "jest"
14
14
  },
15
15
  "dependencies": {
16
- "axios": "^1.7.7",
16
+ "axios": "^1.13.5",
17
17
  "bignumber.js": "^9.1.2",
18
- "socket.io-client": "^4.7.5"
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": "^11.0.3",
25
- "dotenv": "^16.4.5",
24
+ "@semantic-release/npm": "^13.1.4",
25
+ "dotenv": "^17.3.1",
26
26
  "eslint": "^9.11.1",
27
27
  "eslint-plugin-import": "^2.30.0",
28
- "eslint-plugin-n": "^17.0.0",
28
+ "eslint-plugin-n": "^17.24.0",
29
29
  "eslint-plugin-promise": "^7.1.0",
30
- "globals": "^15.9.0",
31
- "jest": "^30.0.4",
32
- "nodemon": "^3.1.4",
33
- "semantic-release": "^22.0.12"
30
+ "globals": "^17.3.0",
31
+ "jest": "^30.2.0",
32
+ "nodemon": "^3.1.11",
33
+ "semantic-release": "^25.0.3"
34
34
  },
35
35
  "files": [
36
36
  "src/*.mjs",
package/src/api.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  // src/api.mjs
2
+ import { randomUUID } from 'crypto';
3
+
2
4
  import axios from 'axios';
3
5
 
4
6
  export function createApi(config) {
@@ -19,32 +21,59 @@ export function createApi(config) {
19
21
  }
20
22
  }
21
23
 
22
- async function postSwap({ amountIn, path, minAmountOut }) {
24
+ async function postSwap({ amountIn, path, minAmountOut, idempotencyKey }) {
23
25
  try {
24
- const response = await api.post('/swap', { amountIn, path, minAmountOut });
26
+ const key = idempotencyKey || randomUUID();
27
+ const response = await api.post('/swap', { amountIn, path, minAmountOut }, {
28
+ headers: { 'X-Idempotency-Key': key },
29
+ });
25
30
  return response.data;
26
31
  } catch (error) {
27
32
  throw new Error(error.response?.data?.error || 'Failed to execute swap');
28
33
  }
29
34
  }
30
35
 
31
- async function depositLiquidity({ coinA, coinB, amountA, amountB }) {
36
+ async function depositLiquidity({ tickerA, tickerB, amountA, amountB, minShares, idempotencyKey }) {
32
37
  try {
33
- const response = await api.post('/liquidity/deposit', { coinA, coinB, amountA, amountB });
38
+ const key = idempotencyKey || randomUUID();
39
+ const headers = { 'X-Idempotency-Key': key };
40
+ const body = { tickerA, tickerB, amountA, amountB };
41
+ if (minShares !== undefined && minShares !== null) {
42
+ body.minShares = minShares;
43
+ }
44
+ const response = await api.post('/liquidity/deposit', body, { headers });
34
45
  return response.data.data;
35
46
  } catch (error) {
36
47
  throw new Error(error.response?.data?.error || 'Failed to deposit liquidity');
37
48
  }
38
49
  }
39
50
 
40
- async function withdrawLiquidity({ coinA, coinB, shares }) {
51
+ async function withdrawLiquidity({ tickerA, tickerB, shares, minAmountA, minAmountB, idempotencyKey }) {
41
52
  try {
42
- const response = await api.post('/liquidity/withdraw', { coinA, coinB, shares });
53
+ const key = idempotencyKey || randomUUID();
54
+ const headers = { 'X-Idempotency-Key': key };
55
+ const body = { tickerA, tickerB, shares };
56
+ if (minAmountA !== undefined && minAmountA !== null) {
57
+ body.minAmountA = minAmountA;
58
+ }
59
+ if (minAmountB !== undefined && minAmountB !== null) {
60
+ body.minAmountB = minAmountB;
61
+ }
62
+ const response = await api.post('/liquidity/withdraw', body, { headers });
43
63
  return response.data.data;
44
64
  } catch (error) {
45
65
  throw new Error(error.response?.data?.error || 'Failed to withdraw liquidity');
46
66
  }
47
67
  }
48
68
 
49
- return { getWallets, postSwap, depositLiquidity, withdrawLiquidity };
50
- }
69
+ async function getPools() {
70
+ try {
71
+ const response = await api.get('/pools');
72
+ return response.data.data;
73
+ } catch (error) {
74
+ throw new Error(error.response?.data?.error || 'Failed to fetch pools');
75
+ }
76
+ }
77
+
78
+ return { getWallets, getPools, postSwap, depositLiquidity, withdrawLiquidity };
79
+ }
package/src/index.mjs CHANGED
@@ -7,7 +7,7 @@ import { getCoins, getCoinByTicker } from './store/coinStore.mjs';
7
7
  import { getWallets as getWalletsStore, getWalletByTicker } from './store/walletStore.mjs';
8
8
  import { getUserShares, getUserShareByPoolId } from './store/userSharesStore.mjs';
9
9
  import { waitForStores } from './waitForStores.mjs';
10
- import { estimateLiquidityFrontend, checkRunesLiquidityFrontend, calculateShareAmounts } from './utils/liquidityUtils.mjs';
10
+ import { estimateLiquidityFrontend, checkRunesLiquidityFrontend, calculateShareAmounts, estimateDepositShares } from './utils/liquidityUtils.mjs';
11
11
  import { estimateSwap } from './utils/swapUtils.mjs';
12
12
  import { createPriceUtils } from './utils/priceUtils.mjs';
13
13
 
@@ -54,6 +54,8 @@ export function createRunesXClient(options = {}) {
54
54
  estimateSwap: (inputCoin, outputCoin, amountIn, maxHops = 6, algorithm = 'dfs') =>
55
55
  estimateSwap(inputCoin, outputCoin, amountIn, getPools(), getCoins(), maxHops, algorithm),
56
56
  estimateLiquidityFrontend,
57
+ estimateDepositShares: ({ pool, amountA, amountB, slippagePercent } = {}) =>
58
+ estimateDepositShares({ pool, amountA, amountB, slippagePercent }),
57
59
  checkRunesLiquidityFrontend: (coinA, coinB) =>
58
60
  checkRunesLiquidityFrontend(coinA, coinB, getPools(), getCoins()),
59
61
  calculateShareAmounts: () => calculateShareAmounts({ userShares: getUserShares(), pools: getPools() }),
@@ -174,6 +174,66 @@ export function estimateLiquidityFrontend({ coinA, coinB, amountA, amountB, pool
174
174
  };
175
175
  }
176
176
 
177
+ /**
178
+ * Estimate the shares that would be minted for a liquidity deposit.
179
+ * Uses the same formula as the backend engine.
180
+ *
181
+ * @param {Object} params
182
+ * @param {Object} params.pool - Pool object from getPools() (must include reserveA, reserveB, totalShares, coinA.dp, coinB.dp)
183
+ * @param {string} params.amountA - Decimal amount of tokenA to deposit
184
+ * @param {string} params.amountB - Decimal amount of tokenB to deposit
185
+ * @param {number} [params.slippagePercent=2] - Slippage tolerance percentage (e.g. 2 for 2%)
186
+ * @returns {{ estimatedShares: string, minShares: string } | null} null if shares cannot be estimated
187
+ */
188
+ export function estimateDepositShares({ pool, amountA, amountB, slippagePercent = 2 }) {
189
+ if (!pool || !amountA || !amountB) {
190
+ return null;
191
+ }
192
+
193
+ const dpA = pool.coinA.dp;
194
+ const dpB = pool.coinB.dp;
195
+ const amountABN = new BigNumber(amountA).decimalPlaces(dpA, BigNumber.ROUND_DOWN);
196
+ const amountBBN = new BigNumber(amountB).decimalPlaces(dpB, BigNumber.ROUND_DOWN);
197
+
198
+ if (!amountABN.isFinite() || !amountBBN.isFinite() || amountABN.lte(0) || amountBBN.lte(0)) {
199
+ return null;
200
+ }
201
+
202
+ const reserveA = new BigNumber(pool.reserveA);
203
+ const reserveB = new BigNumber(pool.reserveB);
204
+ const isNewPool = reserveA.isZero() && reserveB.isZero();
205
+
206
+ let shares;
207
+ if (isNewPool) {
208
+ // New pool: sqrt(amountA * amountB) * 10^9
209
+ shares = amountABN.times(amountBBN).sqrt().shiftedBy(9).integerValue(BigNumber.ROUND_DOWN);
210
+ } else {
211
+ // Existing pool: min((wholeCoinA * totalShares / reserveA), (wholeCoinB * totalShares / reserveB))
212
+ const totalShares = new BigNumber(pool.totalShares);
213
+ if (totalShares.isZero() || reserveA.isZero() || reserveB.isZero()) {
214
+ return null;
215
+ }
216
+
217
+ const wholeCoinA = amountABN.shiftedBy(dpA).integerValue(BigNumber.ROUND_DOWN);
218
+ const wholeCoinB = amountBBN.shiftedBy(dpB).integerValue(BigNumber.ROUND_DOWN);
219
+ const sharesFromA = wholeCoinA.times(totalShares).div(reserveA).integerValue(BigNumber.ROUND_DOWN);
220
+ const sharesFromB = wholeCoinB.times(totalShares).div(reserveB).integerValue(BigNumber.ROUND_DOWN);
221
+ shares = BigNumber.min(sharesFromA, sharesFromB);
222
+ }
223
+
224
+ if (shares.isZero()) {
225
+ return null;
226
+ }
227
+
228
+ const slippageMultiplier = new BigNumber(1).minus(new BigNumber(slippagePercent).div(100));
229
+ const minShares = shares.times(slippageMultiplier).integerValue(BigNumber.ROUND_DOWN).toString();
230
+
231
+ return {
232
+ estimatedShares: shares.toString(),
233
+ minShares,
234
+ };
235
+ }
236
+
177
237
  export function calculateShareAmounts({ userShares, pools }) {
178
238
  return userShares.map(share => {
179
239
  const pool = pools.find(p => p.id === share.poolId);
@@ -39,7 +39,6 @@ export function createPriceUtils() {
39
39
  };
40
40
 
41
41
  const getTokenPriceInRunes = (ticker) => {
42
- ensureInitialized();
43
42
  if (ticker === 'RUNES') {return '1';}
44
43
 
45
44
  const pools = getPools();
@@ -64,7 +63,6 @@ export function createPriceUtils() {
64
63
  };
65
64
 
66
65
  const getTokenPriceUSD = (ticker) => {
67
- ensureInitialized();
68
66
  if (ticker === 'USDC') {return '1';}
69
67
 
70
68
  const coin = getCoinByTicker(ticker);