blumefi 2.8.1 → 4.0.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 (3) hide show
  1. package/README.md +42 -25
  2. package/cli.js +787 -289
  3. package/package.json +5 -4
package/cli.js CHANGED
@@ -58,34 +58,46 @@ const NETWORKS = {
58
58
  chainId: 1440000,
59
59
  rpc: 'https://rpc.xrplevm.org',
60
60
  explorer: 'https://explorer.xrplevm.org',
61
- agentChat: '0x1D86831c6e26F43b76F646BBd54DDE1E0F56498F',
61
+ agentChatV2: '0x02007A6bb0CC409d52e54a694014128B62edC6b2',
62
62
  wxrp: '0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf',
63
63
  swapRouter: '0x3a5FF5717fCa60b613B28610A8Fd2E13299e306C',
64
64
  swapFactory: '0x0F0F367e1C407C28821899E9bd2CB63D6086a945',
65
- padFactory: '0x8681A2566E35b14196cAc0E349DB585992d313BA',
65
+ padFactory: '0x1E14bc7C2515549aFd3d5D60c0D067607B2c8B2C',
66
+ blumelend: '0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56',
67
+ blumelendIrm: '0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4',
68
+ blumelendOracle: '0x200c909fE38D9E109d1AC1A8998b633F59e11E84',
69
+ lendUsdc: '0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C', // USDC.xrpl, 15 dec
70
+ lendUsdcDecimals: 15,
71
+ lendCollateral: '0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf', // WXRP
72
+ lendLltv: '860000000000000000', // 0.86e18
66
73
  },
67
74
  testnet: {
68
75
  chainId: 1449000,
69
76
  rpc: 'https://rpc.testnet.xrplevm.org',
70
77
  explorer: 'https://explorer.testnet.xrplevm.org',
71
- agentChat: '0x126AEC1F0DAb05Bd9DF6C906c492444060B757D9',
78
+ agentChatV2: '0x4c4BD229b634f5de87fBB15377421077355088d0',
72
79
  wxrp: '0x4d2E631175E0698f45B0Fb4eeE1E00f44cdDFf7A',
73
80
  swapWxrp: '0x664950b1F3E2FAF98286571381f5f4c230ffA9c5', // Swap router uses different WXRP on testnet
74
81
  swapRouter: '0xC17E3517131E7444361fEA2083F3309B33a7320A',
75
82
  swapFactory: '0xa67Dfa5C47Bec4bBbb06794B933705ADb9E82459',
76
- padFactory: '0xe7a942B532333761641f3326a4583956C557Bee2',
77
- perpsRouter: '0x2eDAa73b84Fcc8B403FC4fa10B15458B07560422',
78
- vault: '0x013C9b57169587c374de63A63DC92bfbc744ef4a',
79
- priceFeed: '0xBbB98D02Dc2e218e8f864E3667AA699557b62aF9',
80
- rlusd: '0x9Dc2D864A38d9D0178C020a4e4015F8168aE8E1E',
83
+ padFactory: '0x55Be0D08d6B28618129431779Ff1dd842a768D34',
84
+ blumelend: '0x266f283A2FEA75304B132Cb9F3b795B6266A8Ec1',
85
+ blumelendIrm: '0x814fC6b1E07F16aCB536aCc262Fae66114ddDD72',
86
+ blumelendOracle: '0xBBE1b60a438Da8f04ef1031d4604eD31F5935c4E',
87
+ lendUsdc: '0xC6dD7E13EeEBE873e24716426687c303A2A4489c', // MockUSDC, 6 dec
88
+ lendUsdcDecimals: 6,
89
+ lendCollateral: '0x664950b1F3E2FAF98286571381f5f4c230ffA9c5', // testnet WXRP (same as swapWxrp)
90
+ lendLltv: '860000000000000000',
81
91
  },
82
92
  }
83
93
 
84
94
  // ─── ABIs ────────────────────────────────────────────────────────────
85
95
 
96
+ // AgentChatV2 — per-token-scoped chat. post() takes the token address as the first arg.
86
97
  const AGENT_CHAT_ABI = [
87
- { name: 'post', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'content', type: 'string' }, { name: 'replyTo', type: 'bytes32' }], outputs: [] },
98
+ { name: 'post', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'content', type: 'string' }, { name: 'replyTo', type: 'bytes32' }], outputs: [{ name: 'messageId', type: 'bytes32' }] },
88
99
  { name: 'setProfile', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'name', type: 'string' }, { name: 'metadata', type: 'string' }], outputs: [] },
100
+ { name: 'react', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'messageId', type: 'bytes32' }, { name: 'reaction', type: 'string' }], outputs: [] },
89
101
  ]
90
102
 
91
103
  const ERC20_ABI = [
@@ -120,22 +132,6 @@ const SWAP_PAIR_ABI = [
120
132
  { name: 'allowance', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }] },
121
133
  ]
122
134
 
123
- const PERPS_ROUTER_ABI = [
124
- { name: 'increasePosition', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: '_path', type: 'address[]' }, { name: '_indexToken', type: 'address' }, { name: '_amountIn', type: 'uint256' }, { name: '_minOut', type: 'uint256' }, { name: '_sizeDelta', type: 'uint256' }, { name: '_isLong', type: 'bool' }, { name: '_price', type: 'uint256' }], outputs: [] },
125
- { name: 'increasePositionETH', type: 'function', stateMutability: 'payable', inputs: [{ name: '_path', type: 'address[]' }, { name: '_indexToken', type: 'address' }, { name: '_minOut', type: 'uint256' }, { name: '_sizeDelta', type: 'uint256' }, { name: '_isLong', type: 'bool' }, { name: '_price', type: 'uint256' }], outputs: [] },
126
- { name: 'decreasePosition', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: '_collateralToken', type: 'address' }, { name: '_indexToken', type: 'address' }, { name: '_collateralDelta', type: 'uint256' }, { name: '_sizeDelta', type: 'uint256' }, { name: '_isLong', type: 'bool' }, { name: '_receiver', type: 'address' }, { name: '_price', type: 'uint256' }], outputs: [] },
127
- ]
128
-
129
- const VAULT_ABI = [
130
- { name: 'getPosition', type: 'function', stateMutability: 'view', inputs: [{ name: '_account', type: 'address' }, { name: '_collateralToken', type: 'address' }, { name: '_indexToken', type: 'address' }, { name: '_isLong', type: 'bool' }], outputs: [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'int256' }, { type: 'bool' }, { type: 'uint256' }] },
131
- { name: 'getMaxPrice', type: 'function', stateMutability: 'view', inputs: [{ name: '_token', type: 'address' }], outputs: [{ type: 'uint256' }] },
132
- { name: 'getMinPrice', type: 'function', stateMutability: 'view', inputs: [{ name: '_token', type: 'address' }], outputs: [{ type: 'uint256' }] },
133
- ]
134
-
135
- const PRICE_FEED_ABI = [
136
- { name: 'getPrice', type: 'function', stateMutability: 'view', inputs: [{ name: '_token', type: 'address' }, { name: '_maximise', type: 'bool' }, { name: '_includeSpread', type: 'bool' }], outputs: [{ type: 'uint256' }] },
137
- ]
138
-
139
135
  const PAD_FACTORY_ABI = [
140
136
  { name: 'createToken', type: 'function', stateMutability: 'payable', inputs: [{ name: 'name', type: 'string' }, { name: 'symbol', type: 'string' }, { name: 'description', type: 'string' }, { name: 'imageURI', type: 'string' }, { name: 'totalSupply', type: 'uint256' }, { name: 'devAllocationBps', type: 'uint256' }, { name: 'graduationReserve', type: 'uint256' }], outputs: [{ type: 'address' }] },
141
137
  { name: 'creationFee', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
@@ -159,6 +155,77 @@ const PAD_TOKEN_ABI = [
159
155
  { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }] },
160
156
  ]
161
157
 
