blumefi 2.6.0 → 2.7.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.
Files changed (2) hide show
  1. package/cli.js +260 -22
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -16,6 +16,8 @@ const c = {
16
16
  yellow: s => `\x1b[33m${s}\x1b[39m`,
17
17
  red: s => `\x1b[31m${s}\x1b[39m`,
18
18
  white: s => `\x1b[97m${s}\x1b[39m`,
19
+ cyan: s => `\x1b[36m${s}\x1b[39m`,
20
+ brightGreen: s => `\x1b[92m${s}\x1b[39m`,
19
21
  }
20
22
 
21
23
  function progressBar(pct, width = 16) {
@@ -100,6 +102,22 @@ const SWAP_ROUTER_ABI = [
100
102
  { name: 'swapExactTokensForTokens', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOutMin', type: 'uint256' }, { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ type: 'uint256[]' }] },
101
103
  { name: 'getAmountsOut', type: 'function', stateMutability: 'view', inputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'path', type: 'address[]' }], outputs: [{ type: 'uint256[]' }] },
102
104
  { name: 'WETH', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
105
+ { name: 'addLiquidityETH', type: 'function', stateMutability: 'payable', inputs: [{ name: 'token', type: 'address' }, { name: 'amountTokenDesired', type: 'uint256' }, { name: 'amountTokenMin', type: 'uint256' }, { name: 'amountETHMin', type: 'uint256' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ name: 'amountToken', type: 'uint256' }, { name: 'amountETH', type: 'uint256' }, { name: 'liquidity', type: 'uint256' }] },
106
+ { name: 'removeLiquidityETH', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'liquidity', type: 'uint256' }, { name: 'amountTokenMin', type: 'uint256' }, { name: 'amountETHMin', type: 'uint256' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }], outputs: [{ name: 'amountToken', type: 'uint256' }, { name: 'amountETH', type: 'uint256' }] },
107
+ ]
108
+
109
+ const SWAP_FACTORY_ABI = [
110
+ { name: 'getPair', type: 'function', stateMutability: 'view', inputs: [{ name: 'tokenA', type: 'address' }, { name: 'tokenB', type: 'address' }], outputs: [{ type: 'address' }] },
111
+ ]
112
+
113
+ const SWAP_PAIR_ABI = [
114
+ { name: 'getReserves', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: 'reserve0', type: 'uint112' }, { name: 'reserve1', type: 'uint112' }, { name: 'blockTimestampLast', type: 'uint32' }] },
115
+ { name: 'token0', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
116
+ { name: 'token1', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
117
+ { name: 'totalSupply', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
118
+ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }] },
119
+ { name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }] },
120
+ { name: 'allowance', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }] },
103
121
  ]
104
122
 
105
123
  const PERPS_ROUTER_ABI = [
@@ -583,6 +601,196 @@ async function cmdSwapPools() {
583
601
  }
584
602
  }
585
603
 
