edwin-sdk
Version:
SDK for integrating AI agents with DeFi protocols
1,439 lines (1,419 loc) • 147 kB
JavaScript
import { createWalletClient, http, toHex, createPublicClient, formatUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import * as viemChains from 'viem/chains';
import { sepolia } from 'viem/chains';
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { ethers, providers } from 'ethers';
import { PublicKey, Connection, LAMPORTS_PER_SOL, Keypair, Transaction, SystemProgram, VersionedTransaction } from '@solana/web3.js';
import { TokenListProvider } from '@solana/spl-token-registry';
import bs58 from 'bs58';
import axios2 from 'axios';
import { Pool } from '@aave/contract-helpers';
import { AaveV3BNB, AaveV3Arbitrum, AaveV3Polygon, AaveV3Sepolia, AaveV3Ethereum, AaveV3BaseSepolia, AaveV3Base } from '@bgd-labs/aave-address-book';
import { z } from 'zod';
import DLMM, { StrategyType } from '@meteora-ag/dlmm';
import { BN } from '@coral-xyz/anchor';
import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk';
import * as ccxt from 'ccxt';
import { Hyperliquid } from 'hyperliquid';
import { Helius } from 'helius-sdk';
import { tool } from '@langchain/core/tools';
// src/core/wallets/wallet.ts
var EdwinWallet = class {
};
var transports = [];
if (process.env.EDWIN_MCP_MODE !== "true") {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} [${level.toUpperCase()}] ${message}`;
})
)
})
);
}
if (process.env.EDWIN_FILE_LOGGING === "true" || process.env.EDWIN_MCP_MODE === "true") {
try {
const homeDir = os.homedir();
if (!homeDir || homeDir === "/") {
throw new Error("Could not determine home directory for logs");
}
const baseDir = path.join(homeDir, ".edwin");
const logsDir = path.join(baseDir, "logs");
const logFile = path.join(logsDir, "edwin.log");
fs.mkdirSync(baseDir, { recursive: true, mode: 493 });
fs.mkdirSync(logsDir, { recursive: true, mode: 493 });
transports.push(
new winston.transports.File({
filename: logFile,
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
maxsize: 5242880,
// 5MB
maxFiles: 5,
tailable: true
})
);
} catch (error) {
if (process.env.EDWIN_MCP_MODE === "true") {
throw new Error(`Failed to set up required file logging for MCP mode: ${error}`);
}
console.warn("Failed to set up file logging:", error);
}
}
var edwinLogger = winston.createLogger({
level: process.env.LOG_LEVEL || "debug",
exitOnError: false,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} [${level.toUpperCase()}] ${message}`;
})
),
transports
});
var logger_default = edwinLogger;
// src/core/wallets/evm_wallet/evm_public_key_wallet.ts
var erc20Abi = [
{
name: "balanceOf",
type: "function",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ name: "balance", type: "uint256" }],
stateMutability: "view"
},
{
name: "decimals",
type: "function",
inputs: [],
outputs: [{ name: "", type: "uint8" }],
stateMutability: "view"
}
];
var NATIVE_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
var EdwinEVMPublicKeyWallet = class _EdwinEVMPublicKeyWallet extends EdwinWallet {
constructor(publicKey) {
super();
this.currentChain = "mainnet";
this.chains = { ...viemChains };
this.setChains = (chains) => {
if (!chains) {
return;
}
Object.keys(chains).forEach((chain) => {
this.chains[chain] = chains[chain];
});
};
this.setCurrentChain = (chain) => {
this.currentChain = chain;
};
this.createHttpTransport = (chainName) => {
const chain = this.chains[chainName];
if (chain.rpcUrls.custom) {
return http(chain.rpcUrls.custom.http[0]);
}
return http(chain.rpcUrls.default.http[0]);
};
this.walletAddress = publicKey;
}
getAddress() {
return this.walletAddress;
}
getCurrentChain() {
return this.chains[this.currentChain];
}
getPublicClient(chainName) {
const transport = this.createHttpTransport(chainName);
const publicClient = createPublicClient({
chain: this.chains[chainName],
transport
});
return publicClient;
}
getChainConfigs(chainName) {
const chain = viemChains[chainName];
if (!chain?.id) {
throw new Error("Invalid chain name");
}
return chain;
}
async getBalance() {
return this.getBalanceOfWallet(this.getAddress(), this.currentChain);
}
async getWalletBalanceForChain(chainName) {
try {
const client = this.getPublicClient(chainName);
if (!this.walletAddress) {
throw new Error("Account not set");
}
const balance = await client.getBalance({
address: this.walletAddress
});
return formatUnits(balance, 18);
} catch (error) {
logger_default.error("Error getting wallet balance:", error);
return null;
}
}
addChain(chain) {
this.setChains(chain);
}
switchChain(chainName, customRpcUrl) {
if (!this.chains[chainName]) {
const chain = _EdwinEVMPublicKeyWallet.genChainFromName(chainName, customRpcUrl);
this.addChain({ [chainName]: chain });
}
this.setCurrentChain(chainName);
}
static genChainFromName(chainName, customRpcUrl) {
const baseChain = viemChains[chainName];
if (!baseChain?.id) {
throw new Error("Invalid chain name");
}
if (!customRpcUrl) {
return baseChain;
}
const customRpc = {
http: [customRpcUrl]
};
if ("webSocket" in baseChain.rpcUrls.default && baseChain.rpcUrls.default.webSocket) {
customRpc.webSocket = Array.from(baseChain.rpcUrls.default.webSocket);
}
return {
...baseChain,
rpcUrls: {
...baseChain.rpcUrls,
custom: customRpc
}
};
}
/**
* Get token balance for a specific token address on a given chain
* @param chainName The chain name (e.g., 'base', 'mainnet')
* @param tokenAddress The token contract address or symbol
* @returns Formatted token balance as a string
*/
async getTokenBalance(chainName, tokenAddress) {
try {
const balance = await this.getBalanceOfWallet(this.getAddress(), chainName, tokenAddress);
return balance.toString();
} catch (error) {
logger_default.error("Error getting token balance:", error);
throw error;
}
}
/**
* Get balance of any wallet address on any EVM chain
* @param walletAddress The wallet address to check
* @param chainName The chain to query (e.g., 'mainnet', 'base', etc)
* @param tokenAddress Optional ERC20 token address (if not provided, returns native token balance)
* @returns Balance of the wallet
*/
async getBalanceOfWallet(walletAddress, chainName = this.currentChain, tokenAddress) {
try {
const client = this.getPublicClient(chainName);
if (!tokenAddress || tokenAddress === NATIVE_ETH_ADDRESS) {
const balance2 = await client.getBalance({ address: walletAddress });
return Number(formatUnits(balance2, 18));
}
let decimals = 18;
try {
decimals = await client.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "decimals"
});
} catch (error) {
logger_default.warn(`Could not get decimals for token ${tokenAddress}, defaulting to 18. Error: ${error}`);
}
const balance = await client.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [walletAddress]
});
if (typeof balance !== "bigint") {
throw new Error(`Invalid balance returned from contract. Expected bigint, got ${typeof balance}`);
}
return Number(formatUnits(balance, decimals));
} catch (error) {
logger_default.error(`Error getting balance for wallet ${walletAddress}:`, error);
throw new Error(`Failed to get balance for wallet ${walletAddress}: ${error}`);
}
}
};
var EdwinEVMWallet = class extends EdwinEVMPublicKeyWallet {
constructor(privateKey) {
const account = privateKeyToAccount(privateKey);
super(account.address);
this.account = account;
this.evmPrivateKey = privateKey;
}
/**
* Get the wallet client for a specific chain
*/
getWalletClient(chainName) {
const transport = this.createHttpTransport(chainName);
const walletClient = createWalletClient({
chain: this.chains[chainName],
transport,
account: this.account
});
return walletClient;
}
/**
* Get an ethers.js wallet instance
*/
getEthersWallet(walletClient, provider) {
const ethers_wallet = new ethers.Wallet(this.evmPrivateKey, provider);
return ethers_wallet;
}
/**
* Get the account (private key)
*/
getSigner() {
return this.account;
}
};
// src/utils/index.ts
var INITIAL_DELAY = 1e3;
async function withRetry(operation, context, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
const isTimeout = error instanceof Error && (error.message.toLowerCase().includes("timeout") || error.message.toLowerCase().includes("connectionerror"));
if (!isTimeout) {
throw error;
}
if (attempt === maxRetries) {
logger_default.error(`${context} failed after ${maxRetries} attempts:`, error);
throw new Error(`${context} failed after ${maxRetries} retries: ${lastError.message}`);
}
const delay = INITIAL_DELAY * attempt;
logger_default.warn(`${context} attempt ${attempt} failed, retrying in ${delay}ms:`, error);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
// src/core/wallets/solana_wallet/base_client.ts
var NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
var BaseSolanaWalletClient = class {
constructor(publicKey) {
this.publicKey = typeof publicKey === "string" ? new PublicKey(publicKey) : publicKey;
}
/**
* Get wallet address as string
*/
getAddress() {
return this.publicKey.toBase58();
}
/**
* Get Solana connection
*/
getConnection(customRpcUrl, commitment = "confirmed") {
return new Connection(
customRpcUrl || process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com",
commitment
);
}
/**
* Get token address by symbol
*/
async getTokenAddress(symbol) {
const tokens = await new TokenListProvider().resolve();
const tokenList = tokens.filterByChainId(101).getList();
const token = tokenList.find((t) => t.symbol.toLowerCase() === symbol.toLowerCase());
return token ? token.address : null;
}
/**
* Get balance of the current wallet
*/
async getBalance(mintAddress, commitment = "confirmed") {
return this.getBalanceOfWallet(this.getAddress(), mintAddress, commitment);
}
/**
* Get balance of any wallet
*/
async getBalanceOfWallet(walletAddress, mintAddress, commitment = "confirmed") {
try {
const connection = this.getConnection();
const publicKey = new PublicKey(walletAddress);
if (!mintAddress || mintAddress === NATIVE_SOL_MINT) {
return await connection.getBalance(publicKey, commitment) / LAMPORTS_PER_SOL;
}
const tokenMint = new PublicKey(mintAddress);
const tokenAccounts = await connection.getTokenAccountsByOwner(publicKey, {
mint: tokenMint
});
if (tokenAccounts.value.length === 0) {
return 0;
}
const tokenAccount = tokenAccounts.value[0];
const tokenAccountBalance = await connection.getTokenAccountBalance(tokenAccount.pubkey, commitment);
return tokenAccountBalance.value.uiAmount || 0;
} catch (error) {
logger_default.error(`Error getting balance for wallet ${walletAddress}:`, error);
throw new Error(`Failed to get balance for wallet ${walletAddress}: ${error}`);
}
}
/**
* Get token balance change from a transaction
*/
async getTransactionTokenBalanceChange(signature, mint, commitment = "confirmed") {
let actualOutputAmount;
const connection = this.getConnection();
const txInfo = await withRetry(
() => connection.getParsedTransaction(signature, {
maxSupportedTransactionVersion: 0,
commitment
}),
"Get parsed transaction"
);
if (!txInfo || !txInfo.meta) {
throw new Error("Could not fetch transaction details");
}
if (mint === NATIVE_SOL_MINT) {
const accountKeys = txInfo.transaction.message.accountKeys;
const walletIndex = accountKeys.findIndex((key) => key.pubkey.toString() === this.getAddress());
if (walletIndex === -1) {
throw new Error("Wallet not found in transaction account keys");
}
const preLamports = txInfo.meta.preBalances[walletIndex];
const postLamports = txInfo.meta.postBalances[walletIndex];
const fee = txInfo.meta.fee;
const lamportsReceived = postLamports - preLamports + fee;
actualOutputAmount = lamportsReceived / LAMPORTS_PER_SOL;
} else {
const preTokenBalances = txInfo.meta.preTokenBalances || [];
const postTokenBalances = txInfo.meta.postTokenBalances || [];
const findBalance = (balances) => balances.find(
(balance) => balance.owner && balance.mint && balance.owner === this.getAddress() && balance.mint === mint
);
const preBalanceEntry = findBalance(preTokenBalances);
const postBalanceEntry = findBalance(postTokenBalances);
const preBalance = preBalanceEntry?.uiTokenAmount.uiAmount ?? 0;
const postBalance = postBalanceEntry?.uiTokenAmount.uiAmount ?? 0;
actualOutputAmount = (postBalance || 0) - (preBalance || 0);
}
return actualOutputAmount;
}
};
var JitoJsonRpcClient = class {
constructor(baseUrl, uuid) {
this.baseUrl = baseUrl;
this.uuid = uuid;
this.client = axios2.create({
headers: {
"Content-Type": "application/json"
}
});
}
async sendRequest(endpoint, method, params = []) {
const url = `${this.baseUrl}${endpoint}`;
const data = {
jsonrpc: "2.0",
id: 1,
method,
params
};
try {
const response = await this.client.post(url, data);
return response.data;
} catch (error) {
if (axios2.isAxiosError(error)) {
logger_default.error(`HTTP error: ${error.message}`);
throw error;
} else {
logger_default.error(`Unexpected error: ${error}`);
throw new Error("An unexpected error occurred");
}
}
}
async getTipAccounts() {
const endpoint = this.uuid ? `/bundles?uuid=${this.uuid}` : "/bundles";
return this.sendRequest(endpoint, "getTipAccounts");
}
async getRandomTipAccount() {
const tipAccountsResponse = await this.getTipAccounts();
if (tipAccountsResponse.result && Array.isArray(tipAccountsResponse.result) && tipAccountsResponse.result.length > 0) {
const randomIndex = Math.floor(Math.random() * tipAccountsResponse.result.length);
return tipAccountsResponse.result[randomIndex];
}
throw new Error("No tip accounts available");
}
async sendBundle(params) {
const endpoint = this.uuid ? `/bundles?uuid=${this.uuid}` : "/bundles";
return this.sendRequest(endpoint, "sendBundle", params);
}
async sendTxn(params, bundleOnly = false) {
let endpoint = "/transactions";
const queryParams = [];
if (bundleOnly) {
queryParams.push("bundleOnly=true");
}
if (this.uuid) {
queryParams.push(`uuid=${this.uuid}`);
}
if (queryParams.length > 0) {
endpoint += `?${queryParams.join("&")}`;
}
return this.sendRequest(endpoint, "sendTransaction", params);
}
async getInFlightBundleStatuses(bundleIds) {
const endpoint = this.uuid ? `/bundles?uuid=${this.uuid}` : "/bundles";
return this.sendRequest(endpoint, "getInflightBundleStatuses", [bundleIds]);
}
async getBundleStatuses(bundleIds) {
const endpoint = this.uuid ? `/bundles?uuid=${this.uuid}` : "/bundles";
return this.sendRequest(endpoint, "getBundleStatuses", [bundleIds]);
}
async confirmInflightBundle(bundleId, timeoutMs = 6e4) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const response = await this.getInFlightBundleStatuses([bundleId]);
if (response.result?.value && Array.isArray(response.result.value) && response.result.value.length > 0) {
const bundleStatus = response.result.value[0];
logger_default.info(`Bundle status: ${bundleStatus.status}, Landed slot: ${bundleStatus.landed_slot}`);
if (bundleStatus.status === "Failed") {
return bundleStatus;
} else if (bundleStatus.status === "Landed") {
const detailedStatus = await this.getBundleStatuses([bundleId]);
if (detailedStatus.result?.value && Array.isArray(detailedStatus.result.value) && detailedStatus.result.value.length > 0) {
return detailedStatus.result.value[0];
}
return bundleStatus;
}
} else {
logger_default.info("No status returned for the bundle. It may be invalid or very old.");
}
} catch (error) {
logger_default.error("Error checking bundle status:", error);
}
await new Promise((resolve) => setTimeout(resolve, 2e3));
}
logger_default.info(`Bundle ${bundleId} has not reached a final state within ${timeoutMs}ms`);
return { status: "Pending" };
}
};
// src/core/wallets/solana_wallet/clients/keypair/index.ts
var KeypairClient = class extends BaseSolanaWalletClient {
constructor(privateKey) {
const keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
super(keypair.publicKey);
this.keypair = keypair;
}
/**
* Get the underlying Keypair
*/
getKeypair() {
return this.keypair;
}
/**
* Sign a transaction with the wallet's keypair
*/
async signTransaction(transaction) {
if (transaction instanceof Transaction) {
transaction.partialSign(this.keypair);
} else {
transaction.sign([this.keypair]);
}
return transaction;
}
/**
* Sign multiple transactions with the wallet's keypair
*/
async signAllTransactions(transactions) {
for (const tx of transactions) {
await this.signTransaction(tx);
}
return transactions;
}
/**
* Sign a message with the wallet's keypair
* Note: This is a simplified implementation for compatibility
*/
async signMessage(_message) {
return this.keypair.secretKey.slice(0, 64);
}
/**
* Wait for transaction confirmation
*/
async waitForConfirmationGracefully(connection, signature, timeout = 12e4) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const { value } = await connection.getSignatureStatus(signature, {
searchTransactionHistory: true
});
if (value) {
if (value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(value.err)}`);
}
if (value.confirmationStatus === "confirmed" || value.confirmationStatus === "finalized") {
return value;
}
}
await new Promise((resolve) => setTimeout(resolve, 2e3));
}
throw new Error("Transaction confirmation timed out");
}
/**
* Send a transaction using Jito's low latency transaction send API.
* Supports both legacy Transaction and VersionedTransaction.
*/
async sendTransaction(connection, transaction, signers = []) {
const isVersioned = "version" in transaction;
if (isVersioned) {
await this.prepareVersionedTransaction(transaction, signers);
} else {
await this.prepareLegacyTransaction(connection, transaction, signers);
}
return this.sendViaJito(transaction);
}
/**
* Prepare a versioned transaction for sending
*/
async prepareVersionedTransaction(transaction, signers) {
const allSigners = signers.length > 0 ? [...signers, this.keypair] : [this.keypair];
if (!transaction.signatures[0]) {
transaction.sign(allSigners);
} else if (signers.length > 0) {
transaction.sign(signers);
}
}
/**
* Prepare a legacy transaction for sending
*/
async prepareLegacyTransaction(connection, transaction, signers) {
const jitoClient = new JitoJsonRpcClient(
process.env.JITO_RPC_URL || "https://mainnet.block-engine.jito.wtf/api/v1",
process.env.JITO_UUID
);
const jitoTipAccount = new PublicKey(await jitoClient.getRandomTipAccount());
transaction.add(
SystemProgram.transfer({
fromPubkey: this.publicKey,
toPubkey: jitoTipAccount,
lamports: 1e3
// 0.000001 SOL tip
})
);
const { blockhash } = await connection.getLatestBlockhash("finalized");
transaction.recentBlockhash = blockhash;
transaction.feePayer = this.publicKey;
const allSigners = [this.keypair, ...signers];
transaction.sign(...allSigners);
}
/**
* Send transaction via Jito API
*/
async sendViaJito(transaction) {
const serializedTx = transaction.serialize();
const base64Tx = Buffer.from(serializedTx).toString("base64");
const jitoRpcUrl = process.env.JITO_RPC_URL || "https://mainnet.block-engine.jito.wtf";
const jitoApiEndpoint = `${jitoRpcUrl}/api/v1/transactions`;
const isVersioned = "version" in transaction;
const sendOptions = isVersioned ? { encoding: "base64", maxRetries: 3, skipPreflight: true } : { encoding: "base64" };
const response = await fetch(jitoApiEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "sendTransaction",
params: [base64Tx, sendOptions]
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
}
};
// src/core/wallets/solana_wallet/clients/phantom/index.ts
var PhantomClient = class extends BaseSolanaWalletClient {
constructor(provider) {
if (!provider.solana || !provider.solana.publicKey) {
throw new Error("Phantom wallet is not connected");
}
super(provider.solana.publicKey);
this.provider = provider;
}
/**
* Sign a transaction using Phantom wallet
*/
async signTransaction(transaction) {
if (!this.provider.solana) {
throw new Error("Phantom wallet is not connected");
}
return this.provider.solana.signTransaction(transaction);
}
/**
* Sign multiple transactions using Phantom wallet
*/
async signAllTransactions(transactions) {
if (!this.provider.solana) {
throw new Error("Phantom wallet is not connected");
}
return this.provider.solana.signAllTransactions(transactions);
}
/**
* Sign a message using Phantom wallet
*/
async signMessage(message) {
if (!this.provider.solana) {
throw new Error("Phantom wallet is not connected");
}
const { signature } = await this.provider.solana.signMessage(message);
return signature;
}
/**
* Send a transaction using Phantom wallet
* Note: In Phantom's case, we don't need Connection or additional signers
* as Phantom handles the sending internally
*/
async sendTransaction(_connection, transaction, _signers) {
if (!this.provider.solana) {
throw new Error("Phantom wallet is not connected");
}
return this.provider.solana.sendTransaction(transaction);
}
/**
* Wait for transaction confirmation
*/
async waitForConfirmationGracefully(connection, signature, timeout = 12e4) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const { value } = await connection.getSignatureStatus(signature, {
searchTransactionHistory: true
});
if (value) {
if (value.err) {
logger_default.error(`Transaction failed: ${JSON.stringify(value.err)}`);
return { err: value.err };
}
if (value.confirmationStatus === "confirmed" || value.confirmationStatus === "finalized") {
return value;
}
}
await new Promise((resolve) => setTimeout(resolve, 2e3));
}
const timeoutError = new Error("Transaction confirmation timed out");
logger_default.error("Transaction confirmation timed out");
return { err: timeoutError };
}
};
// src/core/wallets/solana_wallet/clients/publickey/index.ts
var PublicKeyClient = class extends BaseSolanaWalletClient {
constructor(publicKey) {
super(publicKey);
}
/**
* Not supported in public key client - throws error
*/
async signTransaction(_transaction) {
throw new Error("Cannot sign transactions with a read-only PublicKeyClient");
}
/**
* Not supported in public key client - throws error
*/
async signAllTransactions(_transactions) {
throw new Error("Cannot sign transactions with a read-only PublicKeyClient");
}
/**
* Not supported in public key client - throws error
*/
async signMessage(_message) {
throw new Error("Cannot sign messages with a read-only PublicKeyClient");
}
/**
* Not supported in public key client - throws error
*/
async sendTransaction(_connection, _transaction, _signers) {
throw new Error("Cannot send transactions with a read-only PublicKeyClient");
}
/**
* Not supported in public key client - throws error
*/
async waitForConfirmationGracefully(_connection, _signature, _timeout) {
throw new Error("Cannot wait for confirmation with a read-only PublicKeyClient");
}
};
// src/core/wallets/solana_wallet/factory.ts
var SolanaWalletFactory = {
/**
* Create a KeypairClient from a private key
* @param privateKey Base58-encoded private key string
* @returns Keypair wallet client instance
*/
fromPrivateKey(privateKey) {
return new KeypairClient(privateKey);
},
/**
* Create a PublicKeyClient from a public key
* @param publicKey PublicKey object or base58-encoded string
* @returns PublicKey wallet client for read-only operations
*/
fromPublicKey(publicKey) {
return new PublicKeyClient(publicKey);
},
/**
* Create a PhantomClient from a Phantom provider
* @param provider Phantom wallet provider instance
* @returns Phantom wallet client instance
*/
fromPhantom(provider) {
return new PhantomClient(provider);
}
};
function canSign(client) {
return !(client instanceof PublicKeyClient);
}
// src/core/classes/edwinPlugin.ts
var EdwinPlugin = class {
constructor(name, toolProviders) {
this.name = name;
this.toolProviders = toolProviders;
this.tools = [];
}
getToolsArray() {
return this.tools;
}
};
// src/core/classes/edwinToolProvider.ts
var EdwinService = class {
};
// src/plugins/aave/aaveService.ts
var AaveService = class extends EdwinService {
constructor(wallet) {
super();
this.supportedChains = ["base", "baseSepolia", "sepolia", "polygon", "arbitrum"];
this.wallet = wallet;
}
async getPortfolio() {
return "";
}
getAaveChain(chain) {
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;
}
/**
* Set up the Aave pool and necessary wallet connections
*/
async setupPool(chain, asset) {
const aaveChain = this.getAaveChain(chain);
this.wallet.switchChain(aaveChain);
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);
const addressBook = this.getAddressBook(aaveChain);
const pool = new Pool(ethers_wallet.provider, {
POOL: addressBook.POOL,
WETH_GATEWAY: addressBook.WETH_GATEWAY
});
const assetKey = Object.keys(addressBook.ASSETS).find((key) => key.toLowerCase() === asset.toLowerCase());
if (!assetKey) {
throw new Error(`Unsupported asset: ${asset}`);
}
if (!addressBook.ASSETS[assetKey]) {
throw new Error(`Unsupported asset: ${asset}`);
}
const reserve = addressBook.ASSETS[assetKey].UNDERLYING;
if (!reserve) {
throw new Error(`Unsupported asset: ${asset}`);
}
return {
pool,
wallet: ethers_wallet,
provider: ethers_wallet.provider,
userAddress: walletClient.account?.address,
reserveAddress: reserve
};
}
/**
* Execute transactions and handle results
*/
async executeTransactions(txs, setup, actionType, amount, asset) {
if (!txs || txs.length === 0) {
throw new Error(`No transaction generated from Aave Pool for ${actionType}`);
}
logger_default.info(`Submitting ${actionType} transactions`);
const txReceipts = [];
for (const tx of txs) {
const receipt = await this.submitTransaction(setup.provider, setup.wallet, tx);
txReceipts.push(receipt);
}
const finalTx = txReceipts[txReceipts.length - 1];
return `Successfully ${actionType === "supply" ? "supplied" : "withdrew"} ` + amount + " " + asset + ` ${actionType === "supply" ? "to" : "from"} Aave, transaction signature: ` + finalTx.transactionHash;
}
getAddressBook(chain) {
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}`);
}
}
async submitTransaction(provider, wallet, tx) {
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) {
const aaveError = error;
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) {
const { chain, amount, asset } = params;
if (!asset) throw new Error("Asset is required");
if (!amount) throw new Error("Amount is required");
try {
const setup = await this.setupPool(chain, asset);
const supplyParams = {
user: setup.userAddress,
reserve: setup.reserveAddress,
amount: String(amount)
};
const txs = await setup.pool.supply(supplyParams);
return await this.executeTransactions(txs, setup, "supply", amount, asset);
} catch (error) {
logger_default.error("Aave supply error:", error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Aave supply failed: ${message}`);
}
}
async withdraw(params) {
const { chain, amount, asset } = params;
if (!asset) throw new Error("Asset is required");
if (!amount) throw new Error("Amount is required");
try {
const setup = await this.setupPool(chain, asset);
const withdrawParams = {
user: setup.userAddress,
reserve: setup.reserveAddress,
amount: String(amount)
};
const txs = await setup.pool.withdraw(withdrawParams);
return await this.executeTransactions(txs, setup, "withdraw", amount, asset);
} catch (error) {
logger_default.error("Aave withdraw error:", error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Aave withdraw failed: ${message}`);
}
}
};
// src/core/utils/createParameterSchema.ts
function createParameterSchema(schema) {
return {
schema,
type: {}
};
}
// src/plugins/aave/parameters.ts
var SupplyParametersSchema = createParameterSchema(
z.object({
chain: z.string().min(1).describe("The chain to supply assets on"),
asset: z.string().min(1).describe("The asset to supply"),
amount: z.number().positive().describe("The amount to supply")
})
);
var WithdrawParametersSchema = createParameterSchema(
z.object({
chain: z.string().min(1).describe("The chain to withdraw assets from"),
asset: z.string().min(1).describe("The asset to withdraw"),
amount: z.number().positive().describe("The amount to withdraw")
})
);
// src/plugins/aave/aavePlugin.ts
var AavePlugin = class extends EdwinPlugin {
constructor(wallet) {
super("aave", [new AaveService(wallet)]);
this.supportsChain = (chain) => chain.type === "evm";
}
getTools() {
return {
...this.getPublicTools(),
...this.getPrivateTools()
};
}
getPublicTools() {
return {};
}
getPrivateTools() {
const aaveService = this.toolProviders.find((provider) => provider instanceof AaveService);
return {
aaveSupply: {
name: "aave_supply",
description: "Supply assets to Aave protocol",
schema: SupplyParametersSchema.schema,
execute: async (params) => {
return await aaveService.supply(params);
}
},
aaveWithdraw: {
name: "aave_withdraw",
description: "Withdraw assets from Aave protocol",
schema: WithdrawParametersSchema.schema,
execute: async (params) => {
return await aaveService.withdraw(params);
}
}
};
}
};
var aave = (wallet) => new AavePlugin(wallet);
// src/plugins/lido/lidoProtocol.ts
var LidoProtocol = class extends EdwinService {
constructor(wallet) {
super();
this.supportedChains = ["mainnet"];
this.wallet = wallet;
}
async getPortfolio() {
return "";
}
async stake(params) {
const { amount } = params;
throw new Error(`Not implemented. Params: ${amount}`);
}
async unstake(params) {
const { amount } = params;
throw new Error(`Not implemented. Params: ${amount}`);
}
async claimRewards(params) {
const { asset, amount } = params;
throw new Error(`Not implemented. Params: ${asset}, ${amount}`);
}
};
var StakeParametersSchema = createParameterSchema(
z.object({
asset: z.string().min(1).describe("The asset to stake"),
amount: z.number().positive().describe("The amount to stake")
})
);
// src/plugins/lido/lidoPlugin.ts
var LidoPlugin = class extends EdwinPlugin {
constructor(wallet) {
super("lido", [new LidoProtocol(wallet)]);
this.supportsChain = (chain) => chain.type === "evm";
}
getTools() {
return {
...this.getPublicTools(),
...this.getPrivateTools()
};
}
getPublicTools() {
return {};
}
getPrivateTools() {
const lidoProtocol = this.toolProviders.find((provider) => provider instanceof LidoProtocol);
return {
lidoStake: {
name: "lido_stake",
description: "Stake ETH in Lido",
schema: StakeParametersSchema.schema,
execute: async (params) => {
return await lidoProtocol.stake(params);
}
},
lidoUnstake: {
name: "lido_unstake",
description: "Unstake ETH from Lido",
schema: StakeParametersSchema.schema,
execute: async (params) => {
return await lidoProtocol.unstake(params);
}
},
lidoClaimRewards: {
name: "lido_claim_rewards",
description: "Claim staking rewards from Lido",
schema: StakeParametersSchema.schema,
execute: async (params) => {
return await lidoProtocol.claimRewards(params);
}
}
};
}
};
var lido = (wallet) => new LidoPlugin(wallet);
var LuloProtocol = class {
constructor(wallet) {
this.supportedChains = ["solana"];
this.wallet = wallet;
}
async getPortfolio() {
return "";
}
async supply(params) {
try {
if (!canSign(this.wallet)) {
throw new Error("Supply operation requires a wallet with signing capabilities");
}
if (!process.env.FLEXLEND_API_KEY) {
throw new Error("FLEXLEND_API_KEY is not set (For lulo.fi)");
}
if (!params.amount) {
throw new Error("Amount is required");
}
const response = await fetch(`https://api.flexlend.fi/generate/account/deposit?priorityFee=50000`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-wallet-pubkey": this.wallet.publicKey.toBase58(),
"x-api-key": process.env.FLEXLEND_API_KEY
},
body: JSON.stringify({
owner: this.wallet.publicKey.toBase58(),
mintAddress: params.asset,
// This should be the mint address of the asset
depositAmount: params.amount.toString()
})
});
logger_default.info(response);
const responseData = await response.json();
const transactionMeta = responseData.data.transactionMeta;
const luloTxn = VersionedTransaction.deserialize(Buffer.from(transactionMeta[0].transaction, "base64"));
const connection = this.wallet.getConnection();
const { blockhash } = await connection.getLatestBlockhash();
luloTxn.message.recentBlockhash = blockhash;
this.wallet.signTransaction(luloTxn);
const signature = await connection.sendTransaction(luloTxn, {
preflightCommitment: "confirmed",
maxRetries: 3
});
const latestBlockhash = await connection.getLatestBlockhash();
await connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
});
return "Successfully supplied " + params.amount + " " + params.asset + " to Lulo.fi, transaction signature: " + signature;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
throw new Error(`Lulo supply failed: ${errorMessage}`);
}
}
async withdraw(params) {
try {
if (!canSign(this.wallet)) {
throw new Error("Withdraw operation requires a wallet with signing capabilities");
}
const response = await fetch(
`https://blink.lulo.fi/actions/withdraw?amount=${params.amount}&symbol=${params.asset}`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
account: this.wallet.publicKey.toBase58()
})
}
);
const data = await response.json();
return "Successfully withdrew " + params.amount + " " + params.asset + " from Lulo.fi, transaction signature: " + data.signature;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
throw new Error(`Lulo withdraw failed: ${errorMessage}`);
}
}
};
var SupplyParametersSchema2 = createParameterSchema(
z.object({
asset: z.string().min(1).describe("The asset to supply"),
amount: z.number().positive().describe("The amount to supply")
})
);
var WithdrawParametersSchema2 = createParameterSchema(
z.object({
asset: z.string().min(1).describe("The asset to withdraw"),
amount: z.number().positive().describe("The amount to withdraw")
})
);
// src/plugins/lulo/luloPlugin.ts
var LuloPlugin = class extends EdwinPlugin {
constructor(wallet) {
super("lulo", [new LuloProtocol(wallet)]);
this.supportsChain = (chain) => chain.type === "solana";
}
getTools() {
return {
...this.getPublicTools(),
...this.getPrivateTools()
};
}
getPublicTools() {
return {};
}
getPrivateTools() {
const luloProtocol = this.toolProviders.find((provider) => provider instanceof LuloProtocol);
return {
luloSupply: {
name: "lulo_supply",
description: "Supply assets to Lulo protocol",
schema: SupplyParametersSchema2.schema,
execute: async (params) => {
return await luloProtocol.supply(params);
}
},
luloWithdraw: {
name: "lulo_withdraw",
description: "Withdraw assets from Lulo protocol",
schema: WithdrawParametersSchema2.schema,
execute: async (params) => {
return await luloProtocol.withdraw(params);
}
}
};
}
};
var lulo = (wallet) => new LuloPlugin(wallet);
async function calculateAmounts(amount, amountB, activeBinPricePerToken, dlmmPool) {
let totalXAmount;
let totalYAmount;
const getDecimals = (token) => {
if (token.mint) {
return token.mint.decimals;
} else if ("decimal" in token) {
if (typeof token.decimal === "number") {
return token.decimal;
}
return 0;
} else {
return 0;
}
};
const tokenXDecimals = getDecimals(dlmmPool.tokenX);
const tokenYDecimals = getDecimals(dlmmPool.tokenY);
if (amount === "auto" && amountB === "auto") {
throw new TypeError(
"Amount for both first asset and second asset cannot be 'auto' for Meteora liquidity provision"
);
} else if (!amount || !amountB) {
throw new TypeError("Both amounts must be specified for Meteora liquidity provision");
}
if (amount === "auto") {
if (!isNaN(Number(amountB))) {
totalXAmount = new BN(Number(amountB) / Number(activeBinPricePerToken) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amountB) * 10 ** tokenYDecimals);
} else {
throw new TypeError("Invalid amountB value for second token for Meteora liquidity provision");
}
} else if (amountB === "auto") {
if (!isNaN(Number(amount))) {
totalXAmount = new BN(Number(amount) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amount) * Number(activeBinPricePerToken) * 10 ** tokenYDecimals);
} else {
throw new TypeError("Invalid amount value for first token for Meteora liquidity provision");
}
} else if (!isNaN(Number(amount)) && !isNaN(Number(amountB))) {
totalXAmount = new BN(Number(amount) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amountB) * 10 ** tokenYDecimals);
} else {
throw new TypeError("Both amounts must be numbers or 'auto' for Meteora liquidity provision");
}
return [totalXAmount, totalYAmount];
}
async function getParsedTransactionWithRetries(connection, signature) {
for (let i = 0; i < 5; i++) {
const txInfo = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 });
if (txInfo) {
return txInfo;
}
if (i < 2) {
await new Promise((resolve) => setTimeout(resolve, 1e3 * (i + 1)));
}
}
throw new Error("Failed to get parsed transaction after 3 attempts");
}
async function extractBalanceChanges(connection, signature, tokenXAddress, tokenYAddress) {
const METEORA_DLMM_PROGRAM_ID = "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo";
const txInfo = await getParsedTransactionWithRetries(connection, signature);
if (!txInfo || !txInfo.meta) {
throw new Error("Transaction details not found or not parsed");
}
const outerInstructions = txInfo.transaction.message.instructions;
const innerInstructions = txInfo.meta.innerInstructions || [];
const innerMap = {};
for (const inner of innerInstructions) {
innerMap[inner.index] = inner.instructions;
}
const meteoraInstructionIndices = [];
outerInstructions.forEach((ix, index) => {
if (ix.programId?.toString() === METEORA_DLMM_PROGRAM_ID) {
meteoraInstructionIndices.push(index);
}
});
if (meteoraInstructionIndices.length < 2) {
throw new Error("Expected at least two Meteora instructions in the transaction");
}
const removeLiquidityIndex = meteoraInstructionIndices[0];
const claimFeeIndex = meteoraInstructionIndices[1];
const decodeTokenTransfers = (instructions) => {
const transfers = [];
for (const ix of instructions) {
if (ix.program === "spl-token" && ix.parsed?.type === "transferChecked") {
transfers.push(ix.parsed.info);
}
}
return transfers;
};
const removeLiquidityTransfers = innerMap[removeLiquidityIndex] ? decodeTokenTransfers(innerMap[removeLiquidityIndex]) : [];
const claimFeeTransfers = innerMap[claimFeeIndex] ? decodeTokenTransfers(innerMap[claimFeeIndex]) : [];
const liquidityRemovedA = removeLiquidityTransfers.find((transfer) => transfer.mint === tokenXAddress)?.tokenAmount.uiAmount || 0;
const liquidityRemovedB = removeLiquidityTransfers.find((transfer) => transfer.mint === tokenYAddress)?.tokenAmount.uiAmount || 0;
const feesClaimedA = claimFeeTransfers.find((transfer) => transfer.mint === tokenXAddress)?.tokenAmount.uiAmount || 0;
const feesClaimedB = claimFeeTransfers.find((transfer) => transfer.mint === tokenYAddress)?.tokenAmount.uiAmount || 0;
return {
liquidityRemoved: [liquidityRemovedA, liquidityRemovedB],
feesClaimed: [feesClaimedA, feesClaimedB]
};
}
async function extractAddLiquidityTokenAmounts(innerInstructions) {
const tokenAmounts = [];
for (const innerInstruction of innerInstructions) {
if (innerInstruction.instructions) {
for (const instruction of innerInstruction.instructions) {
if (instruction.parsed?.type === "transferChecked") {
logger_default.debug(`Transfer info amounts: ${JSON.stringify(instruction.parsed.info.tokenAmount)}`);
tokenAmounts.push(instruction.parsed.info.tokenAmount);
}
}
}
}
return tokenAmounts;
}
async function verifyAddLiquidityTokenAmounts(connection, signature) {
const txInfo = await getParsedTransactionWithRetries(connection, signature);
if (!txInfo || !txInfo.meta) {
throw new Error("Transaction details not found or not parsed");
}
const innerInstructions = txInfo.meta.innerInstructions || [];
return extractAddLiquidityTokenAmounts(innerInstructions);
}
// src/plugins/meteora/errors.ts
var MeteoraStatisticalBugError = class extends Error {
constructor(message, positionAddress) {
super(message);
this.positionAddress = positionAddress;
this.name = "MeteoraStatisticalBugError";
this.positionAddress = positionAddress;
}
};
// src/errors.ts
var InsufficientBalanceError = class extends Error {
constructor(required, available, symbol) {
super(`Insufficient ${symbol} balance. Required: ${required}, Available: ${available}`);
this.required = required;
this.available = available;
this.symbol = symbol;
this.name = "InsufficientBalanceError";
}
};
// src/plugins/meteora/meteoraProtocol.ts
var _MeteoraProtocol = class _MeteoraProtocol {
constructor(wallet) {
this.wallet = wallet;
}
async getPortfolio() {
return "";
}
async getPositionInfo(positionAddress) {
try {
const response = await fetch(`https://dlmm-api.meteora.ag/position_v2/${positionAddress}`);
if (!response.ok) {
throw new Error(`Failed to fetch position info: ${response.statusText}`);
}
return await response.json();
} catch (error) {
logger_default.error("Error fetching Meteora position info:", error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get Meteora position info: ${message}`);
}
}
async getPools(params) {
const { asset, assetB } = params;
const limit = 10;
if (!asset || !assetB) {
throw new Error("Asset A and Asset B are required for Meteora getPools");
}
const response = await fetch(
`${_MeteoraProtocol.BASE_URL}/pair/all_with_pagination?search_term=${asset}-${assetB}&limit=${limit}`
);
const result = await response.json();
if (!result.pairs) {
throw new Error(`No pool found for ${asset}-${assetB}`);
}
return result.pairs.map((pool) => ({
address: pool.address,
name: pool.name,
bin_step: pool.bin_step,
base_fee_percentage: pool.base_fee_percentage,
max_fee_percentage: pool.max_fee_percentage,
protocol_fee_percentage: pool.protocol_fee_percentage,
liquidity: pool.liquidity,
fees_24h: pool.fees_24h,
trade_volume_24h: pool.trade_volume_24h,
current_price: pool.current_price,
apr_percentage: pool.apr
}));
}
async getPositionsFromPool(params) {
const { poolAddress } = params;
if (!poolAddress) {
throw new Error("Pool address is required for Meteora getPositionsFromPool");
}
const connection = this.wallet.getConnection();
const dlmmPool = await DLMM.create(connection, new PublicKey(poolAddress));
const { userPositions } = await withRetry(
async () => dlmmPool.getPositionsByUserAndLbPair(this.wallet.publicKey),
"Meteora get user positions"
);
return userPositions;
}
async getPositions() {
try {
const connection = this.wallet.getConnection();
return await withRetry(
async () => DLMM.getAllLbPairPositionsByUser(connection, this.wallet.publicKey),
"Meteora getPositions"
);
} catch (error) {
logger_default.error("Meteora getPositions error:", error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Meteora getPositions failed