Skip to content

Commit 25e1814

Browse files
committed
feat: add jup lend earn command
1 parent b783a2a commit 25e1814

9 files changed

Lines changed: 836 additions & 113 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ bun run ci
2424

2525
## Architecture
2626

27-
**Entry point:** `src/index.ts` — initializes config, registers 4 command groups with Commander.
27+
**Entry point:** `src/index.ts` — initializes config, registers 5 command groups with Commander.
2828

2929
**Commands** (`src/commands/`): Static classes that register subcommands. Each delegates to library modules.
3030

3131
- `ConfigCommand``config list`, `config set`
3232
- `KeysCommand``keys list/add/delete/edit/use/solana-import`
33+
- `LendCommand``lend earn tokens/positions/deposit/withdraw`
3334
- `PerpsCommand``perps positions/markets/open/set/close`
3435
- `SpotCommand``spot tokens/quote/swap/portfolio/transfer`
3536

@@ -41,17 +42,21 @@ bun run ci
4142
- `KeyPair` — generates/recovers keypairs (BIP39 mnemonics, BIP32 derivation)
4243
- `Output` — renders data as table (via cli-table3) or JSON based on config; also provides display formatters (`formatDollar`, `formatBoolean`, `formatPercentageChange`)
4344
- `NumberConverter` — converts between human-readable and on-chain decimal amounts
45+
- `Swap` — shared swap execution logic used by SpotCommand and LendCommand
4446

4547
**API Clients** (`src/clients/`):
4648

4749
- `DatapiClient` — Jupiter token data/search API
4850
- `UltraClient` — Jupiter Ultra swap API (quote + execute)
4951
- `PerpsClient` — Jupiter Perps API v2 (positions, orders, TP/SL)
52+
- `LendClient` — Jupiter Lend API (earn tokens, positions, earnings)
5053

51-
**Spot swap flow:** token search → UltraClient.getOrder → Signer.signTransaction → UltraClient.postExecute
54+
**Spot swap flow:** token search → Swap.execute → UltraClient.getOrder → Signer.signTransaction → UltraClient.postExecute
5255

5356
**Perps flow:** PerpsClient.post* → Signer.signTransaction → PerpsClient.postExecute
5457

58+
**Lend deposit/withdraw flow:** LendClient.getTokens → resolve jlToken → Swap.execute → LendClient.getPositions (updated state)
59+
5560
## CLI Conventions
5661