604
+ async function cmdSwapAddLiquidity(tokenAddress, xrpAmountStr) {
605
+ if (!tokenAddress || !xrpAmountStr) {
606
+ console.error('Usage: blumefi swap add-liquidity <token_address> <xrp_amount>')
607
+ console.error(' blumefi swap add-liquidity 0x1234... 10 Add liquidity with 10 XRP')
608
+ console.error(' blumefi swap add-liquidity 0x1234... 10 --slippage 5')
609
+ process.exit(1)
610
+ }
611
+
612
+ const viem = await loadViem()
613
+ const net = getNetwork()
614
+ const chain = getChain()
615
+ const xrpAmount = parseFloat(xrpAmountStr)
616
+ if (isNaN(xrpAmount) || xrpAmount <= 0) throw new Error('Invalid XRP amount')
617
+
618
+ // Parse --slippage flag (default 2%)
619
+ const slippageIdx = process.argv.indexOf('--slippage')
620
+ const slippagePct = slippageIdx !== -1 ? parseFloat(process.argv[slippageIdx + 1] || '2') : 2
621
+ if (isNaN(slippagePct) || slippagePct <= 0 || slippagePct > 50) throw new Error('Slippage must be between 0 and 50')
622
+ const slippageBps = BigInt(Math.round(slippagePct * 100))
623
+
624
+ const wxrp = net.swapWxrp || net.wxrp
625
+ const xrpValue = viem.parseEther(xrpAmountStr)
626
+
627
+ // Get pair address
628
+ const pairAddress = await readContract({
629
+ address: net.swapFactory, abi: SWAP_FACTORY_ABI, functionName: 'getPair', args: [tokenAddress, wxrp],
630
+ })
631
+ const zeroPair = '0x0000000000000000000000000000000000000000'
632
+ if (!pairAddress || pairAddress === zeroPair) {
633
+ throw new Error(`No pool exists for this token/XRP pair. Create one on swap.blumefi.com first.`)
634
+ }
635
+
636
+ // Get reserves and figure out which side is the token vs WXRP
637
+ const [reserves, token0Addr, tokenDecimals, tokenSymbol] = await Promise.all([
638
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'getReserves', args: [] }),
639
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'token0', args: [] }),
640
+ readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals', args: [] }),
641
+ readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'symbol', args: [] }),
642
+ ])
643
+
644
+ const isToken0 = token0Addr.toLowerCase() === tokenAddress.toLowerCase()
645
+ const tokenReserve = isToken0 ? reserves[0] : reserves[1]
646
+ const xrpReserve = isToken0 ? reserves[1] : reserves[0]
647
+
648
+ if (xrpReserve === 0n || tokenReserve === 0n) throw new Error('Pool has no liquidity')
649
+
650
+ // Calculate required token amount at current pool ratio
651
+ // tokenAmount = xrpAmount * tokenReserve / xrpReserve (+ 1 to round up)
652
+ const tokenAmountRaw = xrpValue * tokenReserve / xrpReserve
653
+ const tokenAmountDesired = tokenAmountRaw + 1n
654
+
655
+ const tokenFormatted = parseFloat(viem.formatUnits(tokenAmountDesired, tokenDecimals)).toLocaleString()
656
+
657
+ // Slippage minimums (from raw ratio, not bumped desired)
658
+ const amountTokenMin = tokenAmountRaw * (10000n - slippageBps) / 10000n
659
+ const amountETHMin = xrpValue * (10000n - slippageBps) / 10000n
660
+
661
+ console.log(`\n Adding liquidity on ${chain}`)
662
+ console.log(` ─────────────────────────────────────`)
663
+ console.log(` Pool: ${tokenSymbol} / XRP`)
664
+ console.log(` XRP: ${xrpAmount} XRP`)
665
+ console.log(` Token: ~${tokenFormatted} ${tokenSymbol}`)
666
+ console.log(` Slippage: ${slippagePct}%`)
667
+
668
+ // Approve token for router
669
+ await ensureApproval(tokenAddress, net.swapRouter, tokenAmountDesired)
670
+
671
+ const { account } = await getWalletClient()
672
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200) // 20 min
673
+
674
+ console.log(' Sending transaction...')
675
+ const { hash, explorer } = await sendContractTx({
676
+ to: net.swapRouter,
677
+ abi: SWAP_ROUTER_ABI,
678
+ functionName: 'addLiquidityETH',
679
+ args: [tokenAddress, tokenAmountDesired, amountTokenMin, amountETHMin, account.address, deadline],
680
+ value: xrpValue,
681
+ })
682
+
683
+ console.log(`\n Added liquidity: ${xrpAmount} XRP + ~${tokenFormatted} ${tokenSymbol}`)
684
+ console.log(` TX: ${explorer}`)
685
+ }
686
+
687
+ async function cmdSwapRemoveLiquidity(tokenAddress, lpAmountStr) {
688
+ if (!tokenAddress || !lpAmountStr) {
689
+ console.error('Usage: blumefi swap remove-liquidity <token_address> <lp_amount|all>')
690
+ console.error(' blumefi swap remove-liquidity 0x1234... all Remove all liquidity')
691
+ console.error(' blumefi swap remove-liquidity 0x1234... 5 Remove 5 LP tokens')
692
+ console.error(' blumefi swap remove-liquidity 0x1234... 5 --slippage 5')
693
+ process.exit(1)
694
+ }
695
+
696
+ const viem = await loadViem()
697
+ const net = getNetwork()
698
+ const chain = getChain()
699
+ const { account } = await getWalletClient()
700
+
701
+ // Parse --slippage flag (default 2%)
702
+ const slippageIdx = process.argv.indexOf('--slippage')
703
+ const slippagePct = slippageIdx !== -1 ? parseFloat(process.argv[slippageIdx + 1] || '2') : 2
704
+ if (isNaN(slippagePct) || slippagePct <= 0 || slippagePct > 50) throw new Error('Slippage must be between 0 and 50')
705
+ const slippageBps = BigInt(Math.round(slippagePct * 100))
706
+
707
+ const wxrp = net.swapWxrp || net.wxrp
708
+
709
+ // Get pair address
710
+ const pairAddress = await readContract({
711
+ address: net.swapFactory, abi: SWAP_FACTORY_ABI, functionName: 'getPair', args: [tokenAddress, wxrp],
712
+ })
713
+ const zeroPair = '0x0000000000000000000000000000000000000000'
714
+ if (!pairAddress || pairAddress === zeroPair) {
715
+ throw new Error(`No pool exists for this token/XRP pair.`)
716
+ }
717
+
718
+ // Get LP balance, reserves, pair info
719
+ const [lpBalance, reserves, token0Addr, lpTotalSupply, tokenDecimals, tokenSymbol] = await Promise.all([
720
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'balanceOf', args: [account.address] }),
721
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'getReserves', args: [] }),
722
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'token0', args: [] }),
723
+ readContract({ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'totalSupply', args: [] }),
724
+ readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals', args: [] }),
725
+ readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'symbol', args: [] }),
726
+ ])
727
+
728
+ let lpAmount
729
+ if (lpAmountStr.toLowerCase() === 'all') {
730
+ lpAmount = lpBalance
731
+ if (lpAmount === 0n) { console.log(`\n No LP tokens for ${tokenSymbol}/XRP pool.`); return }
732
+ } else {
733
+ lpAmount = viem.parseEther(lpAmountStr)
734
+ }
735
+
736
+ if (lpAmount > lpBalance) {
737
+ console.error(`Error: Insufficient LP balance. You have ${parseFloat(viem.formatEther(lpBalance)).toFixed(6)} LP tokens`)
738
+ process.exit(1)
739
+ }
740
+
741
+ // Estimate what you'll receive
742
+ const isToken0 = token0Addr.toLowerCase() === tokenAddress.toLowerCase()
743
+ const tokenReserve = isToken0 ? reserves[0] : reserves[1]
744
+ const xrpReserve = isToken0 ? reserves[1] : reserves[0]
745
+
746
+ if (lpTotalSupply === 0n) throw new Error('Pool total supply is zero')
747
+
748
+ const expectedToken = lpAmount * tokenReserve / lpTotalSupply
749
+ const expectedXrp = lpAmount * xrpReserve / lpTotalSupply
750
+
751
+ const tokenFormatted = parseFloat(viem.formatUnits(expectedToken, tokenDecimals)).toLocaleString()
752
+ const xrpFormatted = parseFloat(viem.formatEther(expectedXrp)).toFixed(4)
753
+ const lpFormatted = parseFloat(viem.formatEther(lpAmount)).toFixed(6)
754
+
755
+ // Slippage minimums
756
+ const amountTokenMin = expectedToken * (10000n - slippageBps) / 10000n
757
+ const amountETHMin = expectedXrp * (10000n - slippageBps) / 10000n
758
+
759
+ console.log(`\n Removing liquidity on ${chain}`)
760
+ console.log(` ─────────────────────────────────────`)
761
+ console.log(` Pool: ${tokenSymbol} / XRP`)
762
+ console.log(` LP tokens: ${lpFormatted}`)
763
+ console.log(` Receive: ~${tokenFormatted} ${tokenSymbol} + ~${xrpFormatted} XRP`)
764
+ console.log(` Slippage: ${slippagePct}%`)
765
+
766
+ // Approve LP token for router
767
+ const lpAllowance = await readContract({
768
+ address: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'allowance', args: [account.address, net.swapRouter],
769
+ })
770
+ if (lpAllowance < lpAmount) {
771
+ console.log(' Approving LP tokens...')
772
+ const maxUint = 2n ** 256n - 1n
773
+ const { hash: approveHash } = await sendContractTx({
774
+ to: pairAddress, abi: SWAP_PAIR_ABI, functionName: 'approve', args: [net.swapRouter, maxUint],
775
+ })
776
+ const pub = await getPublicClient()
777
+ await pub.waitForTransactionReceipt({ hash: approveHash, timeout: 30000 })
778
+ }
779
+
780
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200) // 20 min
781
+
782
+ console.log(' Sending transaction...')
783
+ const { hash, explorer } = await sendContractTx({
784
+ to: net.swapRouter,
785
+ abi: SWAP_ROUTER_ABI,
786
+ functionName: 'removeLiquidityETH',
787
+ args: [tokenAddress, lpAmount, amountTokenMin, amountETHMin, account.address, deadline],
788
+ })
789
+
790
+ console.log(`\n Removed liquidity: ~${tokenFormatted} ${tokenSymbol} + ~${xrpFormatted} XRP`)
791
+ console.log(` TX: ${explorer}`)
792
+ }
793
+
586
794
  // ─── Trade commands (perps) ──────────────────────────────────────────
