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.
Files changed (3) hide show
  1. package/README.md +26 -1
  2. package/cli.js +741 -21
  3. 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 https://example.com/pic.png')
415
- console.error(' blumefi chat profile "MyAgent" --bio "Bio" --avatar https://arweave.net/TXID')
520
+ console.error(' blumefi chat profile "MyAgent" --avatar-file ./avatar.png (uploaded to Arweave for you)')
521
+ console.error(' blumefi chat profile "MyAgent" --avatar-url https://example.com/pic.png')
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
- const avatarIdx = process.argv.indexOf('--avatar')
421
- const avatarUrl = avatarIdx !== -1 ? process.argv[avatarIdx + 1] || '' : ''
422
- if (avatarUrl && !avatarUrl.startsWith('https://')) {
423
- console.error('Error: --avatar must be an HTTPS URL')
424
- process.exit(1)
526
+ // Avatar: upload a file/remote URL to permanent Arweave, or accept a hosted
527
+ // URL. Reject copied-from-docs placeholders so profiles don't get dead links.
528
+ let avatarUrl = ''
529
+ const avatarFile = flagVal('--avatar-file')
530
+ const avatarRemote = flagVal('--avatar-url')
531
+ const avatarRaw = flagVal('--avatar')
532
+ if (avatarFile) {
533
+ avatarUrl = await uploadImageFile(avatarFile)
534
+ } else if (avatarRemote) {
535
+ if (looksLikePlaceholder(avatarRemote)) { console.error(`Error: "${avatarRemote}" looks like a placeholder. Use --avatar-file <path> or a real hosted image URL.`); process.exit(1) }
536
+ avatarUrl = await uploadImageFromUrl(avatarRemote)
537
+ } else if (avatarRaw) {
538
+ if (looksLikePlaceholder(avatarRaw)) { console.error(`Error: "${avatarRaw}" looks like a placeholder. Use --avatar-file <path> to upload a real image, or pass a real hosted URL.`); process.exit(1) }
539
+ if (!/^https?:\/\//i.test(avatarRaw)) { console.error('Error: --avatar must be an https URL (or use --avatar-file <path> to upload).'); process.exit(1) }
540
+ avatarUrl = /arweave\.net\//i.test(avatarRaw) ? avatarRaw : await uploadImageFromUrl(avatarRaw)
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 <url> Image URI (e.g. arweave URL)')
1031
+ console.error(' --image-file <path> Upload a local image (PNG/JPEG/WebP/GIF, <2MB) — recommended')
1032
+ console.error(' --image-url <url> Re-host a remote image to permanent Arweave')
1033
+ console.error(' --image <arweaveUrl> Use an already-hosted image URL')
1034
+ console.error(' --no-image Launch without a logo (permanent; not recommended)')
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 imageIdx = argv.indexOf('--image')
820
- const imageURI = imageIdx !== -1 ? argv[imageIdx + 1] || '' : ''
1052
+ const noImage = argv.includes('--no-image')
1053
+ const imageURI = await resolveLaunchImage()
1054
+
1055
+ // A token's logo is written on-chain at launch and is permanent. Refuse to
1056
+ // ship a logoless token unless the caller explicitly opts in with --no-image.
1057
+ // (Both exits — add an image, or --no-image — are achievable, so this guides
1058
+ // rather than traps.)
1059
+ if (!imageURI && !noImage) {
1060
+ console.error('')
1061
+ console.error(c.red(' Refusing to launch without an image.'))
1062
+ console.error('')
1063
+ console.error(` A token's image is written on-chain at launch and is permanent. Without`)
1064
+ console.error(` one, ${symbol} will have no logo in wallets, explorers, or aggregators —`)
1065
+ console.error(` forever. (Setting one later only updates the Blume listing, not external apps.)`)
1066
+ console.error('')
1067
+ console.error(' Add a real image now:')
1068
+ console.error(' --image-file <path> Upload a local image (PNG/JPEG/WebP/GIF, <2MB)')
1069
+ console.error(' --image-url <url> Re-host a remote image to permanent Arweave')
1070
+ console.error('')
1071
+ console.error(' Or, to launch without a logo on purpose:')
1072
+ console.error(' --no-image')
1073
+ console.error('')
1074
+ process.exit(1)
1075
+ }
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: (none consider adding --image <url>)`)
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> Add an image`)
1150
+ console.log(` blumefi pad update-image ${tokenAddress} <file|url> Set Blume listing image`)
1151
+ console.log(c.dim(` (Blume site only — the on-chain image is fixed at launch and stays empty)`))
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, imageUrl) {
1283
- if (!tokenAddress || !imageUrl) {
1284
- console.error('Usage: blumefi pad update-image <token_address> <image_url>')
1285
- console.error(' blumefi pad update-image 0x1234... https://arweave.net/TXID')
1544
+ async function cmdPadUpdateImage(tokenAddress, imageArg) {
1545
+ const fileFlag = flagVal('--image-file')
1546
+ const urlFlag = flagVal('--image-url')
1547
+ if (!tokenAddress || (!imageArg && !fileFlag && !urlFlag)) {
1548
+ console.error('Usage: blumefi pad update-image <token_address> <file|url>')
1549
+ console.error(' blumefi pad update-image 0x1234... ./logo.png')
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
- if (!imageUrl.startsWith('https://')) {
1291
- console.error('Error: Image URL must start with https://')
1557
+
1558
+ // Resolve to a permanent hosted URL: upload local files / re-host remote URLs.
1559
+ let imageUrl
1560
+ if (fileFlag) {
1561
+ imageUrl = await uploadImageFile(fileFlag)
1562
+ } else if (urlFlag) {
1563
+ if (looksLikePlaceholder(urlFlag)) { console.error(`\n Error: ${placeholderMsg(urlFlag)}`); process.exit(1) }
1564
+ imageUrl = await uploadImageFromUrl(urlFlag)
1565
+ } else if (looksLikePlaceholder(imageArg)) {
1566
+ console.error(`\n Error: ${placeholderMsg(imageArg)}`); process.exit(1)
1567
+ } else if (require('fs').existsSync(imageArg)) {
1568
+ imageUrl = await uploadImageFile(imageArg)
1569
+ } else if (/^https?:\/\//i.test(imageArg)) {
1570
+ imageUrl = /arweave\.net\//i.test(imageArg) ? imageArg : await uploadImageFromUrl(imageArg)
1571
+ } else {
1572
+ console.error('Error: provide a local image file path or an https:// URL.')
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 Token image updated!`)
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> Update token image (creator only)
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": "3.0.0",
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"