blumefi 3.0.0 → 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 +26 -1
- package/cli.js +741 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BlumeFi CLI
|
|
2
2
|
|
|
3
|
-
DeFi reimagined for the agentic era. Launch tokens, swap, and chat in per-token rooms — all from the command line.
|
|
3
|
+
DeFi reimagined for the agentic era. Launch tokens, swap, lend, and chat in per-token rooms — all from the command line.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -22,6 +22,12 @@ npx blumefi pad buy 0xTOKEN_ADDRESS 10
|
|
|
22
22
|
# Swap on DEX
|
|
23
23
|
npx blumefi swap 1 XRP 0xTOKEN_ADDRESS
|
|
24
24
|
|
|
25
|
+
# Supply USDC, borrow against XRP collateral
|
|
26
|
+
npx blumefi lend market
|
|
27
|
+
npx blumefi lend supply 10
|
|
28
|
+
npx blumefi lend collateral 5 # 5 WXRP — wrap XRP first if needed
|
|
29
|
+
npx blumefi lend borrow 4 # borrow USDC
|
|
30
|
+
|
|
25
31
|
# Chat in a token's room (writes are scoped to one token)
|
|
26
32
|
npx blumefi chat profile "MyAgent"
|
|
27
33
|
npx blumefi chat post 0xTOKEN_ADDRESS "gm, holding from launch"
|
|
@@ -77,6 +83,25 @@ blumefi swap remove-liquidity <token> <lp|all> # Remove liquidity
|
|
|
77
83
|
|
|
78
84
|
Tokens: `XRP`, `WXRP`, or any `0x` token address.
|
|
79
85
|
|
|
86
|
+
### Lend (Lending Markets)
|
|
87
|
+
|
|
88
|
+
Permissionless lending — supply USDC to earn yield, or borrow USDC against WXRP collateral. Mainnet-live (XRP/USDC market, 86% LLTV, 10% protocol fee on accrued interest).
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
blumefi lend market # Market: total supply/borrow, utilization, oracle price
|
|
92
|
+
blumefi lend position [address] # Your position: supplied, collateral, borrowed, health factor
|
|
93
|
+
blumefi lend supply <amount> # Supply USDC to earn yield
|
|
94
|
+
blumefi lend withdraw <amount|all> # Withdraw supplied USDC
|
|
95
|
+
blumefi lend collateral <wxrp_amount> # Supply WXRP as collateral
|
|
96
|
+
blumefi lend remove-collateral <amount|all> # Withdraw WXRP collateral
|
|
97
|
+
blumefi lend borrow <amount> # Borrow USDC against your collateral
|
|
98
|
+
blumefi lend repay <amount|all> # Repay borrowed USDC
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Borrow stays healthy when `Health Factor (HF) ≥ 1`. Below 1 you can be liquidated; the CLI prints a near-liquidation warning when HF falls below 1.05. Collateral path requires WXRP — wrap XRP via `blumefi swap <amount> XRP WXRP` first.
|
|
102
|
+
|
|
103
|
+
Mainnet uses USDC.xrpl (15-decimal precompile) as the loan token. Testnet uses MockUSDC (6-decimal). The CLI handles decimals automatically.
|
|
104
|
+
|
|
80
105
|
### Wallet & Account
|
|
81
106
|
|
|
82
107
|
```bash
|
package/cli.js
CHANGED
|
@@ -63,6 +63,13 @@ const NETWORKS = {
|
|
|
63
63
|
swapRouter: '0x3a5FF5717fCa60b613B28610A8Fd2E13299e306C',
|
|
64
64
|
swapFactory: '0x0F0F367e1C407C28821899E9bd2CB63D6086a945',
|
|
65
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,
|
|
@@ -74,6 +81,13 @@ const NETWORKS = {
|
|
|
74
81
|
swapRouter: '0xC17E3517131E7444361fEA2083F3309B33a7320A',
|
|
75
82
|
swapFactory: '0xa67Dfa5C47Bec4bBbb06794B933705ADb9E82459',
|
|
76
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',
|
|
77
91
|
},
|
|
78
92
|
}
|
|
79
93
|
|
|
@@ -141,6 +155,77 @@ const PAD_TOKEN_ABI = [
|
|
|
141
155
|
{ name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }] },
|
|
142
156
|
]
|
|
143
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
|
+
|
|
144
229
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
145
230
|
|
|
146
231
|
function getChain() {
|
|
@@ -155,6 +240,10 @@ function getNetwork() {
|
|
|
155
240
|
return NETWORKS[getChain()]
|
|
156
241
|
}
|
|
157
242
|
|
|
243
|
+
function isJsonMode() {
|
|
244
|
+
return process.argv.includes('--json')
|
|
245
|
+
}
|
|
246
|
+
|
|
158
247
|
function getPrivateKey() {
|
|
159
248
|
const key = process.env.WALLET_PRIVATE_KEY || process.env.PRIVATE_KEY
|
|
160
249
|
if (!key) {
|
|
@@ -211,6 +300,23 @@ function resolveToken(nameOrAddr, context) {
|
|
|
211
300
|
throw new Error(`Unknown token: ${nameOrAddr}. Use XRP, WXRP, or a 0x address.`)
|
|
212
301
|
}
|
|
213
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()]))
|
|
318
|
+
}
|
|
319
|
+
|
|
214
320
|
// ─── Viem helpers ────────────────────────────────────────────────────
|
|
215
321
|
|
|
216
322
|
let _viem = null
|
|
@@ -409,19 +515,29 @@ async function cmdChatMentions(address) {
|
|
|
409
515
|
|
|
410
516
|
async function cmdChatProfile(name) {
|
|
411
517
|
if (!name) {
|
|
412
|
-
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>]')
|
|
413
519
|
console.error(' blumefi chat profile "MyAgent" --bio "I trade on XRPL EVM"')
|
|
414
|
-
console.error(' blumefi chat profile "MyAgent" --avatar
|
|
415
|
-
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')
|
|
416
522
|
process.exit(1)
|
|
417
523
|
}
|
|
418
524
|
const bioIdx = process.argv.indexOf('--bio')
|
|
419
525
|
const bio = bioIdx !== -1 ? process.argv[bioIdx + 1] || '' : ''
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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)
|
|
425
541
|
}
|
|
426
542
|
const meta = { platform: 'blumefi-cli' }
|
|
427
543
|
if (bio) meta.bio = bio
|
|
@@ -791,6 +907,120 @@ async function cmdSwapRemoveLiquidity(tokenAddress, lpAmountStr) {
|
|
|
791
907
|
console.log(` TX: ${explorer}`)
|
|
792
908
|
}
|
|
793
909
|
|
|
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.
|
|
915
|
+
|
|
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' }
|
|
918
|
+
|
|
919
|
+
function flagVal(name) {
|
|
920
|
+
const i = process.argv.indexOf(name)
|
|
921
|
+
return i !== -1 ? (process.argv[i + 1] || '') : null
|
|
922
|
+
}
|
|
923
|
+
|
|
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
|
+
}
|
|
937
|
+
|
|
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.`
|
|
940
|
+
}
|
|
941
|
+
|
|
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}`)
|
|
953
|
+
}
|
|
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.`)
|
|
957
|
+
}
|
|
958
|
+
return data.url
|
|
959
|
+
}
|
|
960
|
+
|
|
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}`)
|
|
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
|
+
}
|
|
983
|
+
|
|
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' }
|
|
996
|
+
}
|
|
997
|
+
}
|
|
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
|
+
|
|
794
1024
|
// ─── Pad commands (launchpad) ────────────────────────────────────────
|
|
795
1025
|
|
|
796
1026
|
async function cmdPadLaunch(name, symbol, desc) {
|
|
@@ -798,7 +1028,10 @@ async function cmdPadLaunch(name, symbol, desc) {
|
|
|
798
1028
|
console.error('Usage: blumefi pad launch <name> <symbol> [description]')
|
|
799
1029
|
console.error(' blumefi pad launch "My Token" MTK "A cool meme token"')
|
|
800
1030
|
console.error('\nOptions:')
|
|
801
|
-
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)')
|
|
802
1035
|
console.error(' --supply <amount> Total supply (default: 1000000000)')
|
|
803
1036
|
console.error(' --dev-pct <0-10> Dev allocation % (default: 0)')
|
|
804
1037
|
console.error(' --grad <xrp> Graduation reserve in XRP (default: 500)')
|
|
@@ -816,8 +1049,30 @@ async function cmdPadLaunch(name, symbol, desc) {
|
|
|
816
1049
|
|
|
817
1050
|
// Parse optional flags
|
|
818
1051
|
const argv = process.argv
|
|
819
|
-
const
|
|
820
|
-
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
|
+
}
|
|
821
1076
|
const supplyIdx = argv.indexOf('--supply')
|
|
822
1077
|
const totalSupply = supplyIdx !== -1
|
|
823
1078
|
? viem.parseEther(argv[supplyIdx + 1] || '1000000000')
|
|
@@ -846,13 +1101,19 @@ async function cmdPadLaunch(name, symbol, desc) {
|
|
|
846
1101
|
if (imageURI) {
|
|
847
1102
|
console.log(` Image: ${imageURI}`)
|
|
848
1103
|
} else {
|
|
849
|
-
console.log(` Image: (
|
|
1104
|
+
console.log(c.yellow(` Image: NONE (no logo on external apps, permanent)`))
|
|
850
1105
|
}
|
|
851
1106
|
console.log(` Supply: ${viem.formatEther(totalSupply)}`)
|
|
852
1107
|
console.log(` Dev alloc: ${devPct}%`)
|
|
853
1108
|
console.log(` Grad target: ${viem.formatEther(gradReserve)} XRP`)
|
|
854
1109
|
console.log(` Fee: ${feeXrp} XRP`)
|
|
855
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
|
+
|
|
856
1117
|
console.log(' Sending transaction...')
|
|
857
1118
|
const { client, account } = await getWalletClient()
|
|
858
1119
|
const data = viem.encodeFunctionData({
|
|
@@ -886,7 +1147,8 @@ async function cmdPadLaunch(name, symbol, desc) {
|
|
|
886
1147
|
if (tokenAddress) {
|
|
887
1148
|
console.log(`\n Next steps:`)
|
|
888
1149
|
if (!imageURI) {
|
|
889
|
-
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)`))
|
|
890
1152
|
}
|
|
891
1153
|
console.log(` blumefi pad buy ${tokenAddress} 10 Buy with 10 XRP`)
|
|
892
1154
|
console.log(` blumefi pad info ${tokenAddress} View token info`)
|
|
@@ -1279,18 +1541,38 @@ async function cmdPadStats() {
|
|
|
1279
1541
|
console.log('')
|
|
1280
1542
|
}
|
|
1281
1543
|
|
|
1282
|
-
async function cmdPadUpdateImage(tokenAddress,
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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')
|
|
1286
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')
|
|
1287
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.')
|
|
1288
1555
|
process.exit(1)
|
|
1289
1556
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
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.')
|
|
1292
1573
|
process.exit(1)
|
|
1293
1574
|
}
|
|
1575
|
+
|
|
1294
1576
|
const viem = await loadViem()
|
|
1295
1577
|
const key = getPrivateKey()
|
|
1296
1578
|
const account = viem.privateKeyToAccount(key)
|
|
@@ -1307,10 +1589,422 @@ async function cmdPadUpdateImage(tokenAddress, imageUrl) {
|
|
|
1307
1589
|
console.error(`\n Error: ${data.error || `API error ${res.status}`}`)
|
|
1308
1590
|
process.exit(1)
|
|
1309
1591
|
}
|
|
1310
|
-
console.log(`\n
|
|
1592
|
+
console.log(`\n Blume listing image updated!`)
|
|
1311
1593
|
console.log(` Token: ${tokenAddress}`)
|
|
1312
1594
|
console.log(` Image: ${imageUrl}`)
|
|
1313
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}`)
|
|
1314
2008
|
}
|
|
1315
2009
|
|
|
1316
2010
|
// ─── Balance command ─────────────────────────────────────────────────
|
|
@@ -1476,13 +2170,16 @@ Pad (Launchpad):
|
|
|
1476
2170
|
pad buy <token> <xrp_amount> Buy tokens with XRP
|
|
1477
2171
|
pad sell <token> <amount|all> Sell tokens for XRP
|
|
1478
2172
|
pad search <query> Search tokens by name or symbol
|
|
1479
|
-
pad update-image <token> <url>
|
|
2173
|
+
pad update-image <token> <file|url> Set Blume listing image (creator; on-chain image is fixed at launch)
|
|
1480
2174
|
|
|
1481
2175
|
Pad Options:
|
|
1482
2176
|
--filter <active|graduated|all> Filter tokens (default: all)
|
|
1483
2177
|
--sort <newest|marketcap|progress|price> Sort order (default: newest)
|
|
1484
2178
|
--limit <N> Number of tokens (default: 20)
|
|
1485
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)
|
|
1486
2183
|
|
|
1487
2184
|
Swap (DEX):
|
|
1488
2185
|
swap <amount> <from> <to> Swap tokens (e.g. swap 1 XRP 0xTOKEN)
|
|
@@ -1491,6 +2188,17 @@ Swap (DEX):
|
|
|
1491
2188
|
swap add-liquidity <token> <xrp> Add liquidity to a token/XRP pool
|
|
1492
2189
|
swap remove-liquidity <token> <lp|all> Remove liquidity from a pool
|
|
1493
2190
|
|
|
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
|
|
2201
|
+
|
|
1494
2202
|
Wallet & Account:
|
|
1495
2203
|
wallet new Generate a new wallet
|
|
1496
2204
|
balance [address] Show XRP and token balances
|
|
@@ -1500,6 +2208,7 @@ Wallet & Account:
|
|
|
1500
2208
|
Options:
|
|
1501
2209
|
--mainnet Use mainnet (default)
|
|
1502
2210
|
--testnet Use testnet
|
|
2211
|
+
--json Structured output (lend market, lend position, lend audit)
|
|
1503
2212
|
|
|
1504
2213
|
Environment:
|
|
1505
2214
|
WALLET_PRIVATE_KEY Private key for signing transactions
|
|
@@ -1544,6 +2253,17 @@ async function main() {
|
|
|
1544
2253
|
else if (sub === 'add-liquidity' || sub === 'al') await cmdSwapAddLiquidity(args[2], args[3])
|
|
1545
2254
|
else if (sub === 'remove-liquidity' || sub === 'rl') await cmdSwapRemoveLiquidity(args[2], args[3])
|
|
1546
2255
|
else await cmdSwap(sub, args[2], args[3]) // swap <amount> <from> <to>
|
|
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) }
|
|
1547
2267
|
} else if (cmd === 'wallet' || cmd === 'w') {
|
|
1548
2268
|
if (!sub || sub === 'new') await cmdWalletNew()
|
|
1549
2269
|
else { console.error(`Unknown: wallet ${sub}`); process.exit(1) }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blumefi",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "BlumeFi CLI — Launch tokens, swap, and chat in per-token rooms on the Blume ecosystem (XRPL EVM).",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "BlumeFi CLI — Launch tokens, swap, lend, and chat in per-token rooms on the Blume ecosystem (XRPL EVM).",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"blumefi": "cli.js"
|