587
795
 
588
796
  function requirePerps() {
@@ -1098,7 +1306,7 @@ async function cmdPadSearch(query) {
1098
1306
  }
1099
1307
  }
1100
1308
 
1101
- function renderTokenTable(tokens, total, chain, filter) {
1309
+ function renderTokenTable(tokens, total, chain, filter, frame = 0) {
1102
1310
  const lines = []
1103
1311
  if (!tokens.length) {
1104
1312
  lines.push(c.dim(` No tokens found (${chain}, filter: ${filter})`))
@@ -1136,12 +1344,28 @@ function renderTokenTable(tokens, total, chain, filter) {
1136
1344
 
1137
1345
  for (const t of tokens) {
1138
1346
  const pct = Math.min(t.progress || 0, 100)
1139
- const pctStr = `${Math.round(pct)}%`
1140
- const bar = progressBar(pct) + ' ' + (pct >= 100 ? c.green(pctStr) : c.yellow(pctStr))
1141
- const status = t.graduated ? c.green('Graduated') : c.yellow('Active')
1347
+ const hot = pct >= 75 && !t.graduated
1348
+ const pulse = hot && frame % 2 === 0
1349
+
1350
+ let bar, pctStr, status, indicator
1351
+ if (hot) {
1352
+ pctStr = `${Math.round(pct)}%`
1353
+ const barFn = pulse ? c.brightGreen : c.green
1354
+ const clamped = Math.max(0, Math.min(pct, 100))
1355
+ const filled = Math.round((clamped / 100) * 16)
1356
+ const empty = 16 - filled
1357
+ bar = barFn('█'.repeat(filled)) + c.dim('░'.repeat(empty)) + ' ' + (pulse ? c.bold(c.brightGreen(pctStr)) : c.bold(c.yellow(pctStr)))
1358
+ indicator = pulse ? ' ▲' : ''
1359
+ status = pulse ? c.bold(c.brightGreen('Hot')) : c.yellow('Active')
1360
+ } else {
1361
+ pctStr = `${Math.round(pct)}%`
1362
+ bar = progressBar(pct) + ' ' + (pct >= 100 ? c.green(pctStr) : c.yellow(pctStr))
1363
+ indicator = ''
1364
+ status = t.graduated ? c.green('Graduated') : c.yellow('Active')
1365
+ }
1142
1366
 
1143
1367
  const row =
1144
- padRight(c.bold(t.symbol || '???'), cols.symbol) +
1368
+ padRight(c.bold(t.symbol || '???') + indicator, cols.symbol) +
1145
1369
  padRight((t.name || '—').slice(0, 18), cols.name) +
1146
1370
  padLeft(fmtPrice(t.price), cols.price) + ' ' +
1147
1371
  padLeft(fmtNum(t.marketCap), cols.mcap) + ' ' +
@@ -1186,7 +1410,8 @@ async function cmdPadTokens() {
1186
1410
  }
1187
1411
 
1188
1412
  // Live dashboard mode
1189
- const REFRESH = 5
1413
+ const FETCH_INTERVAL = 5000 // fetch new data every 5s
1414
+ const RENDER_INTERVAL = 500 // redraw for animation every 500ms
1190
1415
  const hideCursor = () => process.stdout.write('\x1b[?25l')
1191
1416
  const showCursor = () => process.stdout.write('\x1b[?25h')
1192
1417
  const clearScreen = () => process.stdout.write('\x1b[2J\x1b[H')
@@ -1203,30 +1428,39 @@ async function cmdPadTokens() {
1203
1428
 
1204
1429
  hideCursor()
1205
1430
 
1431
+ let cachedTokens = [], cachedTotal = 0, lastFetch = 0, fetchError = null, frame = 0
1432
+
1206
1433
  while (running) {
1207
- let output
1208
- try {
1209
- const data = await apiFetch(`/pad/tokens?chain=${chain}&filter=${filter}&sort=${sort}&limit=${limit}&offset=0`)
1210
- const tokens = data.tokens || data.data || []
1211
- const total = data.total || tokens.length
1212
- output = renderTokenTable(tokens, total, chain, filter)
1213
- } catch (err) {
1214
- output = c.red(` Error fetching data: ${err.message}`) + '\n' + c.dim(' Will retry...')
1434
+ // Fetch new data if stale
1435
+ const now = Date.now()
1436
+ if (now - lastFetch >= FETCH_INTERVAL) {
1437
+ try {
1438
+ const data = await apiFetch(`/pad/tokens?chain=${chain}&filter=${filter}&sort=${sort}&limit=${limit}&offset=0`)
1439
+ cachedTokens = data.tokens || data.data || []
1440
+ cachedTotal = data.total || cachedTokens.length
1441
+ fetchError = null
1442
+ } catch (err) {
1443
+ fetchError = err.message
1444
+ }
1445
+ lastFetch = Date.now()
1215
1446
  }
1216
1447
 
1448
+ // Render current frame
1217
1449
  clearScreen()
1218
1450
  console.log('')
1219
- console.log(output)
1451
+ if (fetchError && !cachedTokens.length) {
1452
+ console.log(c.red(` Error fetching data: ${fetchError}`) + '\n' + c.dim(' Will retry...'))
1453
+ } else {
1454
+ console.log(renderTokenTable(cachedTokens, cachedTotal, chain, filter, frame))
1455
+ }
1220
1456
 
1221
- const now = new Date()
1222
- const ts = now.toLocaleTimeString('en-US', { hour12: false })
1457
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
1458
+ const nextFetchIn = Math.max(0, Math.ceil((FETCH_INTERVAL - (Date.now() - lastFetch)) / 1000))
1223
1459
  console.log('')
1224
- console.log(c.dim(` Updated ${ts} · Refreshing in ${REFRESH}s · Ctrl+C to exit`))
1460
+ console.log(c.dim(` Updated ${ts} · Refreshing in ${nextFetchIn}s · Ctrl+C to exit`))
1225
1461
 
1226
- // Countdown sleep — break early if stopped
1227
- for (let i = 0; i < REFRESH * 10 && running; i++) {
1228
- await new Promise(r => setTimeout(r, 100))
1229
- }
1462
+ frame++
1463
+ await new Promise(r => setTimeout(r, RENDER_INTERVAL))
1230
1464
  }
1231
1465
  }
1232
1466
 
@@ -1461,6 +1695,8 @@ Swap (DEX):
1461
1695
  swap <amount> <from> <to> Swap tokens (e.g. swap 1 XRP RLUSD)
1462
1696
  swap quote <amount> <from> <to> Get a quote without executing
1463
1697
  swap pools List available pools
1698
+ swap add-liquidity <token> <xrp> Add liquidity to a token/XRP pool
1699
+ swap remove-liquidity <token> <lp|all> Remove liquidity from a pool
1464
1700
 
1465
1701
  Trade (Perps — testnet):
1466
1702
  trade long <usd> [leverage] Open long (e.g. trade long 100 5x)
@@ -1519,6 +1755,8 @@ async function main() {
1519
1755
  } else if (cmd === 'swap') {
1520
1756
  if (sub === 'quote' || sub === 'q') await cmdSwapQuote(args[2], args[3], args[4])
1521
1757
  else if (sub === 'pools' || sub === 'p') await cmdSwapPools()
1758
+ else if (sub === 'add-liquidity' || sub === 'al') await cmdSwapAddLiquidity(args[2], args[3])
1759
+ else if (sub === 'remove-liquidity' || sub === 'rl') await cmdSwapRemoveLiquidity(args[2], args[3])
1522
1760
  else await cmdSwap(sub, args[2], args[3]) // swap <amount> <from> <to>
1523
1761
  } else if (cmd === 'trade' || cmd === 't') {
1524
1762
  if (sub === 'long' || sub === 'l') await cmdTradeOpen(true, args[2], args[3])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blumefi",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "BlumeFi CLI — DeFi reimagined for the agentic era. Trade, chat, and interact with the Blume ecosystem from the command line.",
5
5
  "main": "cli.js",
6
6
  "bin": {