Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TODOS

## CLI: Write/Mutation Commands
**What:** Add write commands (supply, borrow, repay, withdraw, collateral toggle, eMode) to `@aave/cli`.
**Why:** Makes the CLI a full DeFi tool, not just a data viewer. The V3 client already has all transaction-building functions — the missing piece is wallet/signer integration in the CLI (private key, keystore, or hardware wallet).
**Pros:** Complete CLI experience, scriptable DeFi operations.
**Cons:** Requires secure key management, signing UX, gas estimation, approval flows.
**Depends on:** Read-only CLI shipping first.

## CLI: Auto-Generated README
**What:** Set up `oclif readme` script to auto-generate CLI command documentation.
**Why:** Docs drift from code if maintained manually. V4 CLI already uses this pattern (`"readme": "oclif readme"` in scripts).
**Pros:** Zero-maintenance docs, always accurate with command signatures and flags.
**Cons:** None — oclif built-in feature.
**Depends on:** CLI commands being implemented.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"spec:supply": "vitest --project spec packages/spec/markets/supply.spec.ts",
"spec:vaults": "vitest --project spec packages/spec/vaults/vaults.spec.ts",
"spec:withdraw": "vitest --project spec packages/spec/markets/withdraw.spec.ts",
"test": "pnpm test:client && pnpm test:react",
"test": "pnpm test:client && pnpm test:cli && pnpm test:react",
"test:cli": "vitest --project cli --passWithNoTests",
"test:client": "vitest --project client",
"test:client:privy": "vitest --project client packages/client/src/privy.test.ts",
"test:client:thirdweb": "vitest --project client packages/client/src/thirdweb.test.ts",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/dist
/node_modules
oclif.manifest.json
5 changes: 5 additions & 0 deletions packages/cli/bin/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env -S node --import tsx/esm --disable-warning=ExperimentalWarning

import { execute } from '@oclif/core';

await execute({ development: true, dir: import.meta.url });
5 changes: 5 additions & 0 deletions packages/cli/bin/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { execute } from '@oclif/core';

await execute({ dir: import.meta.url });
91 changes: 91 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"name": "@aave/cli",
"version": "0.1.0",
"description": "CLI to interact with the Aave v3 API",
"keywords": [
"aave",
"cli"
],
"repository": {
"directory": "packages/cli",
"type": "git",
"url": "git://github.com/aave/aave-sdk.git"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"aave3": "./bin/run.js"
},
"engines": {
"node": ">=22"
},
"files": [
"./bin",
"./dist",
"./oclif.manifest.json"
],
"scripts": {
"build": "rimraf dist && tsc -b",
"postpack": "rimraf oclif.manifest.json",
"prepack": "oclif manifest",
"readme": "oclif readme"
},
"dependencies": {
"@aave/client": "workspace:*",
"@oclif/core": "^4",
"@oclif/plugin-help": "^6",
"@oclif/plugin-plugins": "^5",
"tty-table": "^5.0.0"
},
"devDependencies": {
"@oclif/test": "^4",
"@types/node": "^24",
"oclif": "^4.22.81",
"rimraf": "^6.1.3",
"tsx": "^4.20.3",
"typescript": "^5"
},
"oclif": {
"bin": "aave3",
"dirname": "aave3",
"commands": "./dist/commands",
"topicSeparator": " ",
"topics": {
"markets": {
"description": "List Aave v3 markets"
},
"reserves": {
"description": "List reserves in a market"
},
"positions": {
"description": "View user positions"
},
"supplies": {
"description": "View user supply positions"
},
"borrows": {
"description": "View user borrow positions"
},
"vaults": {
"description": "List Aave v3 vaults"
},
"gho": {
"description": "GHO savings operations"
},
"history": {
"description": "View user transaction history"
},
"incentives": {
"description": "View user incentive rewards"
},
"health-factor": {
"description": "Preview health factor changes"
}
}
},
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
61 changes: 61 additions & 0 deletions packages/cli/src/commands/borrows/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { MarketUserReserveBorrowPosition } from '@aave/client';
import { userBorrows } from '@aave/client/actions';

import * as common from '../../common.js';

