@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
456 lines (455 loc) • 21.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const viem_1 = require("viem");
const utils_1 = require("../../utils");
const moonwellActionProvider_1 = require("./moonwellActionProvider");
const constants_1 = require("./constants");
const schemas_1 = require("./schemas");
const MOCK_MTOKEN_ADDRESS = "0x73902f619CEB9B31FD8EFecf435CbDf89E369Ba6";
const MOCK_ATOMIC_ASSETS = "1000000000000000000";
const MOCK_WHOLE_ASSETS = "1.0";
const MOCK_TOKEN_ADDRESS = "0x4200000000000000000000000000000000000006";
const MOCK_TX_HASH = "0xabcdef1234567890";
const MOCK_RECEIPT = { status: "success", blockNumber: 1234567 };
const WETH_MTOKEN = "0x628ff693426583D9a7FB391E54366292F509D457";
jest.mock("../../utils");
const mockApprove = utils_1.approve;
describe("Moonwell Action Provider", () => {
const actionProvider = new moonwellActionProvider_1.MoonwellActionProvider();
let mockWallet;
let consoleErrorSpy;
beforeEach(() => {
// Mock console.error to suppress debug logs
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
mockWallet = {
getAddress: jest.fn().mockReturnValue(MOCK_TOKEN_ADDRESS),
getNetwork: jest
.fn()
.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }),
sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH),
waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT),
};
mockApprove.mockResolvedValue("Approval successful");
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
describe("Mint Schema", () => {
it("should successfully parse valid input", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const result = schemas_1.MintSchema.safeParse(validInput);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(validInput);
}
});
it("should fail parsing empty input", () => {
const emptyInput = {};
const result = schemas_1.MintSchema.safeParse(emptyInput);
expect(result.success).toBe(false);
});
it("should fail with invalid mToken address", () => {
const invalidInput = {
mTokenAddress: "not_an_address",
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const result = schemas_1.MintSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should handle valid asset string formats", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const validInputs = [
{ ...validInput, assets: "1.5" },
{ ...validInput, assets: "0.00001" },
{ ...validInput, assets: "1000" },
];
validInputs.forEach(input => {
const result = schemas_1.MintSchema.safeParse(input);
expect(result.success).toBe(true);
});
});
it("should reject invalid asset strings", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const invalidInputs = [
{ ...validInput, assets: "" },
{ ...validInput, assets: "1,000" },
{ ...validInput, assets: "1.2.3" },
{ ...validInput, assets: "abc" },
];
invalidInputs.forEach(input => {
const result = schemas_1.MintSchema.safeParse(input);
expect(result.success).toBe(false);
});
});
});
describe("Redeem Schema", () => {
it("should successfully parse valid input", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_ATOMIC_ASSETS,
};
const result = schemas_1.RedeemSchema.safeParse(validInput);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(validInput);
}
});
it("should fail parsing empty input", () => {
const emptyInput = {};
const result = schemas_1.RedeemSchema.safeParse(emptyInput);
expect(result.success).toBe(false);
});
it("should fail with invalid mToken address", () => {
const invalidInput = {
mTokenAddress: "not_an_address",
assets: MOCK_ATOMIC_ASSETS,
};
const result = schemas_1.RedeemSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
it("should handle valid asset string formats", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_ATOMIC_ASSETS,
};
const validInputs = [
{ ...validInput, assets: "1000000000000000000" },
{ ...validInput, assets: "1" },
{ ...validInput, assets: "999999999999999999999" },
];
validInputs.forEach(input => {
const result = schemas_1.RedeemSchema.safeParse(input);
expect(result.success).toBe(true);
});
});
it("should reject invalid asset strings", () => {
const validInput = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_ATOMIC_ASSETS,
};
const invalidInputs = [
{ ...validInput, assets: "" },
{ ...validInput, assets: "1,000" },
{ ...validInput, assets: "1.2.3" },
{ ...validInput, assets: "abc" },
{ ...validInput, assets: "-1.5" },
];
invalidInputs.forEach(input => {
const result = schemas_1.RedeemSchema.safeParse(input);
expect(result.success).toBe(false);
});
});
});
describe("mint", () => {
it("should successfully deposit to Moonwell MToken", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
const atomicAssets = (0, viem_1.parseEther)(MOCK_WHOLE_ASSETS);
const response = await actionProvider.mint(mockWallet, args);
expect(mockApprove).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS, MOCK_MTOKEN_ADDRESS, atomicAssets);
expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: MOCK_MTOKEN_ADDRESS,
data: (0, viem_1.encodeFunctionData)({
abi: constants_1.MTOKEN_ABI,
functionName: "mint",
args: [atomicAssets],
}),
value: 0n,
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
expect(response).toContain(`Deposited ${MOCK_WHOLE_ASSETS}`);
expect(response).toContain(MOCK_TX_HASH);
expect(response).toContain(JSON.stringify(MOCK_RECEIPT));
});
it("should successfully deposit to Moonwell MToken on sepolia", async () => {
const args = {
mTokenAddress: "0x2F39a349A79492a70E152760ce7123A1933eCf28", // Sepolia WETH mToken
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-sepolia",
});
const atomicAssets = (0, viem_1.parseEther)(MOCK_WHOLE_ASSETS);
const response = await actionProvider.mint(mockWallet, args);
expect(mockApprove).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS, args.mTokenAddress, atomicAssets);
expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: args.mTokenAddress,
data: (0, viem_1.encodeFunctionData)({
abi: constants_1.MTOKEN_ABI,
functionName: "mint",
args: [atomicAssets],
}),
value: 0n,
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
expect(response).toContain(`Deposited ${MOCK_WHOLE_ASSETS}`);
expect(response).toContain(MOCK_TX_HASH);
expect(response).toContain(JSON.stringify(MOCK_RECEIPT));
});
it("should reject deposit with zero assets amount", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: "0.0",
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const response = await actionProvider.mint(mockWallet, args);
expect(response).toBe("Error: Assets amount must be greater than 0");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should reject deposit with invalid MToken address not in MOONWELL_BASE_ADDRESSES", async () => {
const invalidMTokenAddress = "0x1234567890123456789012345678901234567890"; // Valid address format but not in MOONWELL_BASE_ADDRESSES
const args = {
mTokenAddress: invalidMTokenAddress,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
const response = await actionProvider.mint(mockWallet, args);
expect(response).toBe("Error: Invalid MToken address");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should reject deposit with invalid MToken address not in MOONWELL_BASE_SEPOLIA_ADDRESSES", async () => {
const invalidMTokenAddress = "0x1234567890123456789012345678901234567890"; // Valid address format but not in MOONWELL_BASE_SEPOLIA_ADDRESSES
const args = {
mTokenAddress: invalidMTokenAddress,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-sepolia",
});
const response = await actionProvider.mint(mockWallet, args);
expect(response).toBe("Error: Invalid MToken address");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should handle approval failure", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockApprove.mockResolvedValue("Error: Approval failed");
const response = await actionProvider.mint(mockWallet, args);
expect(mockApprove).toHaveBeenCalled();
expect(response).toContain("Error approving Moonwell MToken as spender: Error: Approval failed");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should handle deposit errors", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
const error = new Error("Failed to deposit");
mockWallet.sendTransaction.mockRejectedValue(error);
const response = await actionProvider.mint(mockWallet, args);
expect(mockApprove).toHaveBeenCalled();
expect(mockWallet.sendTransaction).toHaveBeenCalled();
expect(response).toBe("Error minting Moonwell MToken: Failed to deposit");
expect(consoleErrorSpy).toHaveBeenCalledWith("DEBUG - Mint error:", error);
});
describe("ETH deposits via router", () => {
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
mockWallet = {
getAddress: jest.fn().mockReturnValue(MOCK_TOKEN_ADDRESS),
getNetwork: jest
.fn()
.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }),
sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH),
waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT),
};
});
it("should use router for ETH deposits on mainnet", async () => {
const args = {
mTokenAddress: WETH_MTOKEN,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
const atomicAssets = (0, viem_1.parseEther)(MOCK_WHOLE_ASSETS);
const response = await actionProvider.mint(mockWallet, args);
// Should not call approve for ETH deposits
expect(mockApprove).not.toHaveBeenCalled();
expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: constants_1.WETH_ROUTER_ADDRESS,
data: (0, viem_1.encodeFunctionData)({
abi: constants_1.ETH_ROUTER_ABI,
functionName: "mint",
args: [MOCK_TOKEN_ADDRESS],
}),
value: atomicAssets,
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
expect(response).toContain(`Deposited ${MOCK_WHOLE_ASSETS} ETH to Moonwell WETH via router`);
expect(response).toContain(MOCK_TX_HASH);
expect(response).toContain(JSON.stringify(MOCK_RECEIPT));
});
it("should not use router for ETH deposits on sepolia", async () => {
// Clear mocks before test
jest.clearAllMocks();
mockApprove.mockResolvedValue("Approval successful");
const args = {
mTokenAddress: "0x2F39a349A79492a70E152760ce7123A1933eCf28", // Sepolia WETH mToken
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-sepolia",
});
const atomicAssets = (0, viem_1.parseEther)(MOCK_WHOLE_ASSETS);
const response = await actionProvider.mint(mockWallet, args);
// Should call approve for token deposits on non-mainnet
expect(mockApprove).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS, args.mTokenAddress, atomicAssets);
expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: args.mTokenAddress,
data: (0, viem_1.encodeFunctionData)({
abi: constants_1.MTOKEN_ABI,
functionName: "mint",
args: [atomicAssets],
}),
value: 0n,
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
expect(response).toContain(`Deposited ${MOCK_WHOLE_ASSETS}`);
expect(response).toContain(MOCK_TX_HASH);
expect(response).toContain(JSON.stringify(MOCK_RECEIPT));
});
it("should handle errors in ETH deposit via router", async () => {
const args = {
mTokenAddress: WETH_MTOKEN,
assets: MOCK_WHOLE_ASSETS,
tokenAddress: MOCK_TOKEN_ADDRESS,
};
mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
const error = new Error("Failed to deposit ETH");
mockWallet.sendTransaction.mockRejectedValue(error);
const response = await actionProvider.mint(mockWallet, args);
// Should not call approve for ETH deposits on mainnet
expect(mockApprove).not.toHaveBeenCalled();
expect(response).toBe("Error minting Moonwell MToken: Failed to deposit ETH");
expect(consoleErrorSpy).toHaveBeenCalledWith("DEBUG - Mint error:", error);
});
});
});
describe("redeem", () => {
it("should successfully redeem from Moonwell MToken", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: "1.0",
};
const decimals = constants_1.MTOKENS_UNDERLYING_DECIMALS[constants_1.MOONWELL_BASE_ADDRESSES[args.mTokenAddress]];
const atomicAssets = (0, viem_1.parseUnits)(args.assets, decimals);
const response = await actionProvider.redeem(mockWallet, args);
const expectedData = (0, viem_1.encodeFunctionData)({
abi: constants_1.MTOKEN_ABI,
functionName: "redeemUnderlying",
args: [atomicAssets],
});
expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: MOCK_MTOKEN_ADDRESS,
data: expectedData,
value: 0n,
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH);
expect(response).toContain(`Redeemed ${args.assets}`);
expect(response).toContain(MOCK_TX_HASH);
expect(response).toContain(JSON.stringify(MOCK_RECEIPT));
});
it("should reject redeem with zero assets amount", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: "0",
};
const response = await actionProvider.redeem(mockWallet, args);
expect(response).toBe("Error: Assets amount must be greater than 0");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should reject redeem with invalid MToken address not in MOONWELL_BASE_ADDRESSES", async () => {
const invalidMTokenAddress = "0x1234567890123456789012345678901234567890"; // Valid address format but not in MOONWELL_BASE_ADDRESSES
const args = {
mTokenAddress: invalidMTokenAddress,
assets: MOCK_ATOMIC_ASSETS,
};
const response = await actionProvider.redeem(mockWallet, args);
expect(response).toBe("Error: Invalid MToken address");
expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
});
it("should handle errors when redeeming", async () => {
const args = {
mTokenAddress: MOCK_MTOKEN_ADDRESS,
assets: MOCK_ATOMIC_ASSETS,
};
const error = new Error("Failed to redeem");
mockWallet.sendTransaction.mockRejectedValue(error);
const response = await actionProvider.redeem(mockWallet, args);
expect(mockWallet.sendTransaction).toHaveBeenCalled();
expect(response).toBe("Error redeeming from Moonwell MToken: Failed to redeem");
expect(consoleErrorSpy).toHaveBeenCalledWith("DEBUG - Redeem error:", error);
});
});
describe("supportsNetwork", () => {
it("should return true for Base Mainnet", () => {
const result = actionProvider.supportsNetwork({
protocolFamily: "evm",
networkId: "base-mainnet",
});
expect(result).toBe(true);
});
it("should return true for Base Sepolia", () => {
const result = actionProvider.supportsNetwork({
protocolFamily: "evm",
networkId: "base-sepolia",
});
expect(result).toBe(true);
});
it("should return false for other EVM networks", () => {
const result = actionProvider.supportsNetwork({
protocolFamily: "evm",
networkId: "ethereum",
});
expect(result).toBe(false);
});
it("should return false for non-EVM networks", () => {
const result = actionProvider.supportsNetwork({
protocolFamily: "bitcoin",
networkId: "base-mainnet",
});
expect(result).toBe(false);
});
});
});