5762
When adding new commands, both input (options/arguments) and output (JSON/table) must be consistent with existing commands so that both humans and AI agents can reliably use and parse them. Check `docs/` for the canonical command shapes.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ jup spot swap --from SOL --to USDC --amount 1
4040
jup perps open --asset SOL --side long --amount 10 --input USDC --leverage 3
4141
# View your perps positions
4242
jup perps positions
43+
44+
# View lending tokens and APY
45+
jup lend earn tokens
46+
# Deposit 100 USDC into lending
47+
jup lend earn deposit --token USDC --amount 100
48+
# View your lending positions
49+
jup lend earn positions
4350
```
4451

4552
## Docs
@@ -54,6 +61,7 @@ jup perps positions
5461
- [Keys](docs/keys.md): Private key management
5562
- [Spot](docs/spot.md): Spot trading, transfers, token search and portfolio data
5663
- [Perps](docs/perps.md): Perps trading (leveraged longs/shorts)
64+
- [Lend](docs/lend.md): Lending and yield farming
5765

5866
## Changelog
5967

docs/lend.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Lending
2+
3+
Requires: an active key for `deposit` and `withdraw` commands. See [setup](setup.md).
4+
5+
## Commands
6+
7+
### View available tokens
8+
9+
```bash
10+
jup lend earn tokens
11+
```
12+
13+
- Shows all tokens available for lending with APY, TVL, and withdrawable liquidity
14+
15+
```js
16+
// Example JSON response:
17+
{
18+
"tokens": [
19+
{
20+
"token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, // underlying token
21+
"jlToken": { "id": "jl1U...xxx", "symbol": "jlUSDC", "decimals": 6 }, // lending token
22+
"apyPct": 5.97, // total APY; 5.97 means 5.97%
23+
"supplyApyPct": 4.50, // supply rate portion
24+
"rewardsApyPct": 1.47, // rewards rate portion
25+
"totalTvlUsd": 125000000, // total TVL in USD
26+
"withdrawableUsd": 80000000, // available withdrawable liquidity in USD
27+
"priceUsd": 1.00 // underlying token price
28+
}
29+
]
30+
}
31+
```
32+
33+
### View positions
34+
35+
```bash
36+
jup lend earn positions
37+
jup lend earn positions --key mykey
38+
jup lend earn positions --address <wallet-address>
39+
jup lend earn positions --token USDC
40+
```
41+
42+
- With no options, uses the active key's wallet
43+
- `--address` looks up any wallet without needing a key
44+
- `--token` filters by underlying token (symbol or mint address)
45+
46+
```js
47+
// Example JSON response:
48+
{
49+
"positions": [
50+
{
51+
"token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, // underlying token
52+
"jlToken": { "id": "jl1U...xxx", "symbol": "jlUSDC", "decimals": 6 }, // derivative token
53+
"positionAmount": 1025.30, // current position value in underlying token units, including earnings
54+
"positionUsd": 1025.30, // current position value in USD
55+
"earningsAmount": 24.80, // accrued interest in underlying token units
56+
"earningsUsd": 24.80, // accrued interest in USD
57+
"apyPct": 5.97 // current APY; 5.97 means 5.97%
58+
}
59+
]
60+
}
61+
```
62+
63+
### Deposit
64+
65+
```bash
66+
jup lend earn deposit --token USDC --amount 100
67+
jup lend earn deposit --token SOL --amount 1.5 --key mykey
68+
jup lend earn deposit --token USDC --raw-amount 100000000
69+
```
70+
71+
- `--token` (required) — underlying token to deposit (symbol or mint address)
72+
- `--amount` uses human-readable units (e.g. `100` USDC = 100 USDC)
73+
- `--raw-amount` uses on-chain units (e.g. `100000000` = 100 USDC)
74+
- Exactly one of `--amount` or `--raw-amount` is required
75+
- `--key` overrides the active key for this transaction
76+
77+
```js
78+
// Example JSON response:
79+
{
80+
"token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 },
81+
"depositedAmount": "100", // human-readable amount just deposited
82+
"depositedUsd": 100.00, // USD value of deposit
83+
"positionAmount": 1125.30, // total position after deposit
84+
"positionUsd": 1125.30, // total position USD value
85+
"apyPct": 5.97, // current APY; 5.97 means 5.97%
86+
"signature": "3dV9...8zG1" // tx signature
87+
}
88+
```
89+
90+
### Withdraw
91+
92+
```bash
93+
jup lend earn withdraw --token USDC --amount 50
94+
jup lend earn withdraw --token USDC # withdraw entire position
95+
jup lend earn withdraw --token jlUSDC --amount 50 # also accepts jlToken directly
96+
jup lend earn withdraw --token USDC --raw-amount 50000000
97+
```
98+
99+
- `--token` (required) — token to withdraw (accepts underlying symbol/address or jlToken symbol/address)
100+
- `--amount` in human-readable units of the underlying token
101+
- `--raw-amount` in on-chain units of the jlToken
102+
- When neither `--amount` nor `--raw-amount` is provided, withdraws the entire position
103+
- `--key` overrides the active key for this transaction
104+
105+
```js
106+
// Example JSON response:
107+
{
108+
"token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 },
109+
"withdrawnAmount": "50", // human-readable amount just withdrawn
110+
"withdrawnUsd": 50.00, // USD value of withdrawal
111+
"positionAmount": 975.30, // remaining position after withdrawal (0 if fully withdrawn)
112+
"positionUsd": 975.30, // remaining position USD value
113+
"apyPct": 5.97, // current APY; 5.97 means 5.97%
114+
"signature": "5YhT...9AKU" // tx signature
115+
}
116+
```
117+
118+
## Workflows
119+
120+
### Check available tokens then deposit
121+
122+
```bash
123+
jup lend earn tokens
124+
# Pick a token with good APY
125+
jup lend earn deposit --token USDC --amount 100
126+
```
127+
128+
### Check positions then withdraw
129+
130+
```bash
131+
jup lend earn positions
132+
# Review current value and earnings
133+
jup lend earn withdraw --token USDC --amount 50
134+
```
135+
136+
### Withdraw entire position
137+
138+
```bash
139+
jup lend earn withdraw --token USDC
140+
```

src/clients/DatapiClient.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,15 @@ export class DatapiClient {
154154
}
155155
return this.#ky.get("_datapi/v1/txs/users", { searchParams }).json();
156156
}
157+
158+
public static async resolveToken(input: string): Promise<Token> {
159+
const [token] = await this.getTokensSearch({
160+
query: input,
161+
limit: "1",
162+
});
163+
if (!token) {
164+
throw new Error(`Token not found: ${input}`);
165+
}
166+
return token;
167+
}
157168
}

src/clients/LendClient.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import ky from "ky";
2+
3+
import { ClientConfig } from "./ClientConfig.ts";
4+
5+
export type LendAsset = {
6+
address: string;
7+
chain_id: string;
8+
name: string;
9+
symbol: string;
10+
decimals: number;
11+
logo_url: string;
12+
price: string;
13+
coingecko_id: string;
14+
};
15+
16+
export type LiquiditySupplyData = {
17+
modeWithInterest: boolean;
18+
supply: string;
19+
withdrawalLimit: string;
20+
lastUpdateTimestamp: string;
21+
expandPercent: string;
22+
expandDuration: string;
23+
baseWithdrawalLimit: string;
24+
withdrawableUntilLimit: string;
25+
withdrawable: string;
26+
};
27+
28+
export type LendToken = {
29+
id: number;
30+
address: string;
31+
name: string;
32+
symbol: string;
33+
decimals: number;
34+
assetAddress: string;
35+
asset: LendAsset;
36+
totalAssets: string;
37+
totalSupply: string;
38+
convertToShares: string;
39+
convertToAssets: string;
40+
rewardsRate: string;
41+
supplyRate: string;
42+
totalRate: string;
43+
rebalanceDifference: string;
44+
liquiditySupplyData: LiquiditySupplyData;
45+
};
46+
47+
export type LendPosition = {
48+
token: LendToken;
49+
ownerAddress: string;
50+
shares: string;
51+
underlyingAssets: string;
52+
underlyingBalance: string;
53+
allowance: string;
54+
};
55+
56+
export type LendEarning = {
57+
address: string;
58+
ownerAddress: string;
59+
earnings: number;
60+
slot: number;
61+
};
62+
63+
export class LendClient {
64+
static readonly #ky = ky.create({
65+
prefixUrl: `${ClientConfig.host}/lend/v1`,
66+
headers: ClientConfig.headers,
67+
});
68+
69+
public static async getTokens(): Promise<LendToken[]> {
70+
return this.#ky.get("earn/tokens").json();
71+
}
72+
73+
public static async getPositions(users: string): Promise<LendPosition[]> {
74+
return this.#ky.get("earn/positions", { searchParams: { users } }).json();
75+
}
76+
77+
public static async getEarnings(req: {
78+
user: string;
79+
positions: string;
80+
}): Promise<LendEarning[]> {
81+
return this.#ky.get("earn/earnings", { searchParams: req }).json();
82+
}
83+
}

0 commit comments

Comments
 (0)