Skip to content

Commit

Permalink
Merge branch 'develop' into sif-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
actions-user committed Jan 17, 2025
2 parents a4eb128 + 1214de8 commit 0c0ef5f
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 62 deletions.
298 changes: 237 additions & 61 deletions packages/plugin-evm/src/actions/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,90 +9,266 @@ import {
executeRoute,
ExtendedChain,
getRoutes,
Route,
} from "@lifi/sdk";

import { initWalletProvider, WalletProvider } from "../providers/wallet";
import { swapTemplate } from "../templates";
import type { SwapParams, Transaction } from "../types";
import { parseEther } from "viem";
import type { SwapParams, SwapQuote, Transaction } from "../types";
import { Address, ByteArray, encodeFunctionData, Hex, parseAbi, parseEther, parseUnits } from "viem";
import { BebopRoute } from '../types/index';

export { swapTemplate };

export class SwapAction {
private config;
private lifiConfig;
private bebopChainsMap;

constructor(private walletProvider: WalletProvider) {
this.config = createConfig({
integrator: "eliza",
chains: Object.values(this.walletProvider.chains).map((config) => ({
id: config.id,
name: config.name,
key: config.name.toLowerCase(),
chainType: "EVM" as const,
nativeToken: {
...config.nativeCurrency,
chainId: config.id,
address: "0x0000000000000000000000000000000000000000",
coinKey: config.nativeCurrency.symbol,
priceUSD: "0",
logoURI: "",
symbol: config.nativeCurrency.symbol,
decimals: config.nativeCurrency.decimals,
name: config.nativeCurrency.name,
},
rpcUrls: {
public: { http: [config.rpcUrls.default.http[0]] },
},
blockExplorerUrls: [config.blockExplorers.default.url],
metamask: {
chainId: `0x${config.id.toString(16)}`,
chainName: config.name,
nativeCurrency: config.nativeCurrency,
rpcUrls: [config.rpcUrls.default.http[0]],
this.walletProvider = walletProvider;
let lifiChains: ExtendedChain[] = [];
for (const config of Object.values(this.walletProvider.chains)) {
try {
lifiChains.push({
id: config.id,
name: config.name,
key: config.name.toLowerCase(),
chainType: "EVM" as const,
nativeToken: {
...config.nativeCurrency,
chainId: config.id,
address: "0x0000000000000000000000000000000000000000",
coinKey: config.nativeCurrency.symbol,
priceUSD: "0",
logoURI: "",
symbol: config.nativeCurrency.symbol,
decimals: config.nativeCurrency.decimals,
name: config.nativeCurrency.name,
},
rpcUrls: {
public: { http: [config.rpcUrls.default.http[0]] },
},
blockExplorerUrls: [config.blockExplorers.default.url],
},
coin: config.nativeCurrency.symbol,
mainnet: true,
diamondAddress: "0x0000000000000000000000000000000000000000",
})) as ExtendedChain[],
});
metamask: {
chainId: `0x${config.id.toString(16)}`,
chainName: config.name,
nativeCurrency: config.nativeCurrency,
rpcUrls: [config.rpcUrls.default.http[0]],
blockExplorerUrls: [config.blockExplorers.default.url],
},
coin: config.nativeCurrency.symbol,
mainnet: true,
diamondAddress: "0x0000000000000000000000000000000000000000",
} as ExtendedChain);
} catch {
// Skip chains with missing config in viem
}
}
this.lifiConfig = createConfig({
integrator: "eliza",
chains: lifiChains
})
this.bebopChainsMap = {
'mainnet': 'ethereum'
}
}

async swap(params: SwapParams): Promise<Transaction> {
const walletClient = this.walletProvider.getWalletClient(params.chain);
const [fromAddress] = await walletClient.getAddresses();

const routes = await getRoutes({
fromChainId: this.walletProvider.getChainConfigs(params.chain).id,
toChainId: this.walletProvider.getChainConfigs(params.chain).id,
fromTokenAddress: params.fromToken,
toTokenAddress: params.toToken,
fromAmount: parseEther(params.amount).toString(),
fromAddress: fromAddress,
options: {
slippage: params.slippage || 0.5,
order: "RECOMMENDED",
},
// Getting quotes from different aggregators and sorting them by minAmount (amount after slippage)
const sortedQuotes: SwapQuote[] = await this.getSortedQuotes(fromAddress, params);

// Trying to execute the best quote by amount, fallback to the next one if it fails
for (const quote of sortedQuotes) {
let res;
switch (quote.aggregator) {
case "lifi":
res = await this.executeLifiQuote(quote);
break;
case "bebop":
res = await this.executeBebopQuote(quote, params);
break
default:
throw new Error("No aggregator found");
}
if (res !== undefined) return res;
}
throw new Error("Execution failed");
}

private async getSortedQuotes(fromAddress: Address, params: SwapParams): Promise<SwapQuote[]> {
const decimalsAbi = parseAbi(['function decimals() view returns (uint8)']);
const decimals = await this.walletProvider.getPublicClient(params.chain).readContract({
address: params.fromToken,
abi: decimalsAbi,
functionName: 'decimals',
});
const quotes: SwapQuote[] | undefined = await Promise.all([
this.getLifiQuote(fromAddress, params, decimals),
this.getBebopQuote(fromAddress, params, decimals)
]);
const sortedQuotes: SwapQuote[] = quotes.filter((quote) => quote !== undefined) as SwapQuote[];
sortedQuotes.sort((a, b) => BigInt(a.minOutputAmount) > BigInt(b.minOutputAmount) ? -1 : 1);
if (sortedQuotes.length === 0) throw new Error("No routes found");
return sortedQuotes;
}

private async getLifiQuote(fromAddress: Address, params: SwapParams, fromTokenDecimals: number): Promise<SwapQuote | undefined> {
try {
const routes = await getRoutes({
fromChainId: this.walletProvider.getChainConfigs(params.chain).id,
toChainId: this.walletProvider.getChainConfigs(params.chain).id,
fromTokenAddress: params.fromToken,
toTokenAddress: params.toToken,
fromAmount: parseUnits(params.amount, fromTokenDecimals).toString(),
fromAddress: fromAddress,
options: {
slippage: params.slippage / 100 || 0.005,
order: "RECOMMENDED",
},
});
if (!routes.routes.length) throw new Error("No routes found");
return {
aggregator: "lifi",
minOutputAmount: routes.routes[0].steps[0].estimate.toAmountMin,
swapData: routes.routes[0]
}
} catch (error) {
console.debug("Error in getLifiQuote:", error.message);
return undefined;
}
}

if (!routes.routes.length) throw new Error("No routes found");
private async getBebopQuote(fromAddress: Address, params: SwapParams, fromTokenDecimals: number): Promise<SwapQuote | undefined> {
try {
const url = `https://api.bebop.xyz/router/${this.bebopChainsMap[params.chain] ?? params.chain}/v1/quote`;
const reqParams = new URLSearchParams({
sell_tokens: params.fromToken,
buy_tokens: params.toToken,
sell_amounts: parseUnits(params.amount, fromTokenDecimals).toString(),
taker_address: fromAddress,
approval_type: 'Standard',
skip_validation: 'true',
gasless: 'false',
source: 'eliza'
});
const response = await fetch(`${url}?${reqParams.toString()}`, {
method: 'GET',
headers: {'accept': 'application/json'},
});
if (!response.ok) {
throw Error(response.statusText);
}
const data = await response.json();
const route: BebopRoute = {
data: data.routes[0].quote.tx.data,
sellAmount: parseUnits(params.amount, fromTokenDecimals).toString(),
approvalTarget: data.routes[0].quote.approvalTarget as `0x${string}`,
from: data.routes[0].quote.tx.from as `0x${string}`,
value: data.routes[0].quote.tx.value.toString(),
to: data.routes[0].quote.tx.to as `0x${string}`,
gas: data.routes[0].quote.tx.gas.toString(),
gasPrice: data.routes[0].quote.tx.gasPrice.toString()
}
return {
aggregator: "bebop",
minOutputAmount: data.routes[0].quote.buyTokens[params.toToken].minimumAmount.toString(),
swapData: route
}

const execution = await executeRoute(routes.routes[0], this.config);
const process = execution.steps[0]?.execution?.process[0];
} catch (error) {
console.debug("Error in getBebopQuote:", error.message);
return undefined;
}
}

private async executeLifiQuote(quote: SwapQuote): Promise<Transaction | undefined> {
try {
const route: Route = quote.swapData as Route;
const execution = await executeRoute(quote.swapData as Route, this.lifiConfig);
const process = execution.steps[0]?.execution?.process[0];

if (!process?.status || process.status === "FAILED") {
throw new Error("Transaction failed");
if (!process?.status || process.status === "FAILED") {
throw new Error("Transaction failed");
}
return {
hash: process.txHash as `0x${string}`,
from: route.fromAddress! as `0x${string}`,
to: route.steps[0].estimate.approvalAddress as `0x${string}`,
value: 0n,
data: process.data as `0x${string}`,
chainId: route.fromChainId
}
} catch (error) {
return undefined;
}
}

return {
hash: process.txHash as `0x${string}`,
from: fromAddress,
to: routes.routes[0].steps[0].estimate
.approvalAddress as `0x${string}`,
value: 0n,
data: process.data as `0x${string}`,
chainId: this.walletProvider.getChainConfigs(params.chain).id,
};
private async executeBebopQuote(quote: SwapQuote, params: SwapParams): Promise<Transaction | undefined> {
try {
const bebopRoute: BebopRoute = quote.swapData as BebopRoute;
const allowanceAbi = parseAbi(['function allowance(address,address) view returns (uint256)']);
const allowance: bigint = await this.walletProvider.getPublicClient(params.chain).readContract({
address: params.fromToken,
abi: allowanceAbi,
functionName: 'allowance',
args: [bebopRoute.from, bebopRoute.approvalTarget]
});
if (allowance < BigInt(bebopRoute.sellAmount)) {
const approvalData = encodeFunctionData({
abi: parseAbi(['function approve(address,uint256)']),
functionName: 'approve',
args: [bebopRoute.approvalTarget, BigInt(bebopRoute.sellAmount)],
});
await this.walletProvider.getWalletClient(params.chain).sendTransaction({
account: this.walletProvider.getWalletClient(params.chain).account,
to: params.fromToken,
value: 0n,
data: approvalData,
kzg: {
blobToKzgCommitment: function (_: ByteArray): ByteArray {
throw new Error("Function not implemented.");
},
computeBlobKzgProof: function (
_blob: ByteArray,
_commitment: ByteArray
): ByteArray {
throw new Error("Function not implemented.");
},
},
chain: undefined,
});
}
const hash = await this.walletProvider.getWalletClient(params.chain).sendTransaction({
account: this.walletProvider.getWalletClient(params.chain).account,
to: bebopRoute.to,
value: BigInt(bebopRoute.value),
data: bebopRoute.data as Hex,
kzg: {
blobToKzgCommitment: function (_: ByteArray): ByteArray {
throw new Error("Function not implemented.");
},
computeBlobKzgProof: function (
_blob: ByteArray,
_commitment: ByteArray
): ByteArray {
throw new Error("Function not implemented.");
},
},
chain: undefined,
});
return {
hash,
from: this.walletProvider.getWalletClient(params.chain).account.address,
to: bebopRoute.to,
value: BigInt(bebopRoute.value),
data: bebopRoute.data as Hex,
};
} catch (error) {
return undefined;
}
}
}

Expand Down
71 changes: 71 additions & 0 deletions packages/plugin-evm/src/tests/swap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { Account, Chain, Hex } from "viem";

import { TransferAction } from "../actions/transfer";
import { WalletProvider } from "../providers/wallet";
import { SwapAction, swapAction } from '../actions/swap';

// Mock the ICacheManager
const mockCacheManager = {
get: vi.fn().mockResolvedValue(null),
set: vi.fn(),
};

describe("Swap Action", () => {
let wp: WalletProvider;

beforeEach(async () => {
vi.clearAllMocks();
mockCacheManager.get.mockResolvedValue(null);

const pk = generatePrivateKey();
const customChains = prepareChains();
wp = new WalletProvider(pk, mockCacheManager as any, customChains);
});

afterEach(() => {
vi.clearAllTimers();
});

describe("Constructor", () => {
it("should initialize with wallet provider", () => {
const ta = new SwapAction(wp);

expect(ta).toBeDefined();
});
});
describe("Swap", () => {
let ta: SwapAction;
let receiver: Account;

beforeEach(() => {
ta = new SwapAction(wp);
receiver = privateKeyToAccount(generatePrivateKey());
});

it("swap throws if not enough gas/tokens", async () => {
const ta = new SwapAction(wp);
await expect(
ta.swap({
chain: "base",
fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
toToken: "0x4200000000000000000000000000000000000006",
amount: "100",
slippage: 0.5,
})
).rejects.toThrow("Execution failed");
});
});
});

const prepareChains = () => {
const customChains: Record<string, Chain> = {};
const chainNames = ["base"];
chainNames.forEach(
(chain) =>
(customChains[chain] = WalletProvider.genChainFromName(chain))
);

return customChains;
};
Loading

0 comments on commit 0c0ef5f

Please sign in to comment.