@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
301 lines (300 loc) • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const web3_js_1 = require("@solana/web3.js");
const splActionProvider_1 = require("./splActionProvider");
jest.mock("@solana/web3.js", () => ({
...jest.requireActual("@solana/web3.js"),
Connection: jest.fn(),
SendTransactionError: jest.fn().mockReturnValue({
message: "Failed to send",
toString: () => "Failed to send",
}),
VersionedTransaction: jest.fn().mockReturnValue({
sign: jest.fn(),
}),
MessageV0: {
compile: jest.fn().mockReturnValue({}),
},
}));
jest.mock("@solana/spl-token", () => {
/**
* Custom error class for token account not found scenarios.
* Used to simulate cases where a token account doesn't exist.
*/
class TokenAccountNotFoundError extends Error {
/**
* Creates a new TokenAccountNotFoundError instance.
* Sets the error message and name to identify token account not found scenarios.
*/
constructor() {
super("Token account not found");
this.name = "TokenAccountNotFoundError";
}
}
return {
getAssociatedTokenAddress: jest.fn(),
getMint: jest.fn(),
getAccount: jest.fn(),
createAssociatedTokenAccountInstruction: jest.fn(),
createTransferCheckedInstruction: jest.fn(),
TokenAccountNotFoundError,
};
});
jest.mock("../../wallet-providers/svmWalletProvider");
describe("SplActionProvider", () => {
let actionProvider;
let mockWallet;
let mockConnection;
let mockGetAssociatedTokenAddress;
let mockGetMint;
let mockGetAccount;
/**
* Set up test environment before each test.
* Initializes mocks and creates fresh instances of required objects.
*/
beforeEach(() => {
jest.clearAllMocks();
const mocked = jest.requireMock("@solana/spl-token");
mockGetAssociatedTokenAddress = mocked.getAssociatedTokenAddress;
mockGetMint = mocked.getMint;
mockGetAccount = mocked.getAccount;
mockGetMint.mockResolvedValue({ decimals: 6 });
mockGetAccount.mockRejectedValue(new Error("getAccount mock not implemented for this test"));
actionProvider = new splActionProvider_1.SplActionProvider();
mockConnection = {
getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: "mockedBlockhash" }),
};
const MOCK_SIGNATURE = "mock-signature";
const mockSignatureReceipt = {
context: { slot: 1234 },
value: { err: null },
};
mockWallet = {
getConnection: jest.fn().mockReturnValue(mockConnection),
getPublicKey: jest.fn().mockReturnValue(new web3_js_1.PublicKey("11111111111111111111111111111111")),
signAndSendTransaction: jest.fn().mockResolvedValue(MOCK_SIGNATURE),
waitForSignatureResult: jest.fn().mockResolvedValue(mockSignatureReceipt),
getAddress: jest.fn().mockReturnValue("11111111111111111111111111111111"),
getNetwork: jest.fn().mockReturnValue({ protocolFamily: "svm", networkId: "mainnet" }),
getName: jest.fn().mockReturnValue("mock-wallet"),
getBalance: jest.fn().mockResolvedValue(BigInt(1000000000)),
nativeTransfer: jest.fn(),
};
});
describe("constructor", () => {
/**
* Test that the SPL action provider is created with the correct name.
*/
it("should create a provider with correct name", () => {
expect(actionProvider["name"]).toBe("spl");
});
});
describe("supportsNetwork", () => {
/**
* Test that the provider correctly identifies Solana networks as supported.
*/
it("should return true for Solana networks", () => {
const network = {
protocolFamily: "svm",
networkId: "solana-mainnet",
};
expect(actionProvider.supportsNetwork(network)).toBe(true);
});
/**
* Test that the provider correctly identifies non-Solana networks as unsupported.
*/
it("should return false for non-Solana networks", () => {
const network = {
protocolFamily: "evm",
networkId: "ethereum-mainnet",
};
expect(actionProvider.supportsNetwork(network)).toBe(false);
});
});
describe("transfer", () => {
const MINT_ADDRESS = "So11111111111111111111111111111111111111112";
const RECIPIENT_ADDRESS = "DjXsn34uz8yCBQ8bevLrEPYYC1RvhHvjzuVF8opNc4K2";
const SENDER_ADDRESS = "11111111111111111111111111111111";
const MOCK_SIGNATURE = "mock-signature";
const transferArgs = {
recipient: RECIPIENT_ADDRESS,
mintAddress: MINT_ADDRESS,
amount: 100,
};
const mockTokenAccount = {
amount: BigInt(1000000000),
address: new web3_js_1.PublicKey(MINT_ADDRESS),
mint: new web3_js_1.PublicKey(MINT_ADDRESS),
owner: new web3_js_1.PublicKey(RECIPIENT_ADDRESS),
delegate: null,
delegatedAmount: BigInt(0),
closeAuthority: null,
isFrozen: false,
isNative: false,
rentExemptReserve: null,
isInitialized: true,
tlvData: new Map(),
};
const mockSignatureReceipt = {
context: { slot: 1234 },
value: { err: null },
};
beforeEach(() => {
mockWallet.getPublicKey.mockReturnValue(new web3_js_1.PublicKey(SENDER_ADDRESS));
mockWallet.getAddress.mockReturnValue(SENDER_ADDRESS);
mockWallet.signAndSendTransaction.mockResolvedValue(MOCK_SIGNATURE);
mockWallet.waitForSignatureResult.mockResolvedValue(mockSignatureReceipt);
});
/**
* Test successful SPL token transfer with all required steps:
* - Account validation
* - Instruction creation
* - Signing
* - Sending
* - Receipt confirmation
*/
it("should successfully transfer SPL tokens", async () => {
mockGetAccount.mockResolvedValue(mockTokenAccount);
const result = await actionProvider.transfer(mockWallet, transferArgs);
expect(mockGetAssociatedTokenAddress).toHaveBeenNthCalledWith(1, new web3_js_1.PublicKey(transferArgs.mintAddress), new web3_js_1.PublicKey(SENDER_ADDRESS));
expect(mockGetAssociatedTokenAddress).toHaveBeenNthCalledWith(2, new web3_js_1.PublicKey(transferArgs.mintAddress), new web3_js_1.PublicKey(transferArgs.recipient));
expect(mockGetMint).toHaveBeenCalledWith(mockConnection, new web3_js_1.PublicKey(transferArgs.mintAddress));
expect(mockGetAccount).toHaveBeenCalled();
expect(mockWallet.signAndSendTransaction).toHaveBeenCalled();
expect(mockWallet.waitForSignatureResult).toHaveBeenCalledWith(MOCK_SIGNATURE);
expect(result).toContain(`Successfully transferred ${transferArgs.amount} tokens`);
expect(result).toContain(`to ${transferArgs.recipient}`);
expect(result).toContain(`Token mint: ${transferArgs.mintAddress}`);
expect(result).toContain(`Signature: ${MOCK_SIGNATURE}`);
});
/**
* Test handling of insufficient balance.
* Verifies that the provider properly checks token balances and prevents transfers when funds are insufficient.
*/
it("should handle insufficient balance", async () => {
mockGetAccount.mockResolvedValue({
...mockTokenAccount,
amount: BigInt(10000000),
});
const result = await actionProvider.transfer(mockWallet, transferArgs);
expect(result).toBe("Error transferring SPL tokens: Error: Insufficient token balance. Have 10000000, need 100000000");
});
/**
* Test handling of Solana-specific send errors.
* Verifies that the provider properly handles and reports SendTransactionError instances.
*/
it("should handle SendTransactionError", async () => {
mockGetAccount.mockResolvedValue(mockTokenAccount);
const error = new web3_js_1.SendTransactionError({
logs: [],
action: "send",
signature: "mock-signature",
transactionMessage: "Failed to send",
});
mockWallet.signAndSendTransaction.mockRejectedValue(error);
const result = await actionProvider.transfer(mockWallet, transferArgs);
expect(result).toBe("Error transferring SPL tokens: Failed to send");
});
/**
* Test handling of general errors during transfer.
* Verifies that the provider properly handles and reports unexpected errors.
*/
it("should handle regular errors", async () => {
mockGetAccount.mockResolvedValue(mockTokenAccount);
const error = new Error("Regular error message");
mockWallet.signAndSendTransaction.mockRejectedValue(error);
const result = await actionProvider.transfer(mockWallet, transferArgs);
expect(result).toBe("Error transferring SPL tokens: Error: Regular error message");
});
/**
* Test that ATA is created by default when missing
*/
it("should create ATA by default when missing", async () => {
mockGetAccount
.mockResolvedValueOnce(mockTokenAccount)
.mockRejectedValueOnce(new Error("Account does not exist"))
.mockResolvedValue(mockTokenAccount);
const result = await actionProvider.transfer(mockWallet, transferArgs);
const { createAssociatedTokenAccountInstruction } = jest.requireMock("@solana/spl-token");
expect(createAssociatedTokenAccountInstruction).toHaveBeenCalled();
expect(result).toContain(`Successfully transferred ${transferArgs.amount} tokens`);
});
});
/**
* Tests for the getBalance method
*/
describe("getBalance", () => {
const MINT_ADDRESS = "So11111111111111111111111111111111111111112";
const TARGET_ADDRESS = "DjXsn34uz8yCBQ8bevLrEPYYC1RvhHvjzuVF8opNc4K2";
const SENDER_ADDRESS = "11111111111111111111111111111111";
const balanceArgs = {
mintAddress: MINT_ADDRESS,
};
const balanceWithAddressArgs = {
mintAddress: MINT_ADDRESS,
address: TARGET_ADDRESS,
};
const mockTokenAccount = {
amount: BigInt(1000000000),
address: new web3_js_1.PublicKey(MINT_ADDRESS),
mint: new web3_js_1.PublicKey(MINT_ADDRESS),
owner: new web3_js_1.PublicKey(TARGET_ADDRESS),
delegate: null,
delegatedAmount: BigInt(0),
closeAuthority: null,
isFrozen: false,
isNative: false,
rentExemptReserve: null,
isInitialized: true,
tlvData: new Map(),
};
beforeEach(() => {
mockWallet.getPublicKey.mockReturnValue(new web3_js_1.PublicKey(SENDER_ADDRESS));
mockWallet.getAddress.mockReturnValue(SENDER_ADDRESS);
});
/**
* Tests that getBalance returns the correct balance for the connected wallet
* when no specific address is provided.
*/
it("should get balance for connected wallet", async () => {
mockGetAccount.mockResolvedValue(mockTokenAccount);
const result = await actionProvider.getBalance(mockWallet, balanceArgs);
expect(mockGetAssociatedTokenAddress).toHaveBeenCalledWith(new web3_js_1.PublicKey(balanceArgs.mintAddress), new web3_js_1.PublicKey(SENDER_ADDRESS));
expect(mockGetMint).toHaveBeenCalledWith(mockConnection, new web3_js_1.PublicKey(balanceArgs.mintAddress));
expect(mockGetAccount).toHaveBeenCalled();
expect(result).toBe(`Balance for ${SENDER_ADDRESS} is 1000 tokens`);
});
/**
* Tests that getBalance returns the correct balance when a specific
* address is provided in the arguments.
*/
it("should get balance for specified address", async () => {
mockGetAccount.mockResolvedValue(mockTokenAccount);
const result = await actionProvider.getBalance(mockWallet, balanceWithAddressArgs);
expect(mockGetAssociatedTokenAddress).toHaveBeenCalledWith(new web3_js_1.PublicKey(balanceWithAddressArgs.mintAddress), new web3_js_1.PublicKey(balanceWithAddressArgs.address));
expect(mockGetMint).toHaveBeenCalledWith(mockConnection, new web3_js_1.PublicKey(balanceWithAddressArgs.mintAddress));
expect(mockGetAccount).toHaveBeenCalled();
expect(result).toBe(`Balance for ${TARGET_ADDRESS} is 1000 tokens`);
});
/**
* Tests that getBalance correctly handles the case where a token account
* does not exist, returning a zero balance instead of an error.
*/
it("should handle non-existent token account", async () => {
const { TokenAccountNotFoundError } = jest.requireMock("@solana/spl-token");
mockGetAccount.mockRejectedValue(new TokenAccountNotFoundError());
const result = await actionProvider.getBalance(mockWallet, balanceArgs);
expect(result).toBe(`Balance for ${SENDER_ADDRESS} is 0 tokens`);
});
/**
* Tests that getBalance properly handles and reports unexpected errors
* that occur during the balance check.
*/
it("should handle errors", async () => {
const error = new Error("Test error");
mockGetAccount.mockRejectedValue(error);
const result = await actionProvider.getBalance(mockWallet, balanceArgs);
expect(result).toBe("Error getting SPL token balance: Error: Test error");
});
});
});