edwin-sdk
Version:
SDK for integrating AI agents with DeFi protocols
250 lines (218 loc) • 8.59 kB
text/typescript
import { Pool, EthereumTransactionTypeExtended } from '@aave/contract-helpers';
import {
AaveV3Base,
AaveV3BaseSepolia,
AaveV3Ethereum,
AaveV3Polygon,
AaveV3Sepolia,
AaveV3Arbitrum,
AaveV3BNB,
} from '@bgd-labs/aave-address-book';
import { ethers, providers } from 'ethers';
import { EdwinEVMWallet } from '../../core/wallets/evm_wallet/evm_wallet';
import { type SupportedChain, type SupportedEVMChain } from '../../core/types';
import edwinLogger from '../../utils/logger';
import { SupplyParameters, WithdrawParameters } from './parameters';
import { EdwinService } from '../../core/classes/edwinToolProvider';
interface AaveError extends Error {
code?: string;
error?: {
body?: string;
};
reason?: string;
}
interface PoolSetup {
pool: Pool;
wallet: ethers.Wallet;
provider: providers.Provider;
userAddress: string;
reserveAddress: string;
}
// Define an interface with only the properties we need
interface AaveAddressBook {
POOL: string;
WETH_GATEWAY: string;
ASSETS: {
[key: string]: {
UNDERLYING: string;
};
};
}
export class AaveService extends EdwinService {
public supportedChains: SupportedChain[] = ['base', 'baseSepolia', 'sepolia', 'polygon', 'arbitrum'];
private wallet: EdwinEVMWallet;
constructor(wallet: EdwinEVMWallet) {
super();
this.wallet = wallet;
}
async getPortfolio(): Promise<string> {
return '';
}
private getAaveChain(chain: string): SupportedEVMChain {
const matchedChain = this.supportedChains.find(c => c.toLowerCase() === chain.toLowerCase());
if (!matchedChain) {
throw new Error(`Chain ${chain} is not supported by Aave protocol`);
}
return matchedChain as SupportedEVMChain;
}
/**
* Set up the Aave pool and necessary wallet connections
*/
private async setupPool(chain: SupportedChain, asset: string): Promise<PoolSetup> {
// Get the chain and switch wallet
const aaveChain = this.getAaveChain(chain);
this.wallet.switchChain(aaveChain);
// Setup wallet client and provider
const walletClient = this.wallet.getWalletClient(aaveChain);
const provider = new providers.JsonRpcProvider(walletClient.transport.url);
const ethers_wallet = this.wallet.getEthersWallet(walletClient, provider);
ethers_wallet.connect(provider);
// Create Aave Pool
const addressBook = this.getAddressBook(aaveChain);
const pool = new Pool(ethers_wallet.provider, {
POOL: addressBook.POOL,
WETH_GATEWAY: addressBook.WETH_GATEWAY,
});
// Resolve asset address
const assetKey = Object.keys(addressBook.ASSETS).find(key => key.toLowerCase() === asset.toLowerCase());
if (!assetKey) {
throw new Error(`Unsupported asset: ${asset}`);
}
if (!addressBook.ASSETS[assetKey as keyof typeof addressBook.ASSETS]) {
throw new Error(`Unsupported asset: ${asset}`);
}
const reserve = addressBook.ASSETS[assetKey as keyof typeof addressBook.ASSETS].UNDERLYING;
if (!reserve) {
throw new Error(`Unsupported asset: ${asset}`);
}
// Return all necessary objects
return {
pool,
wallet: ethers_wallet,
provider: ethers_wallet.provider,
userAddress: walletClient.account?.address as string,
reserveAddress: reserve,
};
}
/**
* Execute transactions and handle results
*/
private async executeTransactions(
txs: EthereumTransactionTypeExtended[],
setup: PoolSetup,
actionType: 'supply' | 'withdraw',
amount: number,
asset: string
): Promise<string> {
if (!txs || txs.length === 0) {
throw new Error(`No transaction generated from Aave Pool for ${actionType}`);
}
edwinLogger.info(`Submitting ${actionType} transactions`);
const txReceipts = [];
for (const tx of txs) {
const receipt = await this.submitTransaction(setup.provider, setup.wallet, tx);
txReceipts.push(receipt);
}
// Return the last transaction
const finalTx = txReceipts[txReceipts.length - 1];
return (
`Successfully ${actionType === 'supply' ? 'supplied' : 'withdrew'} ` +
amount +
' ' +
asset +
` ${actionType === 'supply' ? 'to' : 'from'} Aave, transaction signature: ` +
finalTx.transactionHash
);
}
private getAddressBook(chain: SupportedEVMChain): AaveAddressBook {
switch (chain.toLowerCase()) {
case 'base':
return AaveV3Base;
case 'basesepolia':
return AaveV3BaseSepolia;
case 'ethereum':
return AaveV3Ethereum;
case 'sepolia':
return AaveV3Sepolia;
case 'polygon':
return AaveV3Polygon;
case 'arbitrum':
return AaveV3Arbitrum;
case 'bnb':
return AaveV3BNB;
default:
throw new Error(`No Aave address book available for chain: ${chain}`);
}
}
private async submitTransaction(
provider: providers.Provider,
wallet: ethers.Wallet,
tx: EthereumTransactionTypeExtended
): Promise<ethers.providers.TransactionReceipt> {
try {
const extendedTxData = await tx.tx();
const { from, ...txData } = extendedTxData;
const txResponse = await wallet.sendTransaction(txData);
const receipt = await txResponse.wait();
return receipt;
} catch (error) {
// Check if error contains gas estimation error details
const aaveError = error as AaveError;
if (aaveError.code === 'UNPREDICTABLE_GAS_LIMIT') {
const reason = aaveError.error?.body
? JSON.parse(aaveError.error.body).error.message
: aaveError.reason;
throw new Error(`Transaction failed: ${reason}`);
}
throw error;
}
}
async supply(params: SupplyParameters): Promise<string> {
const { chain, amount, asset } = params;
// Validate required parameters
if (!asset) throw new Error('Asset is required');
if (!amount) throw new Error('Amount is required');
try {
// Setup pool and connections
const setup = await this.setupPool(chain as SupportedChain, asset);
// Prepare supply parameters
const supplyParams = {
user: setup.userAddress,
reserve: setup.reserveAddress,
amount: String(amount),
};
// Get supply transaction
const txs = await setup.pool.supply(supplyParams);
// Execute transactions
return await this.executeTransactions(txs, setup, 'supply', amount, asset);
} catch (error: unknown) {
edwinLogger.error('Aave supply error:', error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Aave supply failed: ${message}`);
}
}
async withdraw(params: WithdrawParameters): Promise<string> {
const { chain, amount, asset } = params;
// Validate required parameters
if (!asset) throw new Error('Asset is required');
if (!amount) throw new Error('Amount is required');
try {
// Setup pool and connections
const setup = await this.setupPool(chain as SupportedChain, asset);
// Prepare withdraw parameters
const withdrawParams = {
user: setup.userAddress,
reserve: setup.reserveAddress,
amount: String(amount),
};
// Get withdraw transaction
const txs = await setup.pool.withdraw(withdrawParams);
// Execute transactions
return await this.executeTransactions(txs, setup, 'withdraw', amount, asset);
} catch (error: unknown) {
edwinLogger.error('Aave withdraw error:', error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Aave withdraw failed: ${message}`);
}
}
}