Skip to content

Commit 67b4432

Browse files
authored
Merge pull request #205 from Trynax/feat/exppand-cli
feat: add reserves list command to CLI
2 parents ff03d4c + cca9e91 commit 67b4432

File tree

4 files changed

+237
-1
lines changed

4 files changed

+237
-1
lines changed

.changeset/bumpy-plants-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@aave/cli": patch
3+
---
4+
5+
**feat:** add reserves list command to CLI

packages/cli/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ USAGE
3030
# Commands
3131
<!-- commands -->
3232
* [`aave hubs list`](#aave-hubs-list)
33+
* [`aave reserves list`](#aave-reserves-list)
3334
* [`aave spokes list`](#aave-spokes-list)
3435

3536
## `aave hubs list`
@@ -52,6 +53,31 @@ DESCRIPTION
5253

5354
_See code: [src/commands/hubs/list.ts](https://github.com/aave/aave-v4-sdk/blob/v4.1.0-next.3/src/commands/hubs/list.ts)_
5455

56+
## `aave reserves list`
57+
58+
List Aave v4 reserves
59+
60+
```
61+
USAGE
62+
$ aave reserves list [-s <spoke-id>] [-h <hub-id>] [--hub_address <evm-address> -c <chain-id>]
63+
64+
FLAGS
65+
-c, --chain_id=<chain-id> The chain ID (e.g. 1, 137, 42161)
66+
-h, --hub_id=<hub-id> The hub ID (e.g. SGVsbG8h…)
67+
-s, --spoke_id=<spoke-id> The spoke ID (e.g. SGVsbG8h…)
68+
--hub_address=<evm-address> The hub address (e.g. 0x123…)
69+
70+
DESCRIPTION
71+
List Aave v4 reserves
72+
73+
EXAMPLES
74+
$ aave reserves list --spoke_id MTIzNDU2Nzg5OjoweEJh...
75+
$ aave reserves list --hub_id MTIzNDU2Nzg5OjoweGFEOTA1...
76+
$ aave reserves list --chain_id 123456789
77+
```
78+
79+
_See code: [src/commands/reserves/list.ts](https://github.com/aave/aave-v4-sdk/blob/v4.0.0/src/commands/reserves/list.ts)_
80+
5581
## `aave spokes list`
5682

5783
List Aave v4 spokes
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {
2+
InvariantError,
3+
invariant,
4+
ok,
5+
type PercentNumber,
6+
type Reserve,
7+
type ReservesRequest,
8+
ResultAsync,
9+
type UnexpectedError,
10+
} from '@aave/client';
11+
import { reserves } from '@aave/client/actions';
12+
13+
import * as common from '../../common.js';
14+
15+
function formatApy(apy: PercentNumber, decimals = 4): string {
16+
return `${apy.normalized.toFixed(decimals)}%`;
17+
}
18+
19+
export default class ListReserves extends common.V4Command {
20+
static override description = 'List Aave v4 reserves';
21+
22+
static override flags = {
23+
spoke_id: common.spoke({
24+
required: false,
25+
relationships: [
26+
{
27+
type: 'none',
28+
flags: ['hub_id', 'chain_id', 'hub_address'],
29+
},
30+
],
31+
}),
32+
hub_id: common.hub({
33+
required: false,
34+
relationships: [
35+
{
36+
type: 'none',
37+
flags: ['spoke_id', 'chain_id', 'hub_address'],
38+
},
39+
],
40+
}),
41+
chain_id: common.chain({
42+
required: false,
43+
relationships: [
44+
{
45+
type: 'none',
46+
flags: ['spoke_id', 'hub_id'],
47+
},
48+
],
49+
}),
50+
hub_address: common.address({
51+
name: 'hub_address',
52+
description: 'The hub address (e.g. 0x123…)',
53+
relationships: [
54+
{
55+
type: 'none',
56+
flags: ['spoke_id', 'hub_id'],
57+
},
58+
{
59+
type: 'all',
60+
flags: ['chain_id'],
61+
},
62+
],
63+
dependsOn: ['chain_id'],
64+
}),
65+
};
66+
67+
protected override headers = [
68+
{ value: 'Asset' },
69+
{ value: 'Symbol' },
70+
{ value: 'Spoke' },
71+
{ value: 'Chain' },
72+
{ value: 'Supply APY' },
73+
{ value: 'Borrow APY' },
74+
{ value: 'Available Liquidity' },
75+
{ value: 'Total Borrowed' },
76+
{ value: 'Can Supply' },
77+
{ value: 'Can Borrow' },
78+
{ value: 'Collateral' },
79+
{ value: 'ID' },
80+
];
81+
82+
private getReservesRequest(): ResultAsync<
83+
ReservesRequest,
84+
InvariantError | UnexpectedError
85+
> {
86+
return ResultAsync.fromPromise(
87+
this.parse(ListReserves),
88+
(error) => new InvariantError(String(error)),
89+
).andThen(({ flags }) => {
90+
if (flags.spoke_id) {
91+
return ok({
92+
query: { spokeId: flags.spoke_id },
93+
});
94+
}
95+
96+
if (flags.hub_id) {
97+
return ok({
98+
query: { hubId: flags.hub_id },
99+
});
100+
}
101+
102+
if (flags.chain_id && flags.hub_address) {
103+
return ok({
104+
query: {
105+
hub: {
106+
address: flags.hub_address,
107+
chainId: flags.chain_id,
108+
},
109+
},
110+
});
111+
}
112+
113+
if (flags.chain_id) {
114+
return ok({
115+
query: { chainIds: [flags.chain_id] },
116+
});
117+
}
118+
119+
invariant(
120+
false,
121+
'You must provide --spoke_id, --hub_id, --chain_id, or (--hub_address and --chain_id)',
122+
);
123+
});
124+
}
125+
126+
async run(): Promise<Reserve[]> {
127+
const result = await this.getReservesRequest().andThen((request) =>
128+
reserves(this.client, request),
129+
);
130+
131+
if (result.isErr()) {
132+
this.error(result.error);
133+
}
134+
135+
this.display(
136+
result.value.map((item) => [
137+
item.asset.underlying.info.name,
138+
item.asset.underlying.info.symbol,
139+
item.spoke.name,
140+
`${item.chain.name} (${item.chain.chainId})`,
141+
formatApy(item.summary.supplyApy),
142+
formatApy(item.summary.borrowApy),
143+
`${item.summary.supplied.exchange.symbol}${item.summary.supplied.exchange.value.toFixed(2)}`,
144+
`${item.summary.borrowed.exchange.symbol}${item.summary.borrowed.exchange.value.toFixed(2)}`,
145+
item.canSupply ? 'Yes' : 'No',
146+
item.canBorrow ? 'Yes' : 'No',
147+
item.canUseAsCollateral ? 'Yes' : 'No',
148+
item.id,
149+
]),
150+
);
151+
152+
return result.value;
153+
}
154+
}

packages/cli/src/common.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
evmAddress,
77
type HubId,
88
hubId,
9+
production,
10+
type SpokeId,
11+
spokeId,
12+
staging,
913
} from '@aave/client';
1014
import { Command, Flags } from '@oclif/core';
1115
import TtyTable from 'tty-table';
@@ -26,20 +30,67 @@ export const hub = Flags.custom<HubId>({
2630
parse: async (input) => hubId(input),
2731
});
2832

33+
export const spoke = Flags.custom<SpokeId>({
34+
char: 's',
35+
name: 'spoke',
36+
description: 'The spoke ID (e.g. SGVsbG8h…)',
37+
helpValue: '<spoke-id>',
38+
parse: async (input) => spokeId(input),
39+
});
40+
2941
export const address = Flags.custom<EvmAddress>({
3042
parse: async (input) => evmAddress(input),
3143
helpValue: '<evm-address>',
3244
});
3345

46+
function convertBigIntsToStrings(obj: unknown): unknown {
47+
if (typeof obj === 'bigint') {
48+
return obj.toString();
49+
}
50+
51+
if (Array.isArray(obj)) {
52+
return obj.map(convertBigIntsToStrings);
53+
}
54+
55+
if (obj !== null && typeof obj === 'object') {
56+
const result: Record<string, unknown> = {};
57+
for (const [key, value] of Object.entries(obj)) {
58+
result[key] = convertBigIntsToStrings(value);
59+
}
60+
return result;
61+
}
62+
63+
return obj;
64+
}
65+
3466
export abstract class V4Command extends Command {
3567
protected headers: TtyTable.Header[] = [];
3668

3769
public static enableJsonFlag = true;
3870

39-
protected client = AaveClient.create();
71+
static baseFlags = {
72+
staging: Flags.boolean({
73+
hidden: true,
74+
description: 'Use staging environment',
75+
default: false,
76+
}),
77+
};
78+
79+
protected client!: AaveClient;
80+
81+
async init(): Promise<void> {
82+
await super.init();
83+
const { flags } = await this.parse(this.constructor as typeof V4Command);
84+
const environment = flags.staging ? staging : production;
85+
this.client = AaveClient.create({ environment });
86+
}
4087

4188
protected display(rows: unknown[]) {
4289
const out = TtyTable(this.headers, rows).render();
4390
this.log(out);
4491
}
92+
93+
protected toSuccessJson(result: unknown): unknown {
94+
return convertBigIntsToStrings(result);
95+
}
4596
}

0 commit comments

Comments
 (0)