@solana-compass/cli 0.1.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 (120) hide show
  1. package/README.md +327 -0
  2. package/bin/sol.mjs +6 -0
  3. package/dist/commands/config.d.ts +2 -0
  4. package/dist/commands/config.js +73 -0
  5. package/dist/commands/config.js.map +1 -0
  6. package/dist/commands/lend.d.ts +2 -0
  7. package/dist/commands/lend.js +262 -0
  8. package/dist/commands/lend.js.map +1 -0
  9. package/dist/commands/lp.d.ts +2 -0
  10. package/dist/commands/lp.js +158 -0
  11. package/dist/commands/lp.js.map +1 -0
  12. package/dist/commands/network.d.ts +2 -0
  13. package/dist/commands/network.js +126 -0
  14. package/dist/commands/network.js.map +1 -0
  15. package/dist/commands/portfolio.d.ts +2 -0
  16. package/dist/commands/portfolio.js +187 -0
  17. package/dist/commands/portfolio.js.map +1 -0
  18. package/dist/commands/stake.d.ts +2 -0
  19. package/dist/commands/stake.js +143 -0
  20. package/dist/commands/stake.js.map +1 -0
  21. package/dist/commands/token.d.ts +2 -0
  22. package/dist/commands/token.js +551 -0
  23. package/dist/commands/token.js.map +1 -0
  24. package/dist/commands/tx.d.ts +2 -0
  25. package/dist/commands/tx.js +131 -0
  26. package/dist/commands/tx.js.map +1 -0
  27. package/dist/commands/wallet.d.ts +2 -0
  28. package/dist/commands/wallet.js +403 -0
  29. package/dist/commands/wallet.js.map +1 -0
  30. package/dist/core/config-manager.d.ts +31 -0
  31. package/dist/core/config-manager.js +79 -0
  32. package/dist/core/config-manager.js.map +1 -0
  33. package/dist/core/kamino-compat.d.ts +17 -0
  34. package/dist/core/kamino-compat.js +38 -0
  35. package/dist/core/kamino-compat.js.map +1 -0
  36. package/dist/core/lend-service.d.ts +41 -0
  37. package/dist/core/lend-service.js +331 -0
  38. package/dist/core/lend-service.js.map +1 -0
  39. package/dist/core/lp-service.d.ts +30 -0
  40. package/dist/core/lp-service.js +21 -0
  41. package/dist/core/lp-service.js.map +1 -0
  42. package/dist/core/onramp-service.d.ts +15 -0
  43. package/dist/core/onramp-service.js +57 -0
  44. package/dist/core/onramp-service.js.map +1 -0
  45. package/dist/core/portfolio-service.d.ts +55 -0
  46. package/dist/core/portfolio-service.js +272 -0
  47. package/dist/core/portfolio-service.js.map +1 -0
  48. package/dist/core/price-service.d.ts +8 -0
  49. package/dist/core/price-service.js +116 -0
  50. package/dist/core/price-service.js.map +1 -0
  51. package/dist/core/rpc.d.ts +5 -0
  52. package/dist/core/rpc.js +69 -0
  53. package/dist/core/rpc.js.map +1 -0
  54. package/dist/core/stake-service.d.ts +42 -0
  55. package/dist/core/stake-service.js +319 -0
  56. package/dist/core/stake-service.js.map +1 -0
  57. package/dist/core/swap-service.d.ts +31 -0
  58. package/dist/core/swap-service.js +142 -0
  59. package/dist/core/swap-service.js.map +1 -0
  60. package/dist/core/token-registry.d.ts +23 -0
  61. package/dist/core/token-registry.js +174 -0
  62. package/dist/core/token-registry.js.map +1 -0
  63. package/dist/core/token-service.d.ts +20 -0
  64. package/dist/core/token-service.js +92 -0
  65. package/dist/core/token-service.js.map +1 -0
  66. package/dist/core/transaction.d.ts +55 -0
  67. package/dist/core/transaction.js +196 -0
  68. package/dist/core/transaction.js.map +1 -0
  69. package/dist/core/wallet-manager.d.ts +20 -0
  70. package/dist/core/wallet-manager.js +142 -0
  71. package/dist/core/wallet-manager.js.map +1 -0
  72. package/dist/db/database.d.ts +3 -0
  73. package/dist/db/database.js +44 -0
  74. package/dist/db/database.js.map +1 -0
  75. package/dist/db/migrations/001_initial.d.ts +1 -0
  76. package/dist/db/migrations/001_initial.js +116 -0
  77. package/dist/db/migrations/001_initial.js.map +1 -0
  78. package/dist/db/migrations/002_tx_prices.d.ts +1 -0
  79. package/dist/db/migrations/002_tx_prices.js +5 -0
  80. package/dist/db/migrations/002_tx_prices.js.map +1 -0
  81. package/dist/db/repos/price-repo.d.ts +11 -0
  82. package/dist/db/repos/price-repo.js +14 -0
  83. package/dist/db/repos/price-repo.js.map +1 -0
  84. package/dist/db/repos/snapshot-repo.d.ts +27 -0
  85. package/dist/db/repos/snapshot-repo.js +32 -0
  86. package/dist/db/repos/snapshot-repo.js.map +1 -0
  87. package/dist/db/repos/token-repo.d.ts +17 -0
  88. package/dist/db/repos/token-repo.js +53 -0
  89. package/dist/db/repos/token-repo.js.map +1 -0
  90. package/dist/db/repos/transaction-repo.d.ts +22 -0
  91. package/dist/db/repos/transaction-repo.js +28 -0
  92. package/dist/db/repos/transaction-repo.js.map +1 -0
  93. package/dist/db/repos/wallet-repo.d.ts +18 -0
  94. package/dist/db/repos/wallet-repo.js +44 -0
  95. package/dist/db/repos/wallet-repo.js.map +1 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +84 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/output/formatter.d.ts +27 -0
  100. package/dist/output/formatter.js +76 -0
  101. package/dist/output/formatter.js.map +1 -0
  102. package/dist/output/portfolio-renderer.d.ts +3 -0
  103. package/dist/output/portfolio-renderer.js +205 -0
  104. package/dist/output/portfolio-renderer.js.map +1 -0
  105. package/dist/output/table.d.ts +8 -0
  106. package/dist/output/table.js +22 -0
  107. package/dist/output/table.js.map +1 -0
  108. package/dist/utils/fs.d.ts +7 -0
  109. package/dist/utils/fs.js +39 -0
  110. package/dist/utils/fs.js.map +1 -0
  111. package/dist/utils/retry.d.ts +17 -0
  112. package/dist/utils/retry.js +55 -0
  113. package/dist/utils/retry.js.map +1 -0
  114. package/dist/utils/solana.d.ts +9 -0
  115. package/dist/utils/solana.js +26 -0
  116. package/dist/utils/solana.js.map +1 -0
  117. package/dist/utils/token-list.d.ts +9 -0
  118. package/dist/utils/token-list.js +31 -0
  119. package/dist/utils/token-list.js.map +1 -0
  120. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # sol — Solana for Humans and LLM Agents
