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
13 changes: 13 additions & 0 deletions packages/apps/aave-yield-v1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Aave Yield V1 (Base-only)

Minimal CLI to select the highest APY Aave V3 stablecoin pool on Base and deposit via the Vincent Aave ability.

## Run

```bash
pnpm nx run aave-yield-v1:run
```

## Env

See `src/lib/config.ts` for required environment variables.
21 changes: 21 additions & 0 deletions packages/apps/aave-yield-v1/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@lit-protocol/aave-yield-v1",
"version": "0.0.0",
"private": true,
"scripts": {
"run": "tsx src/cli/run.ts"
},
"dependencies": {
"@lit-protocol/vincent-ability-aave": "workspace:*",
"@lit-protocol/vincent-ability-relay-link": "workspace:*",
"@lit-protocol/vincent-app-sdk": "workspace:*",
"dotenv": "^16.4.5",
"ethers": "5.8.0",
"tslib": "2.8.1",
"viem": "^2.41.2",
"zod": "^3.23.8"
},
"devDependencies": {
"tsx": "^4.19.4"
}
}
18 changes: 18 additions & 0 deletions packages/apps/aave-yield-v1/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "aave-yield-v1",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/apps/aave-yield-v1/src",
"projectType": "application",
"tags": [],
"targets": {
"run": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm run run",
"cwd": "{projectRoot}"
}
},
"lint": {},
"clean": {}
}
}
96 changes: 96 additions & 0 deletions packages/apps/aave-yield-v1/src/cli/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { disconnectVincentAbilityClients } from '@lit-protocol/vincent-app-sdk/abilityClient';
import { createPublicClient, formatUnits, http, parseUnits } from 'viem';

import { loadConfig } from '../lib/config';
import { buildUserOp } from '../lib/executor/buildUserOp';
import { signUserOp } from '../lib/executor/signUserOp';
import { submitUserOp } from '../lib/executor/submitUserOp';
import { selectTopPool } from '../lib/strategy/selectTopPool';
import { ERC20_ABI } from '../lib/utils/erc20';

async function main() {
// 1. Load env/config.
const config = loadConfig();

// 2. Create Base RPC client.
const baseClient = createPublicClient({
chain: config.chain,
transport: http(config.baseRpcUrl),
});

// 3. Select the top Aave pool on Base.
const topPool = await selectTopPool({
client: baseClient,
chainId: config.chainId,
allowlistSymbols: config.allowlistSymbols,
});

console.log('[strategy] selected pool', {
asset: topPool.asset,
symbol: topPool.symbol,
apr: topPool.apr,
totalSupply: topPool.totalSupply,
});

// 4. Resolve amount + balance check.
const amount = parseUnits(config.depositAmount, topPool.decimals);
const balance = (await baseClient.readContract({
address: topPool.asset,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [config.agentAddress],
})) as bigint;

if (balance < amount) {
throw new Error(
`Insufficient ${topPool.symbol} balance on agent. Need ${config.depositAmount}, have ${formatUnits(
balance,
topPool.decimals,
)}.`,
);
}

// 5. Build the UserOp (approval + supply).
const userOp = await buildUserOp({
baseClient,
agentAddress: config.agentAddress,
asset: topPool.asset,
amount,
appId: config.appId,
chain: config.chain,
zerodevRpcUrl: config.zerodevRpcUrl,
serializedPermissionAccount: config.serializedPermissionAccount,
});

// 6. Precheck + execute to get a signature.
const { signature } = await signUserOp({
userOp,
alchemyRpcUrl: config.alchemyRpcUrl,
delegateePrivateKey: config.delegateePrivateKey,
delegatorPkpEthAddress: config.delegatorPkpAddress,
agentAddress: config.agentAddress,
});

// 7. Submit the signed UserOp.
const receipt = await submitUserOp({
agentAddress: config.agentAddress,
serializedPermissionAccount: config.serializedPermissionAccount,
userOpSignature: signature,
userOp,
chain: config.chain,
zerodevRpcUrl: config.zerodevRpcUrl,
});

console.log('[submit] userOp included', receipt);
}

void (async () => {
try {
await main();
} catch (error) {
console.error('[aave-yield-v1] failed', error);
process.exitCode = 1;
} finally {
await disconnectVincentAbilityClients();
}
})();
63 changes: 63 additions & 0 deletions packages/apps/aave-yield-v1/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { config as loadEnv } from 'dotenv';
import { z } from 'zod';
import { getAddress } from 'viem';
import { base, baseSepolia } from 'viem/chains';
import type { Address, Chain } from 'viem';

const ConfigSchema = z.object({
APP_ID: z.string(),
DELEGATEE_PRIVATE_KEY: z.string(),
AGENT_ADDRESS: z.string(),
DELEGATOR_PKP_ADDRESS: z.string(),
BASE_RPC_URL: z.string().url(),
ALCHEMY_RPC_URL: z.string().url(),
ZERODEV_RPC_URL: z.string().url(),
SERIALIZED_PERMISSION_ACCOUNT: z.string(),
DEPOSIT_AMOUNT: z.string().regex(/^\d+(\.\d+)?$/),
CHAIN_ID: z.string().optional(),
});

export type AppConfig = {
appId: number;
delegateePrivateKey: string;
agentAddress: Address;
delegatorPkpAddress: Address;
baseRpcUrl: string;
alchemyRpcUrl: string;
zerodevRpcUrl: string;
serializedPermissionAccount: string;
depositAmount: string;
chainId: number;
chain: Chain;
allowlistSymbols: string[];
};

