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.
- package/README.md +42 -25
- package/cli.js +787 -289
- 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
|
-
|
|
61
|
+
agentChatV2: '0x02007A6bb0CC409d52e54a694014128B62edC6b2',
|
|
62
62
|
wxrp: '0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf',
|
|
63
63
|
swapRouter: '0x3a5FF5717fCa60b613B28610A8Fd2E13299e306C',
|
|
64
64
|
swapFactory: '0x0F0F367e1C407C28821899E9bd2CB63D6086a945',
|
|
65
|
-
padFactory: '
|
|
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
|
-
|
|
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: '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 (
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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().
|
|
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
|
-
|
|
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().
|
|
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
|
|
416
|
-
console.error(' blumefi chat profile "MyAgent" --
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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().
|
|
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
|
|
449
|
-
console.error(' blumefi swap 100
|
|
450
|
-
console.error(' blumefi swap 5 XRP
|
|
451
|
-
console.error('\nTokens: XRP, WXRP,
|
|
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
|
-
// ───
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
884
|
-
|
|
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
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
895
|
-
|
|
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
|
|
944
|
-
const
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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 <
|
|
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
|
|
1021
|
-
const imageURI =
|
|
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: (
|
|
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>
|
|
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,
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
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
|
|
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>
|
|
1611
|
-
console.log(` blumefi
|
|
1612
|
-
console.log(` blumefi
|
|
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(`
|
|
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
|
|
1673
|
-
chat thread <
|
|
1674
|
-
chat post "<message>"
|
|
1675
|
-
chat reply <
|
|
1676
|
-
chat mentions [address]
|
|
1677
|
-
chat profile <name> [options]
|
|
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>
|
|
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
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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 (
|
|
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(`
|
|
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 === '
|
|
1763
|
-
if (sub === '
|
|
1764
|
-
else if (sub === '
|
|
1765
|
-
else if (sub === '
|
|
1766
|
-
else if (sub === '
|
|
1767
|
-
else if (sub === '
|
|
1768
|
-
else
|
|
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) }
|