export default class ListBorrows extends common.V3Command {
static override description = 'List user borrow positions';

static override flags = {
user: common.user({
required: true,
description: 'User wallet address',
}),
market: common.market({
required: true,
description: 'The market pool address',
}),
chain: common.chain({
required: true,
description: 'The chain ID the market is on',
}),
};

protected override headers = [
{ value: 'Asset' },
{ value: 'Symbol' },
{ value: 'Debt' },
{ value: 'Debt (USD)' },
{ value: 'Borrow APY' },
];

async run(): Promise<MarketUserReserveBorrowPosition[]> {
const { flags } = await this.parse(ListBorrows);

const result = await userBorrows(this.client, {
markets: [{ address: flags.market, chainId: flags.chain }],
user: flags.user,
});

if (result.isErr()) {
this.error(result.error);
}

if (result.value.length === 0) {
this.log('No borrow positions found for this user');
return result.value;
}

this.display(
result.value.map((item) => [
item.currency.name,
item.currency.symbol,
item.debt.amount.value,
`$${item.debt.usd}`,
item.apy.formatted,
]),
);

return result.value;
}
}
37 changes: 37 additions & 0 deletions packages/cli/src/commands/gho/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { TokenAmount } from '@aave/client';
import { savingsGhoBalance } from '@aave/client/actions';

import * as common from '../../common.js';

export default class GhoBalance extends common.V3Command {
static override description = 'View sGHO (savings GHO) balance for a user';

static override flags = {
user: common.user({
required: true,
description: 'User wallet address',
}),
};

protected override headers = [
{ value: 'Token' },
{ value: 'Balance' },
{ value: 'USD Value' },
];

async run(): Promise<TokenAmount> {
const { flags } = await this.parse(GhoBalance);

const result = await savingsGhoBalance(this.client, {
user: flags.user,
});

if (result.isErr()) {
this.error(result.error);
}

this.display([['sGHO', result.value.amount.value, `$${result.value.usd}`]]);

return result.value;
}
}
124 changes: 124 additions & 0 deletions packages/cli/src/commands/health-factor/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
bigDecimal,
type HealthFactorPreviewRequest,
type HealthFactorPreviewResponse,
} from '@aave/client';
import { healthFactorPreview } from '@aave/client/actions';
import { Flags } from '@oclif/core';

import * as common from '../../common.js';

export default class HealthFactorPreviewCommand extends common.V3Command {
static override description =
'Preview health factor impact of a supply or borrow action';

static override flags = {
action: Flags.string({
required: true,
options: ['supply', 'borrow', 'repay', 'withdraw'],
description: 'The action type to simulate',
}),
market: common.market({
required: true,
description: 'The market pool address',
}),
chain: common.chain({
required: true,
description: 'The chain ID the market is on',
}),
user: common.user({
required: true,
description: 'The user performing the action',
}),
token: common.address({
name: 'token',
required: true,
description: 'The underlying token address',
}),
amount: Flags.string({
required: true,
description: 'The amount to simulate',
}),
};

protected override headers = [
{ value: 'Health Factor Before' },
{ value: 'Health Factor After' },
{ value: 'Change' },
];

async run(): Promise<HealthFactorPreviewResponse> {
const { flags } = await this.parse(HealthFactorPreviewCommand);

const baseRequest = {
market: flags.market,
chainId: flags.chain,
sender: flags.user,
};

const value = bigDecimal(flags.amount);

let action: HealthFactorPreviewRequest['action'];
switch (flags.action) {
case 'supply':
action = {
supply: {
...baseRequest,
amount: { erc20: { currency: flags.token, value } },
},
};
break;
case 'borrow':
action = {
borrow: {
...baseRequest,
amount: { erc20: { currency: flags.token, value } },
},
};
break;
case 'repay':
action = {
repay: {
...baseRequest,
amount: {
erc20: { currency: flags.token, value: { exact: value } },
},
},
};
break;
case 'withdraw':
action = {
withdraw: {
...baseRequest,
amount: {
erc20: { currency: flags.token, value: { exact: value } },
},
},
};
break;
default:
this.error(`Unknown action: ${flags.action}`);
}

const result = await healthFactorPreview(this.client, { action });

if (result.isErr()) {
this.error(result.error);
}

const before = result.value.before;
const after = result.value.after;

this.display([
[
before != null ? Number(before).toFixed(2) : 'N/A (no borrows)',
after != null ? Number(after).toFixed(2) : 'N/A',
before != null && after != null
? `${(Number(after) - Number(before)).toFixed(2)}`
: '-',
],
]);

return result.value;
}
}
Loading
Loading