export function loadConfig(): AppConfig {
loadEnv();
const env = ConfigSchema.parse(process.env);
const chainId = Number(env.CHAIN_ID ?? 84532);

let chain: Chain;
if (chainId === 8453) {
chain = base;
} else if (chainId === 84532) {
chain = baseSepolia;
} else {
throw new Error(`Unsupported CHAIN_ID ${chainId}. Use 8453 or 84532.`);
}

return {
appId: Number(env.APP_ID),
delegateePrivateKey: env.DELEGATEE_PRIVATE_KEY,
agentAddress: getAddress(env.AGENT_ADDRESS),
delegatorPkpAddress: getAddress(env.DELEGATOR_PKP_ADDRESS),
baseRpcUrl: env.BASE_RPC_URL,
alchemyRpcUrl: env.ALCHEMY_RPC_URL,
zerodevRpcUrl: env.ZERODEV_RPC_URL,
serializedPermissionAccount: env.SERIALIZED_PERMISSION_ACCOUNT,
depositAmount: env.DEPOSIT_AMOUNT,
chainId,
chain,
allowlistSymbols: ['USDC', 'USDbC', 'DAI', 'USDT'],
};
}
82 changes: 82 additions & 0 deletions packages/apps/aave-yield-v1/src/lib/executor/buildUserOp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Address, Chain, PublicClient } from 'viem';

import {
getAaveApprovalTx,
getAaveSupplyTx,
getFeeContractAddress,
toVincentUserOp,
} from '@lit-protocol/vincent-ability-aave';
import { transactionsToZerodevUserOp } from '@lit-protocol/vincent-ability-relay-link';

import { ERC20_ABI } from '../utils/erc20';

export async function buildUserOp(params: {
baseClient: PublicClient;
agentAddress: Address;
asset: Address;
amount: bigint;
appId: number;
chain: Chain;
zerodevRpcUrl: string;
serializedPermissionAccount: string;
}) {
const {
baseClient,
agentAddress,
asset,
amount,
appId,
chain,
zerodevRpcUrl,
serializedPermissionAccount,
} = params;

const feeDiamond = getFeeContractAddress(chain.id);
if (!feeDiamond) {
throw new Error(`Fee Diamond not configured for chain ${chain.id}`);
}

const allowance = (await baseClient.readContract({
address: asset,
abi: ERC20_ABI,
functionName: 'allowance',
args: [agentAddress, feeDiamond],
})) as bigint;

const txs = [] as Array<{ to: Address; data: `0x${string}`; value: `0x${string}` }>;

if (allowance < amount) {
txs.push(
getAaveApprovalTx({
accountAddress: agentAddress,
assetAddress: asset,
amount: amount.toString(),
chainId: chain.id,
}),
);
}

txs.push(
getAaveSupplyTx({
appId,
accountAddress: agentAddress,
assetAddress: asset,
amount: amount.toString(),
chainId: chain.id,
}),
);

const userOp = await transactionsToZerodevUserOp({
permittedAddress: agentAddress,
serializedPermissionAccount,
transactions: txs.map((tx) => ({
to: tx.to,
data: tx.data,
value: BigInt(tx.value).toString(),
})),
chain,
zerodevRpcUrl,
});

return toVincentUserOp(userOp as any);
}
54 changes: 54 additions & 0 deletions packages/apps/aave-yield-v1/src/lib/executor/signUserOp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Hex } from 'viem';

import { getVincentAbilityClient } from '@lit-protocol/vincent-app-sdk/abilityClient';
import { bundledVincentAbility } from '@lit-protocol/vincent-ability-aave';
import { ethers } from 'ethers';

export async function signUserOp(params: {
userOp: Record<string, unknown>;
alchemyRpcUrl: string;
delegateePrivateKey: string;
delegatorPkpEthAddress: string;
agentAddress: string;
}): Promise<{ signature: Hex }> {
const { userOp, alchemyRpcUrl, delegateePrivateKey, delegatorPkpEthAddress, agentAddress } =
params;

const delegateeSigner = new ethers.Wallet(delegateePrivateKey);
const abilityClient = getVincentAbilityClient({
ethersSigner: delegateeSigner,
bundledVincentAbility,
});

const precheck = await abilityClient.precheck(
{
userOp,
alchemyRpcUrl,
},
{
delegatorPkpEthAddress,
agentAddress,
},
);

if (!precheck.success) {
throw new Error(precheck.result?.error || precheck.runtimeError || 'Ability precheck failed');
}

const execute = await abilityClient.execute(
{
userOp,
alchemyRpcUrl,
},
{
delegatorPkpEthAddress,
agentAddress,
},
);

if (!execute.success) {
throw new Error(execute.result?.error || execute.runtimeError || 'Ability execute failed');
}

return { signature: execute.result.signature as Hex };
}
30 changes: 30 additions & 0 deletions packages/apps/aave-yield-v1/src/lib/executor/submitUserOp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Address, Chain, Hex } from 'viem';

import { submitSignedUserOp } from '@lit-protocol/vincent-ability-relay-link';

export async function submitUserOp(params: {
agentAddress: Address;
serializedPermissionAccount: string;
userOpSignature: Hex;
userOp: Record<string, unknown>;
chain: Chain;
zerodevRpcUrl: string;
}) {
const {
agentAddress,
serializedPermissionAccount,
userOpSignature,
userOp,
chain,
zerodevRpcUrl,
} = params;

return submitSignedUserOp({
permittedAddress: agentAddress,
serializedPermissionAccount,
userOpSignature,
userOp,
chain,
zerodevRpcUrl,
});
}
Loading
Loading