@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
393 lines (392 loc) • 18.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const sushiRouterSchemas_1 = require("./sushiRouterSchemas");
const evm_1 = require("sushi/evm");
const sushiRouterActionProvider_1 = require("./sushiRouterActionProvider");
const viem_1 = require("viem");
const constants_1 = require("./constants");
// Mock the entire module
jest.mock("sushi/evm", () => {
const originalModule = jest.requireActual("sushi/evm");
return {
__esModule: true,
...originalModule,
getSwap: jest.fn(originalModule.getSwap),
};
});
const mockedGetSwap = evm_1.getSwap;
describe("Sushi Action Provider Input Schemas", () => {
describe("Swap Schema", () => {
it("should successfully parse valid input", () => {
const validInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
maxSlippage: 0.005,
};
const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(validInput);
expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});
it("should fail parsing invalid fromAssetAddress", () => {
const invalidInput = {
fromAssetAddress: "invalid-address",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
maxSlippage: 0.005,
};
const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should fail parsing invalid toAssetAddress", () => {
const invalidInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "invalid-address",
maxSlippage: 0.005,
};
const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should fail parsing invalid maxSlippage (>1)", () => {
const invalidInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
maxSlippage: 1.1,
};
const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should fail parsing invalid maxSlippage (<0)", () => {
const invalidInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
maxSlippage: -1.1,
};
const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
});
describe("Quote Schema", () => {
it("should successfully parse valid input", () => {
const validInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
};
const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(validInput);
expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});
it("should fail parsing invalid fromAssetAddress", () => {
const invalidInput = {
fromAssetAddress: "invalid-address",
amount: "0.0001",
toAssetAddress: "0x1234567890123456789012345678901234567890",
maxSlippage: 0.005,
};
const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should fail parsing invalid toAssetAddress", () => {
const invalidInput = {
fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
amount: "0.0001",
toAssetAddress: "invalid-address",
maxSlippage: 0.005,
};
const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
});
});
describe("Sushi Action Provider", () => {
let actionProvider;
let mockWallet;
const amountIn = BigInt(1000000);
const amountOut = BigInt(500000);
const nativeToken = {
address: evm_1.nativeAddress,
symbol: "ETH",
name: "Ether",
decimals: 18,
};
const tokenIn = {
address: "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa",
symbol: "TIN",
name: "Token In",
decimals: 18,
};
const tokenOut = {
address: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
symbol: "TOU",
name: "Token Out",
decimals: 18,
};
const user = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF";
const txHash = "0xhash";
const chainId = 1;
const getRouteLog = ({ tokenIn, tokenOut, amountIn, amountOut, }) => [
{
data: (0, viem_1.encodeAbiParameters)((0, viem_1.parseAbiParameters)("address to, address tokenOut, uint256 amountIn, uint256 amountOut, int256 slippage, bytes32 diagnosticsFirst32"), [user, tokenOut.address, amountIn, amountOut, 0n, `0x${"00".repeat(32)}`]),
topics: (0, viem_1.encodeEventTopics)({
abi: constants_1.routeProcessor9Abi_Route,
eventName: "Route",
args: {
from: user,
tokenIn: tokenIn.address,
referralCode: 0,
},
}),
},
];
const getSuccessfullSwapResponse = async ({ tokenIn, amountIn, tokenOut, amountOut, }) => ({
amountIn: String(amountIn),
assumedAmountOut: String(amountOut),
priceImpact: 0,
status: evm_1.RouteStatus.Success,
swapPrice: 1,
tokens: [tokenIn, tokenOut],
tokenFrom: tokenIn,
tokenTo: tokenOut,
tx: {
to: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
data: "0x",
from: user,
value: BigInt(0),
gas: "1000000",
gasPrice: 1000000000,
},
});
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
actionProvider = new sushiRouterActionProvider_1.SushiRouterActionProvider();
mockWallet = {
readContract: jest.fn(),
sendTransaction: jest.fn(),
waitForTransactionReceipt: jest.fn(),
getBalance: jest.fn(),
getNetwork: jest.fn().mockResolvedValue({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
chainId: String(chainId),
}),
getAddress: jest.fn().mockReturnValue(user),
};
});
describe("swap", () => {
it("should successfully perform a swap (token -> token)", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: tokenIn.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
/*
* 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
* 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
* 3. Mock the readContract which checks for the approval (0, not approved)
*/
mockWallet.readContract
.mockResolvedValueOnce(tokenIn.decimals)
.mockResolvedValueOnce(amountIn)
.mockResolvedValueOnce(BigInt(0));
mockWallet.sendTransaction.mockResolvedValue(txHash);
/*
* 1. Mock the waitForTransactionReceipt to return success for the approval tx
* 2. Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
*/
mockWallet.waitForTransactionReceipt
.mockResolvedValueOnce({
status: "success",
})
.mockResolvedValueOnce({
status: "success",
logs: getRouteLog({
tokenIn,
tokenOut,
amountIn,
amountOut,
}),
});
mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
tokenIn,
amountIn,
tokenOut,
amountOut,
}));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.readContract).toHaveBeenCalledTimes(3); // Decimals + Balance + Approval
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(2); // Approval + Swap
expect(mockedGetSwap).toHaveBeenCalledTimes(2);
expect(result).toContain(`Swapped ${(0, viem_1.formatUnits)(amountIn, tokenIn.decimals)} of ${tokenIn.symbol} (${tokenIn.address}) for ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)} of ${tokenOut.symbol} (${tokenOut.address})`);
expect(result).toContain(`Transaction hash: ${txHash}`);
expect(result).toContain(`Transaction link: ${(0, evm_1.getEvmChainById)(chainId).getTransactionUrl(txHash)}`);
expect(result).toContain(`on ${(0, evm_1.getEvmChainById)(chainId).shortName}`);
});
it("should successfully perform a swap (native -> token)", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: nativeToken.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
// Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
mockWallet.getBalance.mockResolvedValue(amountIn);
mockWallet.sendTransaction.mockResolvedValue(txHash);
// Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({
status: "success",
logs: getRouteLog({
tokenIn: nativeToken,
tokenOut,
amountIn,
amountOut,
}),
});
mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
tokenIn: nativeToken,
amountIn,
tokenOut,
amountOut,
}));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.getBalance).toHaveBeenCalledTimes(1);
expect(mockWallet.readContract).toHaveBeenCalledTimes(0); // No balance check nor approval
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // Swap
expect(mockedGetSwap).toHaveBeenCalledTimes(2);
expect(result).toContain(`Swapped ${(0, viem_1.formatUnits)(amountIn, nativeToken.decimals)} of ${nativeToken.symbol} (${nativeToken.address}) for ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)} of ${tokenOut.symbol} (${tokenOut.address})`);
expect(result).toContain(`Transaction hash: ${txHash}`);
expect(result).toContain(`Transaction link: ${(0, evm_1.getEvmChainById)(chainId).getTransactionUrl(txHash)}`);
expect(result).toContain(`on ${(0, evm_1.getEvmChainById)(chainId).shortName}`);
});
it("should fail if there isn't enough balance (native)", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: nativeToken.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
// Mock the readContract which checks for the balance of the fromAssetAddress token (99, not enough balance)
mockWallet.getBalance.mockResolvedValue(amountIn - BigInt(1));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.getBalance).toHaveBeenCalledTimes(1);
expect(mockWallet.readContract).toHaveBeenCalledTimes(0);
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
expect(mockedGetSwap).toHaveBeenCalledTimes(1);
expect(result).toContain(`Swap failed: Insufficient balance for ${nativeToken.symbol} (${nativeToken.address})`);
});
it("should fail if there isn't enough balance (token)", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: tokenIn.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
/*
* 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
* 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
*/
mockWallet.readContract.mockResolvedValueOnce(18).mockResolvedValue(amountIn - BigInt(1));
mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
tokenIn,
amountIn,
tokenOut,
amountOut,
}));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
expect(mockWallet.readContract).toHaveBeenCalledTimes(2);
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
expect(mockedGetSwap).toHaveBeenCalledTimes(1);
expect(result).toContain(`Swap failed: Insufficient balance for ${tokenIn.symbol} (${tokenIn.address})`);
});
it("should not approve if already approved", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: tokenIn.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
/*
* 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
* 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
* 3. Mock the readContract which checks for the approval (1000000, approved)
*/
mockWallet.readContract
.mockResolvedValueOnce(tokenIn.decimals)
.mockResolvedValueOnce(amountIn)
.mockResolvedValueOnce(amountIn);
mockWallet.sendTransaction.mockResolvedValue(txHash);
// Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({
status: "success",
logs: getRouteLog({
tokenIn,
tokenOut,
amountIn,
amountOut,
}),
});
mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
tokenIn,
amountIn,
tokenOut,
amountOut,
}));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
expect(mockWallet.readContract).toHaveBeenCalledTimes(3); // Decimals + Balance + Allowance
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // Swap
expect(mockedGetSwap).toHaveBeenCalledTimes(2);
expect(result).toContain(`Swapped`);
});
it("should fail if there's no route", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: tokenIn.address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};
/*
* 1. Mock the readContract which checks for decimals of the fromAssetAddress token (18, default)
* 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
*/
mockWallet.readContract
.mockResolvedValueOnce(tokenIn.decimals)
.mockResolvedValueOnce(amountIn);
mockedGetSwap.mockReturnValue(new Promise(r => r({
status: evm_1.RouteStatus.NoWay,
})));
const result = await actionProvider.swap(mockWallet, args);
expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
expect(mockWallet.readContract).toHaveBeenCalledTimes(1); // Decimals
expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
expect(mockedGetSwap).toHaveBeenCalledTimes(1);
expect(result).toContain(`No route found to swap ${amountIn} of ${tokenIn.address} for ${tokenOut.address}`);
});
});
describe("quote", () => {
it("should successfully fetch a quote (token -> token)", async () => {
const args = {
amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
fromAssetAddress: tokenIn.address,
toAssetAddress: tokenOut.address,
};
mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
tokenIn,
amountIn,
tokenOut,
amountOut,
}));
const result = await actionProvider.quote(mockWallet, args);
expect(mockedGetSwap).toHaveBeenCalledTimes(1);
expect(result).toContain(`Found a quote for ${tokenIn.symbol} (${tokenIn.address}) -> ${tokenOut.symbol} (${tokenOut.address})`);
expect(result).toContain(`AmountIn: ${(0, viem_1.formatUnits)(amountIn, tokenIn.decimals)}`);
expect(result).toContain(`AmountOut: ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)}`);
});
});
});