2
+
3
+ A Solana CLI that reads like English. Every command has structured `--json` output so LLM agents like Claude Code can drive it programmatically — managing wallets, executing swaps, staking, lending, and tracking portfolios without ever touching raw transactions.
4
+
5
+ ```bash
6
+ sol token swap 50 usdc bonk # Swap via Jupiter
7
+ sol stake new 10 # Stake SOL in one command
8
+ sol lend deposit 100 usdc # Earn yield on Kamino
9
+ sol portfolio # See everything you hold
10
+ ```
11
+
12
+ ---
13
+
14
+ ## Why This Exists
15
+
16
+ Solana has great infrastructure but terrible UX for automation. An LLM agent that wants to "swap 50 USDC for BONK" shouldn't need to construct transaction messages, manage blockhashes, or parse binary account data. **sol** wraps all of that behind natural-language commands with structured JSON responses:
17
+
18
+ ```bash
19
+ $ sol token swap 50 usdc bonk --json
20
+ {
21
+ "ok": true,
22
+ "data": {
23
+ "inputToken": "USDC",
24
+ "outputToken": "BONK",
25
+ "inputAmount": 50,
26
+ "outputAmount": 3842519.52,
27
+ "signature": "4xK9...",
28
+ "explorerUrl": "https://solscan.io/tx/4xK9..."
29
+ },
30
+ "meta": { "elapsed_ms": 1823 }
31
+ }
32
+ ```
33
+
34
+ No API keys required. No SDK integration. Just commands and JSON.
35
+
36
+ ---
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npm install -g @solana-compass/cli
42
+ ```
43
+
44
+ Or run without installing:
45
+
46
+ ```bash
47
+ npx @solana-compass/cli wallet list
48
+ ```
49
+
50
+ Requires Node.js >= 20.
51
+
52
+ ### First-time setup
53
+
54
+ ```bash
55
+ sol config set rpc.url https://api.mainnet-beta.solana.com
56
+ sol wallet create
57
+ ```
58
+
59
+ Use a dedicated RPC endpoint for production — the public one rate-limits aggressively. Helius, Triton, or QuickNode all offer free tiers.
60
+
61
+ ---
62
+
63
+ ## Commands
64
+
65
+ ### wallet — Create, import, and manage Solana keypairs
66
+
67
+ ```bash
68
+ sol wallet create # New wallet, auto-named
69
+ sol wallet create --name trading --count 3 # Batch-create 3 wallets
70
+ sol wallet list # List all wallets with SOL balances
71
+ sol wallet balance # Full token balances + USD values
72
+ sol wallet balance trading # Balance for a specific wallet
73
+
74
+ sol wallet import --solana-cli # Import from ~/.config/solana/id.json
75
+ sol wallet import ./keypair.json --name cold # Import from file
76
+ sol wallet export main # Show key file path
77
+ sol wallet remove old-wallet # Remove from registry
78
+
79
+ sol wallet set-default trading # Change the active wallet
80
+ sol wallet label main --add trading # Tag wallets for organization
81
+ sol wallet history # Recent transaction activity
82
+ sol wallet history --type swap --limit 5 # Filtered
83
+ sol wallet fund --amount 100 # Generate fiat onramp URL (Transak)
84
+ ```
85
+
86
+ Wallets are stored locally as key files. The first wallet created becomes the default for all commands. Change it with `sol wallet set-default <name>`, or override per-command with `--wallet <name-or-address>`.
87
+
88
+ ### token — Prices, swaps, transfers, and account management
89
+
90
+ ```bash
91
+ sol token price sol # Current SOL price
92
+ sol token price sol usdc bonk # Multiple prices at once
93
+ sol token info bonk # Token metadata (mint, decimals)
94
+ sol token list # All tokens in your wallet
95
+ sol token sync # Refresh token metadata cache
96
+
97
+ sol token swap 50 usdc bonk # Swap via Jupiter
98
+ sol token swap 1.5 sol usdc --slippage 100 # 1% slippage tolerance
99
+ sol token swap 50 usdc bonk --quote-only # Preview without executing
100
+
101
+ sol token swap 5 usdc sol --wallet backup # Swap from a specific wallet
102
+ sol token send 2 sol GkX...abc # Send SOL to an address
103
+ sol token burn bonk --all # Burn all of a token
104
+ sol token close --all --yes # Close empty accounts, reclaim rent
105
+ ```
106
+
107
+ Swaps use Jupiter's aggregator — best price across all Solana DEXes, no API key needed. Every swap is logged with cost-basis prices for portfolio tracking.
108
+
109
+ ### Token resolution
110
+
111
+ Anywhere a command takes a token, you can use a **symbol** (`sol`, `usdc`, `bonk`) or a **mint address** (`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`). Resolution works in three steps:
112
+
113
+ 1. **Hardcoded well-known list** — 15 major tokens (SOL, USDC, USDT, JUP, BONK, mSOL, jitoSOL, bSOL, ETH, wBTC, PYTH, JTO, WEN, RNDR, JLP) resolve instantly offline. These are immune to spoofing.
114
+
115
+ 2. **Local cache** — Previously resolved tokens are cached in SQLite with a 24-hour TTL. Populated by prior lookups and `sol token sync`.
116
+
117
+ 3. **Jupiter Token API** — If not cached, the CLI searches Jupiter's token database. Results are ranked by liquidity and trading volume, so `usdc` returns the real USDC — not a scam token with the same symbol. The result is cached for next time.
118
+
119
+ **For unfamiliar tokens, verify before transacting:**
120
+
121
+ ```bash
122
+ sol token info peng # Check the resolved mint address
123
+ sol token swap 50 usdc EPjFW...1v --quote-only # Use the mint address directly
124
+ ```
125
+
126
+ Using a mint address bypasses symbol search entirely — useful when you know exactly which token you want, or when dealing with tokens that share a ticker. Agents should prefer mint addresses for safety when operating autonomously.
127
+
128
+ ### stake — Native SOL staking with MEV compounding
129
+
130
+ ```bash
131
+ sol stake new 10 # Stake 10 SOL (default: Solana Compass)
132
+ sol stake new 5 --validator DPm...xyz # Stake with a specific validator
133
+ sol stake list # All stake accounts + claimable MEV
134
+ sol stake claim-mev # Compound MEV tips (re-stake)
135
+ sol stake claim-mev --withdraw # Withdraw MEV to wallet instead
136
+ sol stake withdraw 7gK...abc # Smart withdraw (handles deactivation)
137
+ ```
138
+
139
+ Creates a stake account, funds it, and delegates — all in a single transaction. The CLI handles the multi-step Solana staking process so you don't have to.
140
+
141
+ ### lend — Lending and borrowing on Kamino Finance
142
+
143
+ ```bash
144
+ sol lend rates sol # Deposit/borrow APY for SOL
145
+ sol lend rates usdc # USDC rates
146
+
147
+ sol lend deposit 100 usdc # Deposit USDC to earn yield
148
+ sol lend deposit 5 sol # Deposit SOL as collateral
149
+ sol lend withdraw 50 usdc # Partial withdrawal
150
+ sol lend withdraw max sol # Withdraw entire deposit
151
+
152
+ sol lend borrow 500 usdc --collateral sol # Borrow against collateral
153
+ sol lend repay 250 usdc # Partial repay
154
+ sol lend repay max usdc # Repay full outstanding debt
155
+
156
+ sol lend positions # All deposits, borrows, health factor
157
+ ```
158
+
159
+ Positions include real-time APY, USD values, and health factor monitoring. The CLI warns when health factor drops below 1.1.
160
+
161
+ ### portfolio — Unified view across all positions
162
+
163
+ ```bash
164
+ sol portfolio # Everything: tokens, stakes, lending
165
+ sol portfolio --wallet trading # Single wallet view
166
+
167
+ sol portfolio snapshot # Save current state
168
+ sol portfolio snapshot --label "pre-trade" # With a label
169
+ sol portfolio history # List all snapshots
170
+
171
+ sol portfolio compare # Diff vs latest snapshot
172
+ sol portfolio compare 3 # Diff vs snapshot #3
173
+ sol portfolio pnl # P&L since first snapshot
174
+ sol portfolio pnl --since 5 # P&L since snapshot #5
175
+ ```
176
+
177
+ The portfolio aggregates tokens, staked SOL, and Kamino lending positions across all wallets. Snapshots enable tracking changes over time — useful for agents that need to measure the impact of their actions.
178
+
179
+ ### config — Persistent settings
180
+
181
+ ```bash
182
+ sol config set rpc.url https://my-rpc.com # Set RPC endpoint
183
+ sol config get rpc.url # Read a value
184
+ sol config list # Show all settings
185
+ sol config path # Config file location
186
+ ```
187
+
188
+ ### Other commands
189
+
190
+ ```bash
191
+ sol network # Epoch, TPS, inflation, staking APY
192
+ sol tx 4xK9...abc # Look up a transaction by signature
193
+ ```
194
+
195
+ ---
196
+
197
+ ## For LLM Agents
198
+
199
+ Every command supports `--json` for structured output. Responses follow a consistent envelope:
200
+
201
+ **Success:**
202
+ ```json
203
+ {
204
+ "ok": true,
205
+ "data": { ... },
206
+ "meta": { "elapsed_ms": 450 }
207
+ }
208
+ ```
209
+
210
+ **Error:**
211
+ ```json
212
+ {
213
+ "ok": false,
214
+ "error": "SWAP_FAILED",
215
+ "message": "Insufficient SOL balance"
216
+ }
217
+ ```
218
+
219
+ Error codes are predictable `UPPER_SNAKE_CASE` identifiers (`WALLET_CREATE_FAILED`, `LEND_DEPOSIT_FAILED`, etc.). An agent can branch on `ok`, read `data` for results, and use `error` codes for programmatic error handling without parsing human text.
220
+
221
+ ### Agent workflow example
222
+
223
+ A Claude Code agent managing a DeFi portfolio might run:
224
+
225
+ ```bash
226
+ # 1. Check what we're working with
227
+ sol wallet balance --json
228
+ sol portfolio --json
229
+
230
+ # 2. Sell some BONK for USDC
231
+ sol token swap 1000000 bonk usdc --json
232
+
233
+ # 3. Put idle USDC to work earning yield
234
+ sol lend deposit 100 usdc --json
235
+
236
+ # 4. Stake idle SOL
237
+ sol stake new 5 --json
238
+
239
+ # 5. Verify final state and snapshot for tracking
240
+ sol portfolio --json
241
+ sol portfolio snapshot --label "rebalanced" --json
242
+ ```
243
+
244
+ Each step returns structured data the agent can parse and act on. No web scraping, no API key management, no transaction construction.
245
+
246
+ ### What agents get for free
247
+
248
+ - **Wallet management** — Create and fund wallets without touching key files
249
+ - **Token resolution** — Say `sol` or `usdc` instead of mint addresses
250
+ - **Transaction handling** — Automatic retries, blockhash management, confirmation polling
251
+ - **Cost-basis tracking** — Every swap is logged with USD prices at execution time
252
+ - **Portfolio snapshots** — Track P&L across sessions without external databases
253
+ - **Error classification** — Structured errors distinguish transient failures (retry) from terminal ones (stop)
254
+
255
+ ---
256
+
257
+ ## Global Flags
258
+
259
+ | Flag | Description |
260
+ |------|-------------|
261
+ | `--json` | Structured JSON output |
262
+ | `--wallet <name>` | Override default wallet |
263
+ | `--rpc <url>` | Override RPC endpoint |
264
+ | `--verbose` | Show debug logging |
265
+
266
+ These work on every command: `sol --json --wallet trading token swap 50 usdc bonk`
267
+
268
+ ## RPC Resolution
269
+
270
+ The CLI resolves an RPC endpoint in this order:
271
+
272
+ 1. `--rpc` flag
273
+ 2. `SOL_RPC_URL` environment variable
274
+ 3. `~/.sol/config.toml` → `rpc.url`
275
+ 4. Solana CLI config (`solana config get`)
276
+ 5. Public mainnet RPC (with warning)
277
+
278
+ ## Data Storage
279
+
280
+ All data lives in `~/.sol/`:
281
+
282
+ | Path | Contents |
283
+ |------|----------|
284
+ | `config.toml` | Configuration (TOML) |
285
+ | `data.db` | SQLite — wallets, token cache, transaction log, snapshots |
286
+ | `wallets/*.json` | Keypair files (Solana CLI format, chmod 600) |
287
+
288
+ The transaction log is the source of truth for cost basis and P&L — every swap, transfer, deposit, and withdrawal is recorded with USD prices at the time of execution.
289
+
290
+ ---
291
+
292
+ ## Architecture
293
+
294
+ - **Runtime**: Node.js via `tsx` — no build step needed
295
+ - **CLI framework**: commander.js
296
+ - **Solana SDK**: `@solana/kit` v2
297
+ - **Swaps**: Jupiter REST API (no SDK, no API key)
298
+ - **Lending**: Kamino Finance via `@kamino-finance/klend-sdk`
299
+ - **Prices**: Jupiter Price API with CoinGecko fallback
300
+ - **Database**: SQLite via `better-sqlite3` (WAL mode)
301
+ - **Config**: TOML via `smol-toml`
302
+
303
+ ```
304
+ src/
305
+ index.ts # CLI entry point, global flags
306
+ commands/ # One file per command group
307
+ core/ # Business logic (wallet-manager, swap-service, etc.)
308
+ db/ # SQLite with migration runner
309
+ output/ # JSON envelope + table/portfolio renderers
310
+ utils/ # Solana helpers, token list, retry logic
311
+ ```
312
+
313
+ ## Security
314
+
315
+ - Key files are plain JSON (Solana CLI compatible), stored with `chmod 600`
316
+ - Private keys are never logged or printed to stdout
317
+ - `wallet export` returns the file path, not the key contents
318
+ - All transactions are logged to SQLite for audit
319
+ - Swap commands show quote details before executing
320
+
321
+ ## Disclaimer
322
+
323
+ This software interacts with the Solana blockchain and can execute irreversible transactions involving real funds. You are solely responsible for your own transactions, wallet security, and any financial outcomes. The authors are not liable for any losses. Use at your own risk.
324
+
325
+ ## License
326
+
327
+ MIT
package/bin/sol.mjs ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ await import(join(__dirname, '..', 'dist', 'index.js'));
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerConfigCommand(program: Command): void;
@@ -0,0 +1,73 @@
1
+ import { getConfigValue, setConfigValue, listConfig, getConfigPath } from '../core/config-manager.js';
2
+ import { output, success, failure, isJsonMode } from '../output/formatter.js';
3
+ import { table } from '../output/table.js';
4
+ export function registerConfigCommand(program) {
5
+ const config = program.command('config').description('Manage CLI configuration');
6
+ config
7
+ .command('set <key> <value>')
8
+ .description('Set a config value (e.g., sol config set rpc.url https://my-rpc.com)')
9
+ .action((key, value) => {
10
+ try {
11
+ setConfigValue(key, value);
12
+ if (isJsonMode()) {
13
+ output(success({ key, value: getConfigValue(key) }));
14
+ }
15
+ else {
16
+ console.log(`Set ${key} = ${value}`);
17
+ }
18
+ }
19
+ catch (err) {
20
+ output(failure('CONFIG_SET_FAILED', err.message));
21
+ process.exitCode = 1;
22
+ }
23
+ });
24
+ config
25
+ .command('get <key>')
26
+ .description('Get a config value')
27
+ .action((key) => {
28
+ const value = getConfigValue(key);
29
+ if (isJsonMode()) {
30
+ output(success({ key, value }));
31
+ }
32
+ else if (value === undefined) {
33
+ console.log(`(not set)`);
34
+ }
35
+ else {
36
+ console.log(String(value));
37
+ }
38
+ });
39
+ config
40
+ .command('list')
41
+ .description('List all config values')
42
+ .action(() => {
43
+ const values = listConfig();
44
+ if (isJsonMode()) {
45
+ output(success(values));
46
+ }
47
+ else {
48
+ const entries = Object.entries(values);
49
+ if (entries.length === 0) {
50
+ console.log('No configuration set. Use: sol config set <key> <value>');
51
+ }
52
+ else {
53
+ console.log(table(entries.map(([key, value]) => ({ key, value: String(value) })), [
54
+ { key: 'key', header: 'Key' },
55
+ { key: 'value', header: 'Value' },
56
+ ]));
57
+ }
58
+ }
59
+ });
60
+ config
61
+ .command('path')
62
+ .description('Show config file path')
63
+ .action(() => {
64
+ const path = getConfigPath();
65
+ if (isJsonMode()) {
66
+ output(success({ path }));
67
+ }
68
+ else {
69
+ console.log(path);
70
+ }
71
+ });
72
+ }
73
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLendCommand(program: Command): void;
@@ -0,0 +1,262 @@
1
+ import * as lendService from '../core/lend-service.js';
2
+ import { getDefaultWalletName, resolveWalletName } from '../core/wallet-manager.js';
3
+ import { output, success, failure, isJsonMode, timed } from '../output/formatter.js';
4
+ import { table } from '../output/table.js';
5
+ import * as walletRepo from '../db/repos/wallet-repo.js';
6
+ export function registerLendCommand(program) {
7
+ const lend = program.command('lend').description('Lending and borrowing (Kamino Finance)');
8
+ // ── rates ───────────────────────────────────────────────
9
+ lend
10
+ .command('rates <token>')
11
+ .description('Show Kamino deposit/borrow APY for a token')
12
+ .action(async (token) => {
13
+ try {
14
+ const { result: rates, elapsed_ms } = await timed(() => lendService.getRates(token));
15
+ if (isJsonMode()) {
16
+ output(success({ token, rates }, { elapsed_ms }));
17
+ }
18
+ else if (rates.length === 0) {
19
+ console.log(`No Kamino lending reserve found for "${token}".`);
20
+ }
21
+ else {
22
+ const r = rates[0];
23
+ console.log(`Kamino Lending — ${r.token}\n`);
24
+ console.log(table([{
25
+ depositApy: `${(r.depositApy * 100).toFixed(2)}%`,
26
+ borrowApy: `${(r.borrowApy * 100).toFixed(2)}%`,
27
+ utilization: `${r.utilizationPct.toFixed(1)}%`,
28
+ totalDeposited: fmtLargeAmount(r.totalDeposited),
29
+ totalBorrowed: fmtLargeAmount(r.totalBorrowed),
30
+ }], [
31
+ { key: 'depositApy', header: 'Deposit APY', align: 'right' },
32
+ { key: 'borrowApy', header: 'Borrow APY', align: 'right' },
33
+ { key: 'utilization', header: 'Utilization', align: 'right' },
34
+ { key: 'totalDeposited', header: 'Total Deposited', align: 'right' },
35
+ { key: 'totalBorrowed', header: 'Total Borrowed', align: 'right' },
36
+ ]));
37
+ console.log(`\nRun \`sol lend deposit <amount> ${token}\` to start earning.`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ output(failure('LEND_RATES_FAILED', err.message));
42
+ process.exitCode = 1;
43
+ }
44
+ });
45
+ // ── positions ───────────────────────────────────────────
46
+ lend
47
+ .command('positions')
48
+ .description('List all Kamino lending/borrowing positions')
49
+ .option('--wallet <name>', 'Wallet to check')
50
+ .action(async (opts) => {
51
+ try {
52
+ const walletName = opts.wallet ? resolveWalletName(opts.wallet) : getDefaultWalletName();
53
+ const wallet = walletRepo.getWallet(walletName);
54
+ if (!wallet)
55
+ throw new Error(`Wallet "${walletName}" not found`);
56
+ const { result: positions, elapsed_ms } = await timed(() => lendService.getPositions(wallet.address));
57
+ if (isJsonMode()) {
58
+ output(success({ wallet: walletName, positions }, { elapsed_ms }));
59
+ }
60
+ else if (positions.length === 0) {
61
+ console.log('No Kamino lending positions found.');
62
+ console.log('Run `sol lend rates <token>` to see available rates.');
63
+ }
64
+ else {
65
+ const deposits = positions.filter(p => p.type === 'deposit');
66
+ const borrows = positions.filter(p => p.type === 'borrow');
67
+ if (deposits.length > 0) {
68
+ console.log('Deposits');
69
+ console.log(table(deposits.map(p => ({
70
+ token: p.token,
71
+ amount: fmtAmount(p.amount),
72
+ value: `$${p.valueUsd.toFixed(2)}`,
73
+ apy: `${(p.apy * 100).toFixed(2)}%`,
74
+ })), [
75
+ { key: 'token', header: 'Token' },
76
+ { key: 'amount', header: 'Amount', align: 'right' },
77
+ { key: 'value', header: 'Value', align: 'right' },
78
+ { key: 'apy', header: 'APY', align: 'right' },
79
+ ]));
80
+ console.log('');
81
+ }
82
+ if (borrows.length > 0) {
83
+ console.log('Borrows');
84
+ console.log(table(borrows.map(p => ({
85
+ token: p.token,
86
+ amount: fmtAmount(p.amount),
87
+ value: `$${p.valueUsd.toFixed(2)}`,
88
+ apy: `${(p.apy * 100).toFixed(2)}%`,
89
+ health: p.healthFactor != null ? p.healthFactor.toFixed(2) : '—',
90
+ })), [
91
+ { key: 'token', header: 'Token' },
92
+ { key: 'amount', header: 'Amount', align: 'right' },
93
+ { key: 'value', header: 'Value', align: 'right' },
94
+ { key: 'apy', header: 'APY', align: 'right' },
95
+ { key: 'health', header: 'Health', align: 'right' },
96
+ ]));
97
+ // Health factor warning
98
+ const minHealth = Math.min(...borrows.map(p => p.healthFactor ?? Infinity));
99
+ if (minHealth < 1.1) {
100
+ console.log('\nWarning: health factor below 1.1. Consider repaying or adding collateral.');
101
+ }
102
+ console.log('');
103
+ }
104
+ // Net value
105
+ const totalDeposits = deposits.reduce((s, p) => s + p.valueUsd, 0);
106
+ const totalBorrows = borrows.reduce((s, p) => s + p.valueUsd, 0);
107
+ console.log(`Net lending value: $${(totalDeposits - totalBorrows).toFixed(2)}`);
108
+ }
109
+ }
110
+ catch (err) {
111
+ output(failure('LEND_POSITIONS_FAILED', err.message));
112
+ process.exitCode = 1;
113
+ }
114
+ });
115
+ // ── deposit ─────────────────────────────────────────────
116
+ lend
117
+ .command('deposit <amount> <token>')
118
+ .description('Deposit into Kamino lending vault')
119
+ .option('--wallet <name>', 'Wallet to use')
120
+ .action(async (amountStr, token, opts) => {
121
+ try {
122
+ const amount = parseFloat(amountStr);
123
+ if (isNaN(amount) || amount <= 0)
124
+ throw new Error('Invalid amount');
125
+ const walletName = opts.wallet ? resolveWalletName(opts.wallet) : getDefaultWalletName();
126
+ const { result, elapsed_ms } = await timed(() => lendService.deposit(walletName, token, amount));
127
+ if (isJsonMode()) {
128
+ output(success(result, { elapsed_ms }));
129
+ }
130
+ else {
131
+ console.log(`Deposited ${amount} ${token.toUpperCase()} into Kamino`);
132
+ console.log(` Tx: ${result.explorerUrl}`);
133
+ console.log(`\nRun \`sol lend positions\` to see your deposits.`);
134
+ }
135
+ }
136
+ catch (err) {
137
+ output(failure('LEND_DEPOSIT_FAILED', err.message));
138
+ process.exitCode = 1;
139
+ }
140
+ });
141
+ // ── withdraw ────────────────────────────────────────────
142
+ lend
143
+ .command('withdraw <amount> <token>')
144
+ .description('Withdraw from Kamino lending position (use "max" for full withdrawal)')
145
+ .option('--wallet <name>', 'Wallet to use')
146
+ .action(async (amountStr, token, opts) => {
147
+ try {
148
+ const amount = parseMaxAmount(amountStr);
149
+ const walletName = opts.wallet ? resolveWalletName(opts.wallet) : getDefaultWalletName();
150
+ const { result, elapsed_ms } = await timed(() => lendService.withdraw(walletName, token, amount));
151
+ if (isJsonMode()) {
152
+ output(success(result, { elapsed_ms }));
153
+ }
154
+ else {
155
+ const label = isFinite(amount) ? `${amount} ${token.toUpperCase()}` : `all ${token.toUpperCase()}`;
156
+ console.log(`Withdrew ${label} from Kamino`);
157
+ console.log(` Tx: ${result.explorerUrl}`);
158
+ console.log(`\nRun \`sol lend positions\` to check remaining positions.`);
159
+ }
160
+ }
161
+ catch (err) {
162
+ output(failure('LEND_WITHDRAW_FAILED', err.message));
163
+ process.exitCode = 1;
164
+ }
165
+ });
166
+ // ── borrow ──────────────────────────────────────────────
167
+ lend
168
+ .command('borrow <amount> <token>')
169
+ .description('Borrow against collateral on Kamino')
170
+ .option('--collateral <token>', 'Collateral token (required)')
171
+ .option('--wallet <name>', 'Wallet to use')
172
+ .action(async (amountStr, token, opts) => {
173
+ try {
174
+ const amount = parseFloat(amountStr);
175
+ if (isNaN(amount) || amount <= 0)
176
+ throw new Error('Invalid amount');
177
+ if (!opts.collateral)
178
+ throw new Error('--collateral is required');
179
+ const walletName = opts.wallet ? resolveWalletName(opts.wallet) : getDefaultWalletName();
180
+ const { result, elapsed_ms } = await timed(() => lendService.borrow(walletName, token, amount, opts.collateral));
181
+ if (isJsonMode()) {
182
+ output(success(result, { elapsed_ms }));
183
+ }
184
+ else {
185
+ console.log(`Borrowed ${amount} ${token.toUpperCase()} from Kamino`);
186
+ console.log(` Tx: ${result.explorerUrl}`);
187
+ if (result.healthFactor != null) {
188
+ console.log(` Health factor: ${result.healthFactor.toFixed(2)}`);
189
+ if (result.healthFactor < 1.1) {
190
+ console.log(' Warning: health factor is low. Monitor closely.');
191
+ }
192
+ }
193
+ console.log(`\nRun \`sol lend positions\` to view all positions.`);
194
+ }
195
+ }
196
+ catch (err) {
197
+ output(failure('LEND_BORROW_FAILED', err.message));
198
+ process.exitCode = 1;
199
+ }
200
+ });
201
+ // ── repay ───────────────────────────────────────────────
202
+ lend
203
+ .command('repay <amount> <token>')
204
+ .description('Repay a Kamino loan (use "max" to repay full debt)')
205
+ .option('--wallet <name>', 'Wallet to use')
206
+ .action(async (amountStr, token, opts) => {
207
+ try {
208
+ const amount = parseMaxAmount(amountStr);
209
+ const walletName = opts.wallet ? resolveWalletName(opts.wallet) : getDefaultWalletName();
210
+ const { result, elapsed_ms } = await timed(() => lendService.repay(walletName, token, amount));
211
+ if (isJsonMode()) {
212
+ output(success(result, { elapsed_ms }));
213
+ }
214
+ else {
215
+ const label = isFinite(amount) ? `${amount} ${token.toUpperCase()}` : `all ${token.toUpperCase()}`;
216
+ console.log(`Repaid ${label} on Kamino`);
217
+ console.log(` Tx: ${result.explorerUrl}`);
218
+ if (result.remainingDebt != null) {
219
+ if (result.remainingDebt === 0) {
220
+ console.log(' Loan fully repaid!');
221
+ }
222
+ else {
223
+ console.log(` Remaining debt: ${fmtAmount(result.remainingDebt)} ${token.toUpperCase()}`);
224
+ }
225
+ }
226
+ console.log(`\nRun \`sol lend positions\` to check remaining positions.`);
227
+ }
228
+ }
229
+ catch (err) {
230
+ output(failure('LEND_REPAY_FAILED', err.message));
231
+ process.exitCode = 1;
232
+ }
233
+ });
234
+ }
235
+ // ── Helpers ───────────────────────────────────────────────
236
+ function fmtAmount(n) {
237
+ if (n >= 1_000_000)
238
+ return n.toFixed(0);
239
+ if (n >= 1)
240
+ return n.toFixed(4);
241
+ if (n >= 0.0001)
242
+ return n.toFixed(6);
243
+ return n.toFixed(9);
244
+ }
245
+ function parseMaxAmount(str) {
246
+ if (str === 'max' || str === 'all')
247
+ return Infinity;
248
+ const n = parseFloat(str);
249
+ if (isNaN(n) || n <= 0)
250
+ throw new Error('Invalid amount (use a number or "max")');
251
+ return n;
252
+ }
253
+ function fmtLargeAmount(n) {
254
+ if (n >= 1_000_000_000)
255
+ return `${(n / 1_000_000_000).toFixed(2)}B`;
256
+ if (n >= 1_000_000)
257
+ return `${(n / 1_000_000).toFixed(2)}M`;
258
+ if (n >= 1_000)
259
+ return `${(n / 1_000).toFixed(2)}K`;
260
+ return fmtAmount(n);
261
+ }
262
+ //# sourceMappingURL=lend.js.map