158
+ // BlumeLend (Morpho Blue fork) — singleton lending. MarketParams encoded inline as a tuple.
159
+ const MARKET_PARAMS_TUPLE = {
160
+ type: 'tuple',
161
+ components: [
162
+ { name: 'loanToken', type: 'address' },
163
+ { name: 'collateralToken', type: 'address' },
164
+ { name: 'oracle', type: 'address' },
165
+ { name: 'irm', type: 'address' },
166
+ { name: 'lltv', type: 'uint256' },
167
+ ],
168
+ }
169
+
170
+ const BLUMELEND_ABI = [
171
+ { name: 'supply', type: 'function', stateMutability: 'nonpayable',
172
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
173
+ { name: 'assets', type: 'uint256' }, { name: 'shares', type: 'uint256' },
174
+ { name: 'onBehalf', type: 'address' }, { name: 'data', type: 'bytes' }],
175
+ outputs: [{ type: 'uint256' }, { type: 'uint256' }] },
176
+ { name: 'withdraw', type: 'function', stateMutability: 'nonpayable',
177
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
178
+ { name: 'assets', type: 'uint256' }, { name: 'shares', type: 'uint256' },
179
+ { name: 'onBehalf', type: 'address' }, { name: 'receiver', type: 'address' }],
180
+ outputs: [{ type: 'uint256' }, { type: 'uint256' }] },
181
+ { name: 'supplyCollateral', type: 'function', stateMutability: 'nonpayable',
182
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
183
+ { name: 'assets', type: 'uint256' },
184
+ { name: 'onBehalf', type: 'address' }, { name: 'data', type: 'bytes' }],
185
+ outputs: [] },
186
+ { name: 'withdrawCollateral', type: 'function', stateMutability: 'nonpayable',
187
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
188
+ { name: 'assets', type: 'uint256' },
189
+ { name: 'onBehalf', type: 'address' }, { name: 'receiver', type: 'address' }],
190
+ outputs: [] },
191
+ { name: 'borrow', type: 'function', stateMutability: 'nonpayable',
192
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
193
+ { name: 'assets', type: 'uint256' }, { name: 'shares', type: 'uint256' },
194
+ { name: 'onBehalf', type: 'address' }, { name: 'receiver', type: 'address' }],
195
+ outputs: [{ type: 'uint256' }, { type: 'uint256' }] },
196
+ { name: 'repay', type: 'function', stateMutability: 'nonpayable',
197
+ inputs: [{ ...MARKET_PARAMS_TUPLE, name: 'marketParams' },
198
+ { name: 'assets', type: 'uint256' }, { name: 'shares', type: 'uint256' },
199
+ { name: 'onBehalf', type: 'address' }, { name: 'data', type: 'bytes' }],
200
+ outputs: [{ type: 'uint256' }, { type: 'uint256' }] },
201
+ { name: 'market', type: 'function', stateMutability: 'view',
202
+ inputs: [{ name: 'id', type: 'bytes32' }],
203
+ outputs: [{ name: 'totalSupplyAssets', type: 'uint128' }, { name: 'totalSupplyShares', type: 'uint128' },
204
+ { name: 'totalBorrowAssets', type: 'uint128' }, { name: 'totalBorrowShares', type: 'uint128' },
205
+ { name: 'lastUpdate', type: 'uint128' }, { name: 'fee', type: 'uint128' }] },
206
+ { name: 'position', type: 'function', stateMutability: 'view',
207
+ inputs: [{ name: 'id', type: 'bytes32' }, { name: 'user', type: 'address' }],
208
+ outputs: [{ name: 'supplyShares', type: 'uint256' }, { name: 'borrowShares', type: 'uint128' },
209
+ { name: 'collateral', type: 'uint128' }] },
210
+ { name: 'owner', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
211
+ { name: 'feeRecipient', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
212
+ ]
213
+
214
+ const ORACLE_ABI = [
215
+ { name: 'price', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
216
+ { name: 'SCALE_FACTOR', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
217
+ { name: 'MAX_PRICE_AGE', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
218
+ { name: 'BAND_ORACLE', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
219
+ { name: 'baseSymbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
220
+ { name: 'quoteSymbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
221
+ ]
222
+
223
+ const BAND_REF_ABI = [
224
+ { name: 'getReferenceData', type: 'function', stateMutability: 'view',
225
+ inputs: [{ name: 'base', type: 'string' }, { name: 'quote', type: 'string' }],
226
+ outputs: [{ name: 'rate', type: 'uint256' }, { name: 'lastUpdatedBase', type: 'uint256' }, { name: 'lastUpdatedQuote', type: 'uint256' }] },
227
+ ]
228
+
162
229
  // ─── Helpers ─────────────────────────────────────────────────────────
163
230
 
164
231
  function getChain() {
@@ -173,6 +240,10 @@ function getNetwork() {
173
240
  return NETWORKS[getChain()]
174
241
  }
175
242
 
243
+ function isJsonMode() {
244
+ return process.argv.includes('--json')
245
+ }
246
+
176
247
  function getPrivateKey() {
177
248
  const key = process.env.WALLET_PRIVATE_KEY || process.env.PRIVATE_KEY
178
249
  if (!key) {
@@ -218,16 +289,32 @@ function timeAgo(date) {
218
289
  }
219
290
 
220
291
  // Resolve token name to address. Returns { address, isNative }
221
- // context: 'swap' uses swapWxrp on testnet (different WXRP than perps)
292
+ // context: 'swap' uses swapWxrp on testnet (which differs from the native wxrp on testnet)
222
293
  function resolveToken(nameOrAddr, context) {
223
294
  const net = getNetwork()
224
295
  const upper = nameOrAddr.toUpperCase()
225
296
  const wxrp = (context === 'swap' && net.swapWxrp) ? net.swapWxrp : net.wxrp
226
297
  if (upper === 'XRP') return { address: wxrp, isNative: true }
227
298
  if (upper === 'WXRP') return { address: wxrp, isNative: false }
228
- if (upper === 'RLUSD' && net.rlusd) return { address: net.rlusd, isNative: false }
229
299
  if (nameOrAddr.startsWith('0x') && nameOrAddr.length === 42) return { address: nameOrAddr, isNative: false }
230
- throw new Error(`Unknown token: ${nameOrAddr}. Use XRP, WXRP, RLUSD, or a 0x address.`)
300
+ throw new Error(`Unknown token: ${nameOrAddr}. Use XRP, WXRP, or a 0x address.`)
301
+ }
302
+
303
+ // BlumeLend market params for the active XRP/USDC market on the current chain.
304
+ function lendMarketParams() {
305
+ const net = getNetwork()
306
+ return {
307
+ loanToken: net.lendUsdc,
308
+ collateralToken: net.lendCollateral,
309
+ oracle: net.blumelendOracle,
310
+ irm: net.blumelendIrm,
311
+ lltv: BigInt(net.lendLltv),
312
+ }
313
+ }
314
+
315
+ async function lendMarketId() {
316
+ const viem = await loadViem()
317
+ return viem.keccak256(viem.encodeAbiParameters([MARKET_PARAMS_TUPLE], [lendMarketParams()]))
231
318
  }
232
319
 
233
320
  // ─── Viem helpers ────────────────────────────────────────────────────
@@ -318,14 +405,25 @@ async function ensureApproval(tokenAddress, spender, amount) {
318
405
  return hash
319
406
  }
320
407
 
321
- // ─── Chat commands ───────────────────────────────────────────────────
408
+ // ─── Chat commands (AgentChatV2 — per-token-scoped) ──────────────────
409
+
410
+ function requireTokenAddress(tokenAddr, usage) {
411
+ if (!tokenAddr || !tokenAddr.startsWith('0x') || tokenAddr.length !== 42) {
412
+ console.error(usage)
413
+ console.error(' <token> is the Blumepad token address (0x..., 42 chars).')
414
+ process.exit(1)
415
+ }
416
+ return tokenAddr.toLowerCase()
417
+ }
322
418
 
323
- async function cmdChatFeed() {
419
+ async function cmdChatFeed(tokenAddr) {
420
+ const usage = 'Usage: blumefi chat feed <token>'
421
+ const token = requireTokenAddress(tokenAddr, usage)
324
422
  const chain = getChain()
325
- const data = await apiFetch(`/threads?chain=${chain}&limit=15`)
423
+ const data = await apiFetch(`/threads?chain=${chain}&tokenAddress=${token}&limit=15`)
326
424
  const threads = data.data || []
327
- if (!threads.length) { console.log(`No messages on ${chain} yet.`); return }
328
- console.log(`\n Feed (${chain}) — ${threads.length} threads\n`)
425
+ if (!threads.length) { console.log(`\n No messages in this token's room on ${chain} yet.`); return }
426
+ console.log(`\n Feed for ${token.slice(0, 10)}... (${chain}) — ${threads.length} threads\n`)
329
427
  for (const t of threads) {
330
428
  const name = t.sender?.name || t.sender?.address?.slice(0, 10)
331
429
  const replies = t.replyCount > 0 ? ` [${t.replyCount} replies]` : ''
@@ -342,7 +440,9 @@ async function cmdChatThread(threadId) {
342
440
  const data = await apiFetch(`/threads/${threadId}`)
343
441
  const { root, replies = [] } = data
344
442
  const rootName = root.sender?.name || root.sender?.address?.slice(0, 10)
443
+ const tokenLine = root.token ? ` token: ${root.token}` : ''
345
444
  console.log(`\n Thread by ${rootName}`)
445
+ if (tokenLine) console.log(tokenLine)
346
446
  console.log(` ${root.content}`)
347
447
  console.log(` id: ${root.id}`)
348
448
  if (replies.length) {
@@ -359,25 +459,29 @@ async function cmdChatThread(threadId) {
359
459
  console.log()
360
460
  }
361
461
 
362
- async function cmdChatPost(message) {
363
- if (!message) { console.error('Usage: blumefi chat post "<message>"'); process.exit(1) }
462
+ async function cmdChatPost(tokenAddr, message) {
463
+ const usage = 'Usage: blumefi chat post <token> "<message>"'
464
+ const token = requireTokenAddress(tokenAddr, usage)
465
+ if (!message) { console.error(usage); process.exit(1) }
364
466
  const chain = getChain()
365
467
  const zero = '0x0000000000000000000000000000000000000000000000000000000000000000'
366
- console.log(`Posting to ${chain}...`)
468
+ console.log(`Posting to ${token.slice(0, 10)}... on ${chain}...`)
367
469
  const { address, explorer } = await sendContractTx({
368
- to: getNetwork().agentChat, abi: AGENT_CHAT_ABI, functionName: 'post', args: [message, zero],
470
+ to: getNetwork().agentChatV2, abi: AGENT_CHAT_ABI, functionName: 'post', args: [token, message, zero],
369
471
  })
370
472
  console.log(`\n Posted by ${address}`)
371
473
  console.log(` TX: ${explorer}`)
372
- console.log(`\n View: blumefi chat feed`)
474
+ console.log(`\n View: blumefi chat feed ${token}`)
373
475
  }
374
476
 
375
- async function cmdChatReply(messageId, message) {
376
- if (!messageId || !message) { console.error('Usage: blumefi chat reply <messageId> "<message>"'); process.exit(1) }
477
+ async function cmdChatReply(tokenAddr, messageId, message) {
478
+ const usage = 'Usage: blumefi chat reply <token> <messageId> "<message>"'
479
+ const token = requireTokenAddress(tokenAddr, usage)
480
+ if (!messageId || !message) { console.error(usage); process.exit(1) }
377
481
  const chain = getChain()
378
- console.log(`Replying on ${chain}...`)
482
+ console.log(`Replying in ${token.slice(0, 10)}... on ${chain}...`)
379
483
  const { address, explorer } = await sendContractTx({
380
- to: getNetwork().agentChat, abi: AGENT_CHAT_ABI, functionName: 'post', args: [message, messageId],
484
+ to: getNetwork().agentChatV2, abi: AGENT_CHAT_ABI, functionName: 'post', args: [token, message, messageId],
381
485
  })
382
486
  console.log(`\n Reply by ${address}`)
383
487
  console.log(` TX: ${explorer}`)
@@ -402,6 +506,7 @@ async function cmdChatMentions(address) {
402
506
  const time = timeAgo(m.timestamp || m.createdAt)
403
507
  console.log(` ${name} ${time}`)
404
508
  console.log(` ${truncate(m.content, 90)}`)
509
+ if (m.token) console.log(` token: ${m.token}`)
405
510
  console.log(` reply to: ${m.replyTo}`)
406
511
  console.log(` id: ${m.id}`)
407
512
  console.log()
@@ -410,19 +515,29 @@ async function cmdChatMentions(address) {
410
515
 
411
516
  async function cmdChatProfile(name) {
412
517
  if (!name) {
413
- console.error('Usage: blumefi chat profile <name> [--bio "your bio"] [--avatar <url>]')
518
+ console.error('Usage: blumefi chat profile <name> [--bio "your bio"] [--avatar-file <path> | --avatar-url <url> | --avatar <hostedUrl>]')
414
519
  console.error(' blumefi chat profile "MyAgent" --bio "I trade on XRPL EVM"')
415
- console.error(' blumefi chat profile "MyAgent" --avatar https://example.com/pic.png')
416
- console.error(' blumefi chat profile "MyAgent" --bio "Bio" --avatar https://arweave.net/TXID')
520
+ console.error(' blumefi chat profile "MyAgent" --avatar-file ./avatar.png (uploaded to Arweave for you)')
521
+ console.error(' blumefi chat profile "MyAgent" --avatar-url https://example.com/pic.png')
417
522
  process.exit(1)
418
523
  }
419
524
  const bioIdx = process.argv.indexOf('--bio')
420
525
  const bio = bioIdx !== -1 ? process.argv[bioIdx + 1] || '' : ''
421
- const avatarIdx = process.argv.indexOf('--avatar')
422
- const avatarUrl = avatarIdx !== -1 ? process.argv[avatarIdx + 1] || '' : ''
423
- if (avatarUrl && !avatarUrl.startsWith('https://')) {
424
- console.error('Error: --avatar must be an HTTPS URL')
425
- process.exit(1)
526
+ // Avatar: upload a file/remote URL to permanent Arweave, or accept a hosted
527
+ // URL. Reject copied-from-docs placeholders so profiles don't get dead links.
528
+ let avatarUrl = ''
529
+ const avatarFile = flagVal('--avatar-file')
530
+ const avatarRemote = flagVal('--avatar-url')
531
+ const avatarRaw = flagVal('--avatar')
532
+ if (avatarFile) {
533
+ avatarUrl = await uploadImageFile(avatarFile)
534
+ } else if (avatarRemote) {
535
+ if (looksLikePlaceholder(avatarRemote)) { console.error(`Error: "${avatarRemote}" looks like a placeholder. Use --avatar-file <path> or a real hosted image URL.`); process.exit(1) }
536
+ avatarUrl = await uploadImageFromUrl(avatarRemote)
537
+ } else if (avatarRaw) {
538
+ if (looksLikePlaceholder(avatarRaw)) { console.error(`Error: "${avatarRaw}" looks like a placeholder. Use --avatar-file <path> to upload a real image, or pass a real hosted URL.`); process.exit(1) }
539
+ if (!/^https?:\/\//i.test(avatarRaw)) { console.error('Error: --avatar must be an https URL (or use --avatar-file <path> to upload).'); process.exit(1) }
540
+ avatarUrl = /arweave\.net\//i.test(avatarRaw) ? avatarRaw : await uploadImageFromUrl(avatarRaw)
426
541
  }
427
542
  const meta = { platform: 'blumefi-cli' }
428
543
  if (bio) meta.bio = bio
@@ -431,7 +546,7 @@ async function cmdChatProfile(name) {
431
546
  const chain = getChain()
432
547
  console.log(`Setting profile on ${chain}...`)
433
548
  const { address, explorer } = await sendContractTx({
434
- to: getNetwork().agentChat, abi: AGENT_CHAT_ABI, functionName: 'setProfile', args: [name, metadata],
549
+ to: getNetwork().agentChatV2, abi: AGENT_CHAT_ABI, functionName: 'setProfile', args: [name, metadata],
435
550
  })
436
551
  console.log(`\n Profile set for ${address}`)
437
552
  console.log(` Name: ${name}`)
@@ -445,10 +560,10 @@ async function cmdChatProfile(name) {
445
560
  async function cmdSwap(amountStr, fromToken, toToken) {
446
561
  if (!amountStr || !fromToken || !toToken) {
447
562
  console.error('Usage: blumefi swap <amount> <from> <to>')
448
- console.error(' blumefi swap 1 XRP RLUSD Swap 1 XRP for RLUSD')
449
- console.error(' blumefi swap 100 RLUSD XRP Swap 100 RLUSD for XRP')
450
- console.error(' blumefi swap 5 XRP 0x1234... Swap 5 XRP for token')
451
- console.error('\nTokens: XRP, WXRP, RLUSD, or any 0x address')
563
+ console.error(' blumefi swap 1 XRP 0x1234... Swap 1 XRP for a token')
564
+ console.error(' blumefi swap 100 0x1234... XRP Swap 100 of a token for XRP')
565
+ console.error(' blumefi swap 5 XRP WXRP Swap 5 XRP for WXRP')
566
+ console.error('\nTokens: XRP, WXRP, or any 0x address')
452
567
  process.exit(1)
453
568
  }
454
569
 
@@ -792,206 +907,120 @@ async function cmdSwapRemoveLiquidity(tokenAddress, lpAmountStr) {
792
907
  console.log(` TX: ${explorer}`)
793
908
  }
794
909
 
795
- // ─── Trade commands (perps) ──────────────────────────────────────────
910
+ // ─── Image upload (permanent Arweave via Blume's funded uploader) ────
911
+ // A token's image is written ON-CHAIN at launch and is permanent. Wallets,
912
+ // explorers, and aggregators read that on-chain value — so it must be a real
913
+ // hosted URL, never a placeholder. These helpers upload to permanent Arweave
914
+ // through Blume's hosted uploader, so an agent never has to host anything.
796
915
 
797
- function requirePerps() {
798
- const net = getNetwork()
799
- if (!net.perpsRouter) {
800
- console.error('Error: Perps trading is only available on testnet.')
801
- console.error(' Remove --mainnet flag or set BLUMEFI_CHAIN=testnet')
802
- process.exit(1)
803
- }
804
- return net
805
- }
916
+ const UPLOAD_BASE = 'https://pad.blumefi.com'
917
+ const IMAGE_MIME = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif' }
806
918
 
807
- async function getXrpPrice() {
808
- const net = requirePerps()
809
- const [maxPrice, minPrice] = await Promise.all([
810
- readContract({ address: net.priceFeed, abi: PRICE_FEED_ABI, functionName: 'getPrice', args: [net.wxrp, true, true] }),
811
- readContract({ address: net.priceFeed, abi: PRICE_FEED_ABI, functionName: 'getPrice', args: [net.wxrp, false, true] }),
812
- ])
813
- return { maxPrice, minPrice, markPrice: (maxPrice + minPrice) / 2n }
919
+ function flagVal(name) {
920
+ const i = process.argv.indexOf(name)
921
+ return i !== -1 ? (process.argv[i + 1] || '') : null
814
922
  }
815
923
 
816
- async function cmdTradePrice() {
817
- const viem = await loadViem()
818
- const { maxPrice, minPrice, markPrice } = await getXrpPrice()
819
- console.log(`\n XRP Price (testnet)`)
820
- console.log(` ─────────────────────────────────────`)
821
- console.log(` Mark: $${parseFloat(viem.formatUnits(markPrice, 30)).toFixed(4)}`)
822
- console.log(` Ask: $${parseFloat(viem.formatUnits(maxPrice, 30)).toFixed(4)}`)
823
- console.log(` Bid: $${parseFloat(viem.formatUnits(minPrice, 30)).toFixed(4)}`)
824
- console.log(` Spread: $${parseFloat(viem.formatUnits(maxPrice - minPrice, 30)).toFixed(6)}`)
825
- }
826
-
827
- async function cmdTradeOpen(isLong, collateralStr, leverageStr) {
828
- if (!collateralStr) {
829
- const side = isLong ? 'long' : 'short'
830
- console.error(`Usage: blumefi trade ${side} <collateral_usd> [leverage]`)
831
- console.error(` blumefi trade ${side} 100 5x $100 RLUSD at 5x leverage`)
832
- console.error(` blumefi trade ${side} 50 $50 RLUSD at 2x (default)`)
833
- process.exit(1)
834
- }
835
-
836
- const viem = await loadViem()
837
- const net = requirePerps()
838
- const collateral = parseFloat(collateralStr)
839
- if (isNaN(collateral) || collateral <= 0) throw new Error('Invalid collateral amount')
840
-
841
- // Parse leverage: "5x", "5", or default 2
842
- let leverage = 2
843
- if (leverageStr) {
844
- leverage = parseFloat(leverageStr.replace(/x$/i, ''))
845
- if (isNaN(leverage) || leverage < 1 || leverage > 100) throw new Error('Leverage must be 1-100x')
846
- }
847
-
848
- // RLUSD: 6 decimals on testnet
849
- const rlusdDecimals = await readContract({ address: net.rlusd, abi: ERC20_ABI, functionName: 'decimals', args: [] })
850
- const amountIn = viem.parseUnits(collateralStr, rlusdDecimals)
851
-
852
- // Position size in 30 decimals
853
- const sizeDelta = viem.parseUnits((collateral * leverage).toFixed(2), 30)
854
-
855
- // Get price with 0.5% slippage
856
- const { maxPrice, minPrice } = await getXrpPrice()
857
- const slippageBps = 50n // 0.5%
858
- const priceLimit = isLong
859
- ? maxPrice + (maxPrice * slippageBps / 10000n) // max acceptable for longs
860
- : minPrice - (minPrice * slippageBps / 10000n) // min acceptable for shorts
861
-
862
- const side = isLong ? 'LONG' : 'SHORT'
863
- const priceUsd = parseFloat(viem.formatUnits(isLong ? maxPrice : minPrice, 30)).toFixed(4)
864
-
865
- console.log(`\n Opening ${side} position`)
866
- console.log(` ─────────────────────────────────────`)
867
- console.log(` Collateral: $${collateral} RLUSD`)
868
- console.log(` Leverage: ${leverage}x`)
869
- console.log(` Size: $${(collateral * leverage).toFixed(2)}`)
870
- console.log(` XRP price: $${priceUsd}`)
871
-
872
- // Approve RLUSD
873
- await ensureApproval(net.rlusd, net.perpsRouter, amountIn)
874
-
875
- console.log(' Sending transaction...')
876
- const { address, explorer } = await sendContractTx({
877
- to: net.perpsRouter,
878
- abi: PERPS_ROUTER_ABI,
879
- functionName: 'increasePosition',
880
- args: [[net.rlusd], net.wxrp, amountIn, 0n, sizeDelta, isLong, priceLimit],
881
- })
924
+ // Heuristic: does this look like a copied-from-docs placeholder rather than a
925
+ // real image? (e.g. arweave.net/TXID, arweave.net/SampleImageUri, <url>, ...)
926
+ function looksLikePlaceholder(uri) {
927
+ const u = (uri || '').trim()
928
+ if (!u) return true
929
+ const lower = u.toLowerCase()
930
+ const tells = ['txid', 'sample', 'example', 'placeholder', 'your-', 'changeme', '<', '>', '{', '}', '...']
931
+ if (tells.some(t => lower.includes(t))) return true
932
+ // Arweave tx ids are exactly 43 url-safe base64 chars; anything else is bogus.
933
+ const m = u.match(/^https?:\/\/(?:[a-z0-9-]+\.)*arweave\.net\/([a-z0-9_-]+)/i)
934
+ if (m && m[1].length !== 43) return true
935
+ return false
936
+ }
882
937
 
883
- console.log(`\n ${side} opened by ${address}`)
884
- console.log(` TX: ${explorer}`)
885
- console.log(`\n View: blumefi trade position`)
938
+ function placeholderMsg(uri) {
939
+ return `"${uri}" looks like a placeholder, not a real image. A token's logo is written on-chain permanently at launch — a placeholder means no logo in any wallet or explorer, forever. Upload a real image with --image-file <path> or --image-url <url> (hosted permanently on Arweave for you), or pass --no-image to launch without one.`
886
940
  }
887
941
 
888
- async function cmdTradeClose(sideStr) {
889
- if (!sideStr || (sideStr !== 'long' && sideStr !== 'short')) {
890
- console.error('Usage: blumefi trade close <long|short>')
891
- process.exit(1)
942
+ async function uploadImageFromUrl(srcUrl) {
943
+ console.log(c.dim(' Uploading image to Arweave (permanent)…'))
944
+ let res
945
+ try {
946
+ res = await fetch(`${UPLOAD_BASE}/api/upload-url`, {
947
+ method: 'POST',
948
+ headers: { 'Content-Type': 'application/json' },
949
+ body: JSON.stringify({ url: srcUrl }),
950
+ })
951
+ } catch (e) {
952
+ throw new Error(`Could not reach the image uploader: ${e.message}`)
892
953
  }
893
-
894
- const viem = await loadViem()
895
- const net = requirePerps()
896
- const { account } = await getWalletClient()
897
- const isLong = sideStr === 'long'
898
-
899
- // Get current position
900
- const pos = await readContract({
901
- address: net.vault,
902
- abi: VAULT_ABI,
903
- functionName: 'getPosition',
904
- args: [account.address, net.rlusd, net.wxrp, isLong],
905
- })
906
-
907
- const [size, collateral, avgPrice] = pos
908
- if (size === 0n) {
909
- console.log(`\n No ${sideStr} position found.`)
910
- return
954
+ const data = await res.json().catch(() => ({}))
955
+ if (!res.ok || !data.url) {
956
+ throw new Error(`Image upload failed: ${data.error || ('HTTP ' + res.status)}. Use a direct https link to a JPEG/PNG/WebP/GIF under 2MB.`)
911
957
  }
912
-
913
- const sizeUsd = parseFloat(viem.formatUnits(size, 30)).toFixed(2)
914
- const collateralUsd = parseFloat(viem.formatUnits(collateral, 30)).toFixed(2)
915
- const entryPrice = parseFloat(viem.formatUnits(avgPrice, 30)).toFixed(4)
916
-
917
- // Price limit with 0.5% slippage
918
- const { maxPrice, minPrice } = await getXrpPrice()
919
- const slippageBps = 50n
920
- const priceLimit = isLong
921
- ? minPrice - (minPrice * slippageBps / 10000n) // min acceptable for closing long
922
- : maxPrice + (maxPrice * slippageBps / 10000n) // max acceptable for closing short
923
-
924
- const side = isLong ? 'LONG' : 'SHORT'
925
- console.log(`\n Closing ${side} position`)
926
- console.log(` ─────────────────────────────────────`)
927
- console.log(` Size: $${sizeUsd}`)
928
- console.log(` Collateral: $${collateralUsd}`)
929
- console.log(` Entry: $${entryPrice}`)
930
-
931
- console.log(' Sending transaction...')
932
- const { address, explorer } = await sendContractTx({
933
- to: net.perpsRouter,
934
- abi: PERPS_ROUTER_ABI,
935
- functionName: 'decreasePosition',
936
- args: [net.rlusd, net.wxrp, 0n, size, isLong, account.address, priceLimit],
937
- })
938
-
939
- console.log(`\n ${side} closed by ${address}`)
940
- console.log(` TX: ${explorer}`)
958
+ return data.url
941
959
  }
942
960
 
943
- async function cmdTradePosition() {
944
- const viem = await loadViem()
945
- const net = requirePerps()
946
- const { account } = await getWalletClient()
947
- const { markPrice } = await getXrpPrice()
948
-
949
- const [longPos, shortPos] = await Promise.all([
950
- readContract({ address: net.vault, abi: VAULT_ABI, functionName: 'getPosition', args: [account.address, net.rlusd, net.wxrp, true] }),
951
- readContract({ address: net.vault, abi: VAULT_ABI, functionName: 'getPosition', args: [account.address, net.rlusd, net.wxrp, false] }),
952
- ])
953
-
954
- const currentPrice = parseFloat(viem.formatUnits(markPrice, 30)).toFixed(4)
955
- console.log(`\n Positions for ${account.address}`)
956
- console.log(` XRP: $${currentPrice}`)
957
- console.log(` ─────────────────────────────────────`)
958
-
959
- let hasPosition = false
960
-
961
- for (const [label, pos, isLong] of [['LONG', longPos, true], ['SHORT', shortPos, false]]) {
962
- const [size, collateral, avgPrice, , , realisedPnl, hasProfit] = pos
963
- if (size === 0n) continue
964
- hasPosition = true
965
-
966
- const sizeUsd = parseFloat(viem.formatUnits(size, 30))
967
- const collateralUsd = parseFloat(viem.formatUnits(collateral, 30))
968
- const entry = parseFloat(viem.formatUnits(avgPrice, 30))
969
- const leverage = sizeUsd / collateralUsd
970
- const mark = parseFloat(viem.formatUnits(markPrice, 30))
971
-
972
- // Calculate unrealized P&L
973
- let pnl
974
- if (isLong) {
975
- pnl = sizeUsd * (mark - entry) / entry
976
- } else {
977
- pnl = sizeUsd * (entry - mark) / entry
978
- }
979
- const pnlPct = (pnl / collateralUsd * 100)
980
-
981
- console.log(`\n ${label}`)
982
- console.log(` Size: $${sizeUsd.toFixed(2)}`)
983
- console.log(` Collateral: $${collateralUsd.toFixed(2)}`)
984
- console.log(` Leverage: ${leverage.toFixed(1)}x`)
985
- console.log(` Entry: $${entry.toFixed(4)}`)
986
- console.log(` PnL: ${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`)
961
+ async function uploadImageFile(filePath) {
962
+ const fs = require('fs')
963
+ const path = require('path')
964
+ if (!fs.existsSync(filePath)) throw new Error(`Image file not found: ${filePath}`)
965
+ const buf = fs.readFileSync(filePath)
966
+ if (buf.length > 2 * 1024 * 1024) throw new Error(`Image too large (${(buf.length / 1048576).toFixed(2)}MB; max 2MB).`)
967
+ const ext = path.extname(filePath).slice(1).toLowerCase()
968
+ const type = IMAGE_MIME[ext]
969
+ if (!type) throw new Error(`Unsupported image type ".${ext}". Use PNG, JPEG, WebP, or GIF.`)
970
+ console.log(c.dim(' Uploading image to Arweave (permanent)…'))
971
+ const form = new FormData()
972
+ form.append('image', new Blob([buf], { type }), path.basename(filePath))
973
+ let res
974
+ try {
975
+ res = await fetch(`${UPLOAD_BASE}/api/upload`, { method: 'POST', body: form })
976
+ } catch (e) {
977
+ throw new Error(`Could not reach the image uploader: ${e.message}`)
987
978
  }
979
+ const data = await res.json().catch(() => ({}))
980
+ if (!res.ok || !data.url) throw new Error(`Image upload failed: ${data.error || ('HTTP ' + res.status)}.`)
981
+ return data.url
982
+ }
988
983
 
989
- if (!hasPosition) {
990
- console.log('\n No open positions.')
991
- console.log(`\n Open one: blumefi trade long 100 5x`)
984
+ async function checkImageResolves(url) {
985
+ try {
986
+ const ctrl = new AbortController()
987
+ const t = setTimeout(() => ctrl.abort(), 8000)
988
+ const res = await fetch(url, { signal: ctrl.signal, redirect: 'follow' })
989
+ clearTimeout(t)
990
+ if (!res.ok) return { ok: false, reason: `URL returned HTTP ${res.status}` }
991
+ const ct = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
992
+ if (!ct.startsWith('image/')) return { ok: false, reason: `content-type is "${ct || 'unknown'}", not an image` }
993
+ return { ok: true }
994
+ } catch {
995
+ return { ok: null, reason: 'network error reaching the URL' }
992
996
  }
993
997
  }
994
998
 
999
+ // Resolve the final on-chain image URI from the caller's flags, uploading to
1000
+ // permanent Arweave when given a file or remote URL. Returns '' when no image
1001
+ // was supplied. Throws (hard fail) on placeholders or confirmed-bad URLs.
1002
+ async function resolveLaunchImage() {
1003
+ const file = flagVal('--image-file')
1004
+ const remote = flagVal('--image-url')
1005
+ const raw = flagVal('--image')
1006
+
1007
+ if (file) return await uploadImageFile(file)
1008
+ if (remote) {
1009
+ if (looksLikePlaceholder(remote)) throw new Error(placeholderMsg(remote))
1010
+ return await uploadImageFromUrl(remote)
1011
+ }
1012
+ if (raw) {
1013
+ if (looksLikePlaceholder(raw)) throw new Error(placeholderMsg(raw))
1014
+ if (!/^https?:\/\//i.test(raw)) throw new Error('--image must be an https URL. To upload a local file use --image-file <path>; to re-host a remote image use --image-url <url>.')
1015
+ const chk = await checkImageResolves(raw)
1016
+ if (chk.ok === false) throw new Error(`That --image URL is not usable: ${chk.reason}. Upload a real one with --image-file <path> or --image-url <url>, or pass --no-image to launch without a logo.`)
1017
+ if (chk.ok === null) console.log(c.yellow(` Warning: couldn't verify the image URL (${chk.reason}). The on-chain image is permanent — make sure it's correct.`))
1018
+ if (!/arweave\.net\//i.test(raw)) console.log(c.dim(' Tip: --image-url re-hosts to permanent Arweave so the logo can never rot.'))
1019
+ return raw
1020
+ }
1021
+ return ''
1022
+ }
1023
+
995
1024
  // ─── Pad commands (launchpad) ────────────────────────────────────────
996
1025
 
997
1026
  async function cmdPadLaunch(name, symbol, desc) {
@@ -999,7 +1028,10 @@ async function cmdPadLaunch(name, symbol, desc) {
999
1028
  console.error('Usage: blumefi pad launch <name> <symbol> [description]')
1000
1029
  console.error(' blumefi pad launch "My Token" MTK "A cool meme token"')
1001
1030
  console.error('\nOptions:')
1002
- console.error(' --image <url> Image URI (e.g. arweave URL)')
1031
+ console.error(' --image-file <path> Upload a local image (PNG/JPEG/WebP/GIF, <2MB) — recommended')
1032
+ console.error(' --image-url <url> Re-host a remote image to permanent Arweave')
1033
+ console.error(' --image <arweaveUrl> Use an already-hosted image URL')
1034
+ console.error(' --no-image Launch without a logo (permanent; not recommended)')
1003
1035
  console.error(' --supply <amount> Total supply (default: 1000000000)')
1004
1036
  console.error(' --dev-pct <0-10> Dev allocation % (default: 0)')
1005
1037
  console.error(' --grad <xrp> Graduation reserve in XRP (default: 500)')
@@ -1017,8 +1049,30 @@ async function cmdPadLaunch(name, symbol, desc) {
1017
1049
 
1018
1050
  // Parse optional flags
1019
1051
  const argv = process.argv
1020
- const imageIdx = argv.indexOf('--image')
1021
- const imageURI = imageIdx !== -1 ? argv[imageIdx + 1] || '' : ''
1052
+ const noImage = argv.includes('--no-image')
1053
+ const imageURI = await resolveLaunchImage()
1054
+
1055
+ // A token's logo is written on-chain at launch and is permanent. Refuse to
1056
+ // ship a logoless token unless the caller explicitly opts in with --no-image.
1057
+ // (Both exits — add an image, or --no-image — are achievable, so this guides
1058
+ // rather than traps.)
1059
+ if (!imageURI && !noImage) {
1060
+ console.error('')
1061
+ console.error(c.red(' Refusing to launch without an image.'))
1062
+ console.error('')
1063
+ console.error(` A token's image is written on-chain at launch and is permanent. Without`)
1064
+ console.error(` one, ${symbol} will have no logo in wallets, explorers, or aggregators —`)
1065
+ console.error(` forever. (Setting one later only updates the Blume listing, not external apps.)`)
1066
+ console.error('')
1067
+ console.error(' Add a real image now:')
1068
+ console.error(' --image-file <path> Upload a local image (PNG/JPEG/WebP/GIF, <2MB)')
1069
+ console.error(' --image-url <url> Re-host a remote image to permanent Arweave')
1070
+ console.error('')
1071
+ console.error(' Or, to launch without a logo on purpose:')
1072
+ console.error(' --no-image')
1073
+ console.error('')
1074
+ process.exit(1)
1075
+ }
1022
1076
  const supplyIdx = argv.indexOf('--supply')
1023
1077
  const totalSupply = supplyIdx !== -1
1024
1078
  ? viem.parseEther(argv[supplyIdx + 1] || '1000000000')
@@ -1047,13 +1101,19 @@ async function cmdPadLaunch(name, symbol, desc) {
1047
1101
  if (imageURI) {
1048
1102
  console.log(` Image: ${imageURI}`)
1049
1103
  } else {
1050
- console.log(` Image: (none consider adding --image <url>)`)
1104
+ console.log(c.yellow(` Image: NONE (no logo on external apps, permanent)`))
1051
1105
  }
1052
1106
  console.log(` Supply: ${viem.formatEther(totalSupply)}`)
1053
1107
  console.log(` Dev alloc: ${devPct}%`)
1054
1108
  console.log(` Grad target: ${viem.formatEther(gradReserve)} XRP`)
1055
1109
  console.log(` Fee: ${feeXrp} XRP`)
1056
1110
 
1111
+ if (!imageURI) {
1112
+ console.log('')
1113
+ console.log(c.yellow(` Launching with NO image — ${symbol} will show no logo on wallets,`))
1114
+ console.log(c.yellow(` explorers, and aggregators, permanently.`))
1115
+ }
1116
+
1057
1117
  console.log(' Sending transaction...')
1058
1118
  const { client, account } = await getWalletClient()
1059
1119
  const data = viem.encodeFunctionData({
@@ -1087,7 +1147,8 @@ async function cmdPadLaunch(name, symbol, desc) {
1087
1147
  if (tokenAddress) {
1088
1148
  console.log(`\n Next steps:`)
1089
1149
  if (!imageURI) {
1090
- console.log(` blumefi pad update-image ${tokenAddress} <url> Add an image`)
1150
+ console.log(` blumefi pad update-image ${tokenAddress} <file|url> Set Blume listing image`)
1151
+ console.log(c.dim(` (Blume site only — the on-chain image is fixed at launch and stays empty)`))
1091
1152
  }
1092
1153
  console.log(` blumefi pad buy ${tokenAddress} 10 Buy with 10 XRP`)
1093
1154
  console.log(` blumefi pad info ${tokenAddress} View token info`)
@@ -1480,18 +1541,38 @@ async function cmdPadStats() {
1480
1541
  console.log('')
1481
1542
  }
1482
1543
 
1483
- async function cmdPadUpdateImage(tokenAddress, imageUrl) {
1484
- if (!tokenAddress || !imageUrl) {
1485
- console.error('Usage: blumefi pad update-image <token_address> <image_url>')
1486
- console.error(' blumefi pad update-image 0x1234... https://arweave.net/TXID')
1544
+ async function cmdPadUpdateImage(tokenAddress, imageArg) {
1545
+ const fileFlag = flagVal('--image-file')
1546
+ const urlFlag = flagVal('--image-url')
1547
+ if (!tokenAddress || (!imageArg && !fileFlag && !urlFlag)) {
1548
+ console.error('Usage: blumefi pad update-image <token_address> <file|url>')
1549
+ console.error(' blumefi pad update-image 0x1234... ./logo.png')
1487
1550
  console.error(' blumefi pad update-image 0x1234... https://example.com/img.png')
1551
+ console.error(' blumefi pad update-image 0x1234... --image-file ./logo.png')
1488
1552
  console.error('\nOnly the token creator can update the image.')
1553
+ console.error('Note: this updates the Blume listing only — the on-chain image set at')
1554
+ console.error('launch is permanent and cannot be changed.')
1489
1555
  process.exit(1)
1490
1556
  }
1491
- if (!imageUrl.startsWith('https://')) {
1492
- console.error('Error: Image URL must start with https://')
1557
+
1558
+ // Resolve to a permanent hosted URL: upload local files / re-host remote URLs.
1559
+ let imageUrl
1560
+ if (fileFlag) {
1561
+ imageUrl = await uploadImageFile(fileFlag)
1562
+ } else if (urlFlag) {
1563
+ if (looksLikePlaceholder(urlFlag)) { console.error(`\n Error: ${placeholderMsg(urlFlag)}`); process.exit(1) }
1564
+ imageUrl = await uploadImageFromUrl(urlFlag)
1565
+ } else if (looksLikePlaceholder(imageArg)) {
1566
+ console.error(`\n Error: ${placeholderMsg(imageArg)}`); process.exit(1)
1567
+ } else if (require('fs').existsSync(imageArg)) {
1568
+ imageUrl = await uploadImageFile(imageArg)
1569
+ } else if (/^https?:\/\//i.test(imageArg)) {
1570
+ imageUrl = /arweave\.net\//i.test(imageArg) ? imageArg : await uploadImageFromUrl(imageArg)
1571
+ } else {
1572
+ console.error('Error: provide a local image file path or an https:// URL.')
1493
1573
  process.exit(1)
1494
1574
  }
1575
+
1495
1576
  const viem = await loadViem()
1496
1577
  const key = getPrivateKey()
1497
1578
  const account = viem.privateKeyToAccount(key)
@@ -1508,10 +1589,422 @@ async function cmdPadUpdateImage(tokenAddress, imageUrl) {
1508
1589
  console.error(`\n Error: ${data.error || `API error ${res.status}`}`)
1509
1590
  process.exit(1)
1510
1591
  }
1511
- console.log(`\n Token image updated!`)
1592
+ console.log(`\n Blume listing image updated!`)
1512
1593
  console.log(` Token: ${tokenAddress}`)
1513
1594
  console.log(` Image: ${imageUrl}`)
1514
1595
  console.log(` Creator: ${account.address}`)
1596
+ console.log(c.dim(`\n Note: this updates the Blume listing only. The image written on-chain at`))
1597
+ console.log(c.dim(` launch is immutable, so wallets/explorers/aggregators are unaffected.`))
1598
+ }
1599
+
1600
+ // ─── Lend commands (BlumeLend — XRP/USDC market) ─────────────────────
1601
+
1602
+ async function _readLendPosition(addr) {
1603
+ const id = await lendMarketId()
1604
+ const net = getNetwork()
1605
+ const [marketRaw, positionRaw, oraclePrice] = await Promise.all([
1606
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'market', args: [id] }),
1607
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'position', args: [id, addr] }),
1608
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'price', args: [] }),
1609
+ ])
1610
+ return {
1611
+ market: { tsa: marketRaw[0], tss: marketRaw[1], tba: marketRaw[2], tbs: marketRaw[3], lastUpdate: marketRaw[4], fee: marketRaw[5] },
1612
+ position: { supplyShares: positionRaw[0], borrowShares: positionRaw[1], collateral: positionRaw[2] },
1613
+ oraclePrice,
1614
+ }
1615
+ }
1616
+
1617
+ function _supplyAssets(supplyShares, tsa, tss) {
1618
+ if (tss === 0n || supplyShares === 0n) return 0n
1619
+ return (supplyShares * tsa) / tss
1620
+ }
1621
+
1622
+ function _borrowAssets(borrowShares, tba, tbs) {
1623
+ if (tbs === 0n || borrowShares === 0n) return 0n
1624
+ return (borrowShares * tba) / tbs
1625
+ }
1626
+
1627
+ // HF = collateral × oraclePrice ÷ 1e36 × lltv ÷ 1e18 ÷ borrowAssets.
1628
+ // Mirrors Morpho Blue's _isHealthy. Returns Infinity when borrow is 0.
1629
+ function _healthFactor(collateral, oraclePrice, lltv, borrowAssets) {
1630
+ if (borrowAssets === 0n) return Infinity
1631
+ const SCALE = 10n ** 54n // 1e36 (oracle) × 1e18 (lltv WAD)
1632
+ const num = collateral * oraclePrice * lltv * 10000n
1633
+ const denom = borrowAssets * SCALE
1634
+ return Number(num / denom) / 10000
1635
+ }
1636
+
1637
+ async function cmdLendPosition(addr) {
1638
+ const viem = await loadViem()
1639
+ const net = getNetwork()
1640
+ let user = addr
1641
+ if (!user) {
1642
+ const { account } = await getWalletClient()
1643
+ user = account.address
1644
+ }
1645
+ if (!user.startsWith('0x') || user.length !== 42) {
1646
+ console.error(`Usage: blumefi lend position [address]`)
1647
+ process.exit(1)
1648
+ }
1649
+ // Lowercase to bypass viem's EIP-55 checksum check on mis-cased input.
1650
+ user = user.toLowerCase()
1651
+ const { market, position, oraclePrice } = await _readLendPosition(user)
1652
+ const usdcDec = net.lendUsdcDecimals
1653
+ const supply = _supplyAssets(position.supplyShares, market.tsa, market.tss)
1654
+ const borrow = _borrowAssets(position.borrowShares, market.tba, market.tbs)
1655
+ const hf = _healthFactor(position.collateral, oraclePrice, BigInt(net.lendLltv), borrow)
1656
+
1657
+ if (isJsonMode()) {
1658
+ console.log(JSON.stringify({
1659
+ network: getChain(),
1660
+ address: user,
1661
+ suppliedUsdc: viem.formatUnits(supply, usdcDec),
1662
+ collateralWxrp: viem.formatUnits(position.collateral, 18),
1663
+ borrowedUsdc: viem.formatUnits(borrow, usdcDec),
1664
+ healthFactor: hf === Infinity ? null : hf,
1665
+ raw: {
1666
+ supplyShares: position.supplyShares.toString(),
1667
+ borrowShares: position.borrowShares.toString(),
1668
+ collateral: position.collateral.toString(),
1669
+ },
1670
+ }, null, 2))
1671
+ return
1672
+ }
1673
+
1674
+ const hfStr = hf === Infinity
1675
+ ? c.dim('∞ (no debt)')
1676
+ : hf < 1.05 ? c.red(hf.toFixed(3) + ' (near liquidation)')
1677
+ : hf < 1.30 ? c.yellow(hf.toFixed(3))
1678
+ : c.green(hf.toFixed(3))
1679
+
1680
+ console.log(`\n ${c.bold('Position')} ${c.dim(user.slice(0,10) + '…' + user.slice(-6))} ${c.dim(getChain())}`)
1681
+ console.log(` ─────────────`)
1682
+ console.log(` Supplied: ${viem.formatUnits(supply, usdcDec)} USDC`)
1683
+ console.log(` Collateral: ${viem.formatUnits(position.collateral, 18)} WXRP`)
1684
+ console.log(` Borrowed: ${viem.formatUnits(borrow, usdcDec)} USDC`)
1685
+ console.log(` Health: ${hfStr}`)
1686
+ }
1687
+
1688
+ async function cmdLendMarket() {
1689
+ const viem = await loadViem()
1690
+ const net = getNetwork()
1691
+ const id = await lendMarketId()
1692
+ const [marketRaw, oraclePrice, oracleScale] = await Promise.all([
1693
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'market', args: [id] }),
1694
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'price', args: [] }),
1695
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'SCALE_FACTOR', args: [] }),
1696
+ ])
1697
+ const tsa = marketRaw[0], tba = marketRaw[2], fee = marketRaw[5]
1698
+ const usdcDec = net.lendUsdcDecimals
1699
+ const utilization = tsa === 0n ? 0 : Number((tba * 10000n) / tsa) / 100
1700
+ // bandPriceWad = oraclePrice / SCALE_FACTOR — Band's USD price in 1e18 fixed point.
1701
+ const bandPriceWad = oracleScale === 0n ? 0n : oraclePrice / oracleScale
1702
+ const xrpUsd = Number(viem.formatUnits(bandPriceWad, 18))
1703
+ const feePct = (Number(fee) / 1e18 * 100)
1704
+
1705
+ if (isJsonMode()) {
1706
+ console.log(JSON.stringify({
1707
+ network: getChain(),
1708
+ pair: 'XRP / USDC',
1709
+ marketId: id,
1710
+ totalSupplyUsdc: viem.formatUnits(tsa, usdcDec),
1711
+ totalBorrowUsdc: viem.formatUnits(tba, usdcDec),
1712
+ utilizationPct: utilization,
1713
+ xrpUsd,
1714
+ lltvPct: 86,
1715
+ protocolFeePct: feePct,
1716
+ raw: {
1717
+ totalSupplyAssets: tsa.toString(),
1718
+ totalSupplyShares: marketRaw[1].toString(),
1719
+ totalBorrowAssets: tba.toString(),
1720
+ totalBorrowShares: marketRaw[3].toString(),
1721
+ lastUpdate: marketRaw[4].toString(),
1722
+ fee: fee.toString(),
1723
+ oraclePrice: oraclePrice.toString(),
1724
+ oracleScale: oracleScale.toString(),
1725
+ },
1726
+ }, null, 2))
1727
+ return
1728
+ }
1729
+
1730
+ console.log(`\n ${c.bold('BlumeLend — XRP / USDC')} ${c.dim(getChain())}`)
1731
+ console.log(` ─────────────`)
1732
+ console.log(` Total supply: ${fmtNum(viem.formatUnits(tsa, usdcDec))} USDC`)
1733
+ console.log(` Total borrow: ${fmtNum(viem.formatUnits(tba, usdcDec))} USDC`)
1734
+ console.log(` Utilization: ${utilization.toFixed(2)}%`)
1735
+ console.log(` XRP/USD: $${xrpUsd.toFixed(4)} ${c.dim('(Band oracle)')}`)
1736
+ console.log(` LLTV: 86%`)
1737
+ console.log(` Protocol fee: ${feePct.toFixed(2)}% of accrued interest`)
1738
+ }
1739
+
1740
+ async function cmdLendAudit() {
1741
+ const viem = await loadViem()
1742
+ const net = getNetwork()
1743
+ const chain = getChain()
1744
+ const id = await lendMarketId()
1745
+ const pub = await getPublicClient()
1746
+
1747
+ const [
1748
+ marketRaw, oraclePrice, oracleScale, oracleMaxAge, oracleBaseSym, oracleQuoteSym, bandAddress,
1749
+ owner, feeRecipient, latestBlock,
1750
+ ] = await Promise.all([
1751
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'market', args: [id] }),
1752
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'price', args: [] }),
1753
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'SCALE_FACTOR', args: [] }),
1754
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'MAX_PRICE_AGE', args: [] }),
1755
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'baseSymbol', args: [] }),
1756
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'quoteSymbol', args: [] }),
1757
+ readContract({ address: net.blumelendOracle, abi: ORACLE_ABI, functionName: 'BAND_ORACLE', args: [] }),
1758
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'owner', args: [] }),
1759
+ readContract({ address: net.blumelend, abi: BLUMELEND_ABI, functionName: 'feeRecipient', args: [] }),
1760
+ pub.getBlock(),
1761
+ ])
1762
+
1763
+ const refData = await readContract({
1764
+ address: bandAddress, abi: BAND_REF_ABI, functionName: 'getReferenceData',
1765
+ args: [oracleBaseSym, oracleQuoteSym],
1766
+ })
1767
+ // refData = (rate, lastUpdatedBase, lastUpdatedQuote)
1768
+
1769
+ const stalenessSec = Number(latestBlock.timestamp - refData[1])
1770
+ const isFresh = stalenessSec >= 0 && stalenessSec <= Number(oracleMaxAge)
1771
+
1772
+ const tsa = marketRaw[0], tba = marketRaw[2], fee = marketRaw[5]
1773
+ const utilization = tsa === 0n ? 0 : Number((tba * 10000n) / tsa) / 100
1774
+ const bandPriceWad = oracleScale === 0n ? 0n : oraclePrice / oracleScale
1775
+ const xrpUsd = Number(viem.formatUnits(bandPriceWad, 18))
1776
+ const feePct = Number(fee) / 1e18 * 100
1777
+
1778
+ const result = {
1779
+ network: chain,
1780
+ chainId: net.chainId,
1781
+ rpc: net.rpc,
1782
+ explorer: net.explorer,
1783
+ marketId: id,
1784
+ contracts: {
1785
+ BlumeLend: { address: net.blumelend, explorer: `${net.explorer}/address/${net.blumelend}`, sourceVerified: true, verificationNote: 'Blockscout is_partially_verified=true (cancun→shanghai metadata workaround); source IS visible.' },
1786
+ AdaptiveCurveIrm: { address: net.blumelendIrm, explorer: `${net.explorer}/address/${net.blumelendIrm}` },
1787
+ BandOracleAdapter: { address: net.blumelendOracle, explorer: `${net.explorer}/address/${net.blumelendOracle}`, sourceVerified: true },
1788
+ BandStdReferenceProxy: { address: bandAddress, explorer: `${net.explorer}/address/${bandAddress}`, sourceVerified: true },
1789
+ LoanToken: { address: net.lendUsdc, decimals: net.lendUsdcDecimals },
1790
+ Collateral: { address: net.lendCollateral, decimals: 18 },
1791
+ },
1792
+ lineage: {
1793
+ base: 'Morpho Blue',
1794
+ coreSelectorsIdentical: true,
1795
+ callbackRenaming: 'onMorpho* → onBlumeLend*',
1796
+ note: 'Callbacks live on the caller, not on the BlumeLend contract — they are not "missing selectors" in the BlumeLend bytecode.',
1797
+ },
1798
+ oracle: {
1799
+ type: 'band-protocol-std-reference',
1800
+ adapter: net.blumelendOracle,
1801
+ adapterFunction: 'price()',
1802
+ adapterSelector: '0xa035b1fe',
1803
+ upstream: bandAddress,
1804
+ upstreamFunction: 'getReferenceData(string,string)',
1805
+ upstreamSelector: '0x65555bcc',
1806
+ pair: `${oracleBaseSym}/${oracleQuoteSym}`,
1807
+ priceUsd: xrpUsd,
1808
+ maxPriceAgeSec: Number(oracleMaxAge),
1809
+ lastUpdatedBase: Number(refData[1]),
1810
+ currentBlockTimestamp: Number(latestBlock.timestamp),
1811
+ stalenessSec,
1812
+ fresh: isFresh,
1813
+ },
1814
+ market: {
1815
+ pair: 'XRP / USDC',
1816
+ lltvPct: 86,
1817
+ lltvWad: net.lendLltv,
1818
+ protocolFeePct: feePct,
1819
+ protocolFeeMaxPct: 25,
1820
+ totalSupplyAssetsUsdc: viem.formatUnits(tsa, net.lendUsdcDecimals),
1821
+ totalBorrowAssetsUsdc: viem.formatUnits(tba, net.lendUsdcDecimals),
1822
+ utilizationPct: utilization,
1823
+ },
1824
+ governance: {
1825
+ owner,
1826
+ feeRecipient,
1827
+ privileges: ['enableIrm(address)', 'enableLltv(uint256)', 'setFee(MarketParams,uint256) — capped at 25%', 'setFeeRecipient(address)'],
1828
+ cannotDo: ['pause', 'freeze positions', 'modify existing markets', 'upgrade', 'seize user funds'],
1829
+ },
1830
+ commonProbeFailures: [
1831
+ 'latestAnswer() (0x50d25bcd) and latestRoundData() (0xfeaf968c) are Chainlink AggregatorV3 selectors. This oracle is Band Protocol — use price() (0xa035b1fe) on the adapter or getReferenceData(string,string) (0x65555bcc) on the upstream.',
1832
+ 'Selector 0x8b95dd9b is NOT market(bytes32). The real market(bytes32) selector is 0x5c60e39a — verify with `cast sig "market(bytes32)"`.',
1833
+ 'Blockscout is_partially_verified=true means source IS visible (cancun→shanghai metadata workaround), not unverified.',
1834
+ 'Callbacks renamed onMorpho* → onBlumeLend* live on the caller, not on the BlumeLend contract.',
1835
+ ],
1836
+ }
1837
+
1838
+ if (isJsonMode()) {
1839
+ console.log(JSON.stringify(result, null, 2))
1840
+ return
1841
+ }
1842
+
1843
+ const freshTag = isFresh ? c.green(`fresh (${stalenessSec}s old, max ${oracleMaxAge}s)`) : c.red(`STALE (${stalenessSec}s old, max ${oracleMaxAge}s)`)
1844
+
1845
+ console.log(`\n ${c.bold('BlumeLend audit primer')} ${c.dim(`${chain} · chainId ${net.chainId}`)}`)
1846
+ console.log(` ─────────────`)
1847
+ console.log(`\n ${c.bold('Contracts')} (verified on Blockscout — partial verification = source IS visible):`)
1848
+ console.log(` BlumeLend ${net.blumelend}`)
1849
+ console.log(` ${c.dim(net.explorer + '/address/' + net.blumelend)}`)
1850
+ console.log(` BandOracleAdapter ${net.blumelendOracle}`)
1851
+ console.log(` ${c.dim(net.explorer + '/address/' + net.blumelendOracle)}`)
1852
+ console.log(` Band feed (proxy) ${bandAddress}`)
1853
+ console.log(` ${c.dim(net.explorer + '/address/' + bandAddress)}`)
1854
+ console.log(`\n ${c.bold('Lineage')}: Morpho Blue fork. Core selectors identical. Callbacks renamed onMorpho* → onBlumeLend*.`)
1855
+ console.log(`\n ${c.bold('Oracle')}: Band Protocol StdReferenceProxy ${c.dim('(NOT Chainlink — see "common probe failures" below)')}`)
1856
+ console.log(` pair: ${oracleBaseSym}/${oracleQuoteSym} current: $${xrpUsd.toFixed(4)} ${freshTag}`)
1857
+ console.log(`\n ${c.bold('Active market')} ${id.slice(0,10)}…${id.slice(-6)}`)
1858
+ console.log(` totalSupply: ${fmtNum(viem.formatUnits(tsa, net.lendUsdcDecimals))} USDC`)
1859
+ console.log(` totalBorrow: ${fmtNum(viem.formatUnits(tba, net.lendUsdcDecimals))} USDC`)
1860
+ console.log(` utilization: ${utilization.toFixed(2)}%`)
1861
+ console.log(` LLTV: 86%`)
1862
+ console.log(` protocol fee: ${feePct.toFixed(2)}% ${c.dim('(cap 25%)')}`)
1863
+ console.log(`\n ${c.bold('Governance')}`)
1864
+ console.log(` owner / feeRecipient: ${owner}`)
1865
+ console.log(` ${c.dim('privileges: enableIrm, enableLltv, setFee≤25%, setFeeRecipient — cannot pause, freeze, modify, upgrade, or seize.')}`)
1866
+ console.log(`\n ${c.bold('Common probe failure modes')} ${c.dim("(don't be this agent)")}`)
1867
+ console.log(` - latestAnswer() / latestRoundData() are Chainlink — this oracle is Band.`)
1868
+ console.log(` - 0x8b95dd9b is NOT market(bytes32) (real: 0x5c60e39a).`)
1869
+ console.log(` - is_partially_verified = true means source IS visible.`)
1870
+ console.log(` - Callbacks renamed onMorpho* → onBlumeLend* live on the caller, not this contract.`)
1871
+ console.log(`\n ${c.dim('Run with --json for structured output. Full primer: ' + 'https://lend.blumefi.com/skill.md#quick-verify')}`)
1872
+ console.log()
1873
+ }
1874
+
1875
+ async function cmdLendSupply(amountStr) {
1876
+ if (!amountStr) { console.error(`Usage: blumefi lend supply <amount>`); process.exit(1) }
1877
+ const viem = await loadViem()
1878
+ const net = getNetwork()
1879
+ const assets = viem.parseUnits(String(amountStr), net.lendUsdcDecimals)
1880
+ const { account } = await getWalletClient()
1881
+
1882
+ console.log(`\n Supplying ${amountStr} USDC...`)
1883
+ await ensureApproval(net.lendUsdc, net.blumelend, assets)
1884
+ const { explorer } = await sendContractTx({
1885
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'supply',
1886
+ args: [lendMarketParams(), assets, 0n, account.address, '0x'],
1887
+ })
1888
+ console.log(` ${c.green('✓ Supplied')} ${explorer}`)
1889
+ }
1890
+
1891
+ async function cmdLendWithdraw(amountStr) {
1892
+ if (!amountStr) { console.error(`Usage: blumefi lend withdraw <amount|all>`); process.exit(1) }
1893
+ const viem = await loadViem()
1894
+ const net = getNetwork()
1895
+ const { account } = await getWalletClient()
1896
+
1897
+ let assets = 0n, shares = 0n, displayAmount = `${amountStr} USDC`
1898
+ if (String(amountStr).toLowerCase() === 'all') {
1899
+ const { position } = await _readLendPosition(account.address)
1900
+ if (position.supplyShares === 0n) { console.error(` No supply position to withdraw.`); process.exit(1) }
1901
+ shares = position.supplyShares
1902
+ displayAmount = `ALL (${position.supplyShares} shares)`
1903
+ } else {
1904
+ assets = viem.parseUnits(String(amountStr), net.lendUsdcDecimals)
1905
+ }
1906
+
1907
+ console.log(`\n Withdrawing ${displayAmount}...`)
1908
+ const { explorer } = await sendContractTx({
1909
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'withdraw',
1910
+ args: [lendMarketParams(), assets, shares, account.address, account.address],
1911
+ })
1912
+ console.log(` ${c.green('✓ Withdrawn')} ${explorer}`)
1913
+ }
1914
+
1915
+ async function cmdLendSupplyCollateral(amountStr) {
1916
+ if (!amountStr) { console.error(`Usage: blumefi lend collateral <wxrp_amount>`); process.exit(1) }
1917
+ const viem = await loadViem()
1918
+ const net = getNetwork()
1919
+ const { account } = await getWalletClient()
1920
+ const assets = viem.parseUnits(String(amountStr), 18)
1921
+
1922
+ // Fail early if user needs to wrap XRP first — supplying needs WXRP balance.
1923
+ const balance = await readContract({ address: net.lendCollateral, abi: ERC20_ABI, functionName: 'balanceOf', args: [account.address] })
1924
+ if (balance < assets) {
1925
+ console.error(`\n Insufficient WXRP balance.`)
1926
+ console.error(` You have: ${viem.formatUnits(balance, 18)} WXRP`)
1927
+ console.error(` Required: ${amountStr} WXRP`)
1928
+ console.error(` Wrap XRP to WXRP first via: blumefi swap ${amountStr} XRP WXRP`)
1929
+ process.exit(1)
1930
+ }
1931
+
1932
+ console.log(`\n Supplying ${amountStr} WXRP as collateral...`)
1933
+ await ensureApproval(net.lendCollateral, net.blumelend, assets)
1934
+ const { explorer } = await sendContractTx({
1935
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'supplyCollateral',
1936
+ args: [lendMarketParams(), assets, account.address, '0x'],
1937
+ })
1938
+ console.log(` ${c.green('✓ Collateral supplied')} ${explorer}`)
1939
+ }
1940
+
1941
+ async function cmdLendWithdrawCollateral(amountStr) {
1942
+ if (!amountStr) { console.error(`Usage: blumefi lend remove-collateral <amount|all>`); process.exit(1) }
1943
+ const viem = await loadViem()
1944
+ const net = getNetwork()
1945
+ const { account } = await getWalletClient()
1946
+
1947
+ let assets, displayAmount
1948
+ if (String(amountStr).toLowerCase() === 'all') {
1949
+ const { position } = await _readLendPosition(account.address)
1950
+ if (position.collateral === 0n) { console.error(` No collateral to withdraw.`); process.exit(1) }
1951
+ assets = position.collateral
1952
+ displayAmount = `ALL (${viem.formatUnits(position.collateral, 18)} WXRP)`
1953
+ } else {
1954
+ assets = viem.parseUnits(String(amountStr), 18)
1955
+ displayAmount = `${amountStr} WXRP`
1956
+ }
1957
+
1958
+ console.log(`\n Withdrawing ${displayAmount} collateral...`)
1959
+ const { explorer } = await sendContractTx({
1960
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'withdrawCollateral',
1961
+ args: [lendMarketParams(), assets, account.address, account.address],
1962
+ })
1963
+ console.log(` ${c.green('✓ Collateral withdrawn')} ${explorer}`)
1964
+ }
1965
+
1966
+ async function cmdLendBorrow(amountStr) {
1967
+ if (!amountStr) { console.error(`Usage: blumefi lend borrow <amount>`); process.exit(1) }
1968
+ const viem = await loadViem()
1969
+ const net = getNetwork()
1970
+ const { account } = await getWalletClient()
1971
+ const assets = viem.parseUnits(String(amountStr), net.lendUsdcDecimals)
1972
+
1973
+ console.log(`\n Borrowing ${amountStr} USDC...`)
1974
+ const { explorer } = await sendContractTx({
1975
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'borrow',
1976
+ args: [lendMarketParams(), assets, 0n, account.address, account.address],
1977
+ })
1978
+ console.log(` ${c.green('✓ Borrowed')} ${explorer}`)
1979
+ }
1980
+
1981
+ async function cmdLendRepay(amountStr) {
1982
+ if (!amountStr) { console.error(`Usage: blumefi lend repay <amount|all>`); process.exit(1) }
1983
+ const viem = await loadViem()
1984
+ const net = getNetwork()
1985
+ const { account } = await getWalletClient()
1986
+
1987
+ let assets = 0n, shares = 0n, displayAmount
1988
+ if (String(amountStr).toLowerCase() === 'all') {
1989
+ const { market, position } = await _readLendPosition(account.address)
1990
+ if (position.borrowShares === 0n) { console.error(` No debt to repay.`); process.exit(1) }
1991
+ shares = position.borrowShares
1992
+ const owedAssets = _borrowAssets(position.borrowShares, market.tba, market.tbs)
1993
+ // Approve a 2% buffer to absorb interest accrual between read and tx.
1994
+ await ensureApproval(net.lendUsdc, net.blumelend, (owedAssets * 102n) / 100n)
1995
+ displayAmount = `${viem.formatUnits(owedAssets, net.lendUsdcDecimals)} USDC (full debt)`
1996
+ } else {
1997
+ assets = viem.parseUnits(String(amountStr), net.lendUsdcDecimals)
1998
+ await ensureApproval(net.lendUsdc, net.blumelend, assets)
1999
+ displayAmount = `${amountStr} USDC`
2000
+ }
2001
+
2002
+ console.log(`\n Repaying ${displayAmount}...`)
2003
+ const { explorer } = await sendContractTx({
2004
+ to: net.blumelend, abi: BLUMELEND_ABI, functionName: 'repay',
2005
+ args: [lendMarketParams(), assets, shares, account.address, '0x'],
2006
+ })
2007
+ console.log(` ${c.green('✓ Repaid')} ${explorer}`)
1515
2008
  }
1516
2009
 
1517
2010
  // ─── Balance command ─────────────────────────────────────────────────
@@ -1538,11 +2031,8 @@ async function cmdBalance(address) {
1538
2031
  console.log(` ─────────────────────────────────────`)
1539
2032
  console.log(` XRP: ${xrpFormatted}`)
1540
2033
 
1541
- // Check known tokens
1542
- const tokens = []
1543
- if (net.rlusd) tokens.push({ address: net.rlusd, name: 'RLUSD' })
1544
-
1545
2034
  // Check pad tokens the user might hold — query API for recent tokens
2035
+ const tokens = []
1546
2036
  try {
1547
2037
  const data = await apiFetch(`/pad/tokens?chain=${chain}&limit=50&sort=recent`)
1548
2038
  const padTokens = data.data || data.tokens || data || []
@@ -1607,9 +2097,9 @@ async function cmdFaucet(address) {
1607
2097
  console.log(`\n Sent 25 XRP to ${address}`)
1608
2098
  if (data.txHash) console.log(` TX: ${NETWORKS.testnet.explorer}/tx/${data.txHash}`)
1609
2099
  console.log(`\n Next steps:`)
1610
- console.log(` blumefi chat profile <name> Set your display name`)
1611
- console.log(` blumefi chat post "<message>" Say hello`)
1612
- console.log(` blumefi trade long 100 5x Open a leveraged position`)
2100
+ console.log(` blumefi chat profile <name> Set your display name`)
2101
+ console.log(` blumefi pad tokens Browse tokens to chat about`)
2102
+ console.log(` blumefi chat post <token> "<msg>" Post in a token's chat room`)
1613
2103
  }
1614
2104
 
1615
2105
  async function cmdStatus() {
@@ -1624,15 +2114,11 @@ async function cmdStatus() {
1624
2114
  console.log(` Chain ID: ${net.chainId}`)
1625
2115
  console.log(` RPC: ${net.rpc}`)
1626
2116
  console.log(` Explorer: ${net.explorer}`)
1627
- console.log(` AgentChat: ${net.agentChat}`)
2117
+ console.log(` AgentChatV2: ${net.agentChatV2}`)
1628
2118
  console.log(` WXRP: ${net.wxrp}`)
1629
2119
  console.log(` Swap Router: ${net.swapRouter}`)
1630
2120
  console.log(` Swap Factory: ${net.swapFactory}`)
1631
2121
  if (net.padFactory) console.log(` Pad Factory: ${net.padFactory}`)
1632
- if (net.perpsRouter) console.log(` Perps Router: ${net.perpsRouter}`)
1633
- if (net.vault) console.log(` Vault: ${net.vault}`)
1634
- if (net.priceFeed) console.log(` Price Feed: ${net.priceFeed}`)
1635
- if (net.rlusd) console.log(` RLUSD: ${net.rlusd}`)
1636
2122
  console.log(` ─────────────────────────────────────`)
1637
2123
  console.log(` API: ${API}`)
1638
2124
  console.log(` WebSocket: wss://api.blumefi.com/ws`)
@@ -1668,13 +2154,13 @@ BlumeFi CLI — DeFi reimagined for the agentic era
1668
2154
 
1669
2155
  Usage: blumefi <command> [options]
1670
2156
 
1671
- Chat:
1672
- chat feed Read the feed (root posts)
1673
- chat thread <id> Read a full thread with replies
1674
- chat post "<message>" Post a new message
1675
- chat reply <id> "<message>" Reply to a message
1676
- chat mentions [address] Check replies to your posts
1677
- chat profile <name> [options] Set your display name and avatar
2157
+ Chat (per-token rooms — AgentChatV2):
2158
+ chat feed <token> Read a token's chat feed
2159
+ chat thread <messageId> Read a full thread with replies
2160
+ chat post <token> "<message>" Post in a token's chat room
2161
+ chat reply <token> <messageId> "<msg>" Reply to a message in a token's room
2162
+ chat mentions [address] Check replies to your posts (global)
2163
+ chat profile <name> [options] Set your global display name and avatar
1678
2164
 
1679
2165
  Pad (Launchpad):
1680
2166
  pad tokens List tokens with graduation progress
@@ -1684,27 +2170,34 @@ Pad (Launchpad):
1684
2170
  pad buy <token> <xrp_amount> Buy tokens with XRP
1685
2171
  pad sell <token> <amount|all> Sell tokens for XRP
1686
2172
  pad search <query> Search tokens by name or symbol
1687
- pad update-image <token> <url> Update token image (creator only)
2173
+ pad update-image <token> <file|url> Set Blume listing image (creator; on-chain image is fixed at launch)
1688
2174
 
1689
2175
  Pad Options:
1690
2176
  --filter <active|graduated|all> Filter tokens (default: all)
1691
2177
  --sort <newest|marketcap|progress|price> Sort order (default: newest)
1692
2178
  --limit <N> Number of tokens (default: 20)
1693
2179
  --watch, -w Live dashboard (auto-refresh every 5s)
2180
+ --image-file <path> Launch: upload a logo (permanent on-chain; recommended)
2181
+ --image-url <url> Launch: re-host a remote logo to Arweave
2182
+ --no-image Launch: no logo (permanent; not recommended)
1694
2183
 
1695
2184
  Swap (DEX):
1696
- swap <amount> <from> <to> Swap tokens (e.g. swap 1 XRP RLUSD)
2185
+ swap <amount> <from> <to> Swap tokens (e.g. swap 1 XRP 0xTOKEN)
1697
2186
  swap quote <amount> <from> <to> Get a quote without executing
1698
2187
  swap pools List available pools
1699
2188
  swap add-liquidity <token> <xrp> Add liquidity to a token/XRP pool
1700
2189
  swap remove-liquidity <token> <lp|all> Remove liquidity from a pool
1701
2190
 
1702
- Trade (Perpstestnet):
1703
- trade long <usd> [leverage] Open long (e.g. trade long 100 5x)
1704
- trade short <usd> [leverage] Open short (e.g. trade short 50 10x)
1705
- trade close <long|short> Close a position
1706
- trade position View your open positions
1707
- trade price Get current XRP price
2191
+ Lend (Lending markets XRP/USDC):
2192
+ lend market Show market: total supply/borrow, utilization, price
2193
+ lend position [address] Show position: supplied, collateral, borrowed, HF
2194
+ lend audit One-shot chain-of-trust report (contracts, oracle, governance, lineage)
2195
+ lend supply <amount> Supply USDC to earn yield
2196
+ lend withdraw <amount|all> Withdraw supplied USDC
2197
+ lend collateral <amount> Supply WXRP as collateral (wrap XRP first)
2198
+ lend remove-collateral <amount|all> Withdraw WXRP collateral
2199
+ lend borrow <amount> Borrow USDC against your collateral
2200
+ lend repay <amount|all> Repay borrowed USDC
1708
2201
 
1709
2202
  Wallet & Account:
1710
2203
  wallet new Generate a new wallet
@@ -1715,6 +2208,7 @@ Wallet & Account:
1715
2208
  Options:
1716
2209
  --mainnet Use mainnet (default)
1717
2210
  --testnet Use testnet
2211
+ --json Structured output (lend market, lend position, lend audit)
1718
2212
 
1719
2213
  Environment:
1720
2214
  WALLET_PRIVATE_KEY Private key for signing transactions
@@ -1736,13 +2230,13 @@ async function main() {
1736
2230
  if (!cmd || cmd === 'help') {
1737
2231
  await cmdHelp()
1738
2232
  } else if (cmd === 'chat' || cmd === 'c') {
1739
- if (!sub || sub === 'feed' || sub === 'f') await cmdChatFeed()
2233
+ if (sub === 'feed' || sub === 'f') await cmdChatFeed(args[2])
1740
2234
  else if (sub === 'thread' || sub === 't') await cmdChatThread(args[2])
1741
- else if (sub === 'post' || sub === 'p') await cmdChatPost(args[2])
1742
- else if (sub === 'reply' || sub === 'r') await cmdChatReply(args[2], args[3])
2235
+ else if (sub === 'post' || sub === 'p') await cmdChatPost(args[2], args[3])
2236
+ else if (sub === 'reply' || sub === 'r') await cmdChatReply(args[2], args[3], args[4])
1743
2237
  else if (sub === 'mentions' || sub === 'm') await cmdChatMentions(args[2])
1744
2238
  else if (sub === 'profile') await cmdChatProfile(args[2])
1745
- else { console.error(`Unknown: chat ${sub}. Try: feed, thread, post, reply, mentions, profile`); process.exit(1) }
2239
+ else { console.error(`Usage: blumefi chat <feed|thread|post|reply|mentions|profile> ...`); console.error(` Run "blumefi help" for full signatures.`); process.exit(1) }
1746
2240
  } else if (cmd === 'pad' || cmd === 'p') {
1747
2241
  if (sub === 'launch' || sub === 'l' || sub === 'create') await cmdPadLaunch(args[2], args[3], args[4])
1748
2242
  else if (sub === 'buy' || sub === 'b') await cmdPadBuy(args[2], args[3])
@@ -1759,13 +2253,17 @@ async function main() {
1759
2253
  else if (sub === 'add-liquidity' || sub === 'al') await cmdSwapAddLiquidity(args[2], args[3])
1760
2254
  else if (sub === 'remove-liquidity' || sub === 'rl') await cmdSwapRemoveLiquidity(args[2], args[3])
1761
2255
  else await cmdSwap(sub, args[2], args[3]) // swap <amount> <from> <to>
1762
- } else if (cmd === 'trade' || cmd === 't') {
1763
- if (sub === 'long' || sub === 'l') await cmdTradeOpen(true, args[2], args[3])
1764
- else if (sub === 'short' || sub === 's') await cmdTradeOpen(false, args[2], args[3])
1765
- else if (sub === 'close' || sub === 'c') await cmdTradeClose(args[2])
1766
- else if (sub === 'position' || sub === 'pos' || sub === 'p') await cmdTradePosition()
1767
- else if (sub === 'price') await cmdTradePrice()
1768
- else { console.error(`Unknown: trade ${sub}. Try: long, short, close, position, price`); process.exit(1) }
2256
+ } else if (cmd === 'lend' || cmd === 'l') {
2257
+ if (sub === 'position' || sub === 'pos') await cmdLendPosition(args[2])
2258
+ else if (sub === 'market' || sub === 'm') await cmdLendMarket()
2259
+ else if (sub === 'audit' || sub === 'a') await cmdLendAudit()
2260
+ else if (sub === 'supply' || sub === 's') await cmdLendSupply(args[2])
2261
+ else if (sub === 'withdraw' || sub === 'w') await cmdLendWithdraw(args[2])
2262
+ else if (sub === 'collateral' || sub === 'col' || sub === 'c') await cmdLendSupplyCollateral(args[2])
2263
+ else if (sub === 'remove-collateral' || sub === 'rc') await cmdLendWithdrawCollateral(args[2])
2264
+ else if (sub === 'borrow' || sub === 'b') await cmdLendBorrow(args[2])
2265
+ else if (sub === 'repay' || sub === 'r') await cmdLendRepay(args[2])
2266
+ else { console.error(`Unknown: lend ${sub}. Try: position, market, audit, supply, withdraw, collateral, remove-collateral, borrow, repay`); process.exit(1) }
1769
2267
  } else if (cmd === 'wallet' || cmd === 'w') {
1770
2268
  if (!sub || sub === 'new') await cmdWalletNew()
1771
2269
  else { console.error(`Unknown: wallet ${sub}`); process.exit(1) }