diff --git a/packages/agent-sdk/examples/erc20transfer.ts b/packages/agent-sdk/examples/erc20transfer.ts index 92998dc..217fcb2 100644 --- a/packages/agent-sdk/examples/erc20transfer.ts +++ b/packages/agent-sdk/examples/erc20transfer.ts @@ -7,6 +7,7 @@ import { numberField, validateInput, type FieldParser, + signRequestFor, } from "../src"; interface Input { @@ -34,14 +35,14 @@ export async function GET(req: Request): Promise { parsers, ); const decimals = await getTokenDecimals(chainId, token); + const tx = erc20Transfer({ + token, + to: recipient, + amount: parseUnits(amount.toString(), decimals), + }); return Response.json( { - transaction: erc20Transfer({ - chainId, - token, - to: recipient, - amount: parseUnits(amount.toString(), decimals), - }), + transaction: signRequestFor({ chainId, metaTransactions: [tx] }), }, { status: 200 }, ); diff --git a/packages/agent-sdk/src/evm/erc20.ts b/packages/agent-sdk/src/evm/erc20.ts index b03a403..88f9c92 100644 --- a/packages/agent-sdk/src/evm/erc20.ts +++ b/packages/agent-sdk/src/evm/erc20.ts @@ -1,58 +1,45 @@ import { erc20Abi } from "viem"; import { encodeFunctionData, type Address } from "viem"; -import { signRequestFor } from ".."; -import { getClient, type SignRequestData } from "near-safe"; +import { getClient, type MetaTransaction } from "near-safe"; import type { TokenInfo } from "./types"; const MAX_APPROVAL = BigInt( "115792089237316195423570985008687907853269984665640564039457584007913129639935", ); -export async function erc20Transfer(params: { - chainId: number; +export function erc20Transfer(params: { token: Address; to: Address; amount: bigint; -}): Promise { - const { chainId, token, to, amount } = params; - return signRequestFor({ - chainId, - metaTransactions: [ - { - to: token, - value: "0x", - data: encodeFunctionData({ - abi: erc20Abi, - functionName: "transfer", - args: [to, amount], - }), - }, - ], - }); +}): MetaTransaction { + const { token, to, amount } = params; + return { + to: token, + value: "0x0", + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [to, amount], + }), + }; } -export async function erc20Approve(params: { - chainId: number; +export function erc20Approve(params: { token: Address; spender: Address; // If not provided, the maximum amount will be approved. amount?: bigint; -}): Promise { - const { chainId, token, spender, amount } = params; - return signRequestFor({ - chainId, - metaTransactions: [ - { - to: token, - value: "0x", - data: encodeFunctionData({ - abi: erc20Abi, - functionName: "approve", - args: [spender, amount ?? MAX_APPROVAL], - }), - }, - ], - }); +}): MetaTransaction { + const { token, spender, amount } = params; + return { + to: token, + value: "0x0", + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, amount ?? MAX_APPROVAL], + }), + }; } export async function checkAllowance( diff --git a/packages/agent-sdk/src/evm/index.ts b/packages/agent-sdk/src/evm/index.ts index 47a3fe3..ff13f36 100644 --- a/packages/agent-sdk/src/evm/index.ts +++ b/packages/agent-sdk/src/evm/index.ts @@ -45,7 +45,8 @@ export function createResponse( return { json: (_data, responseInit) => ({ data: responseData, - ...(responseInit ?? init), + ...init, + ...responseInit, }), }; } diff --git a/packages/agent-sdk/src/evm/weth.ts b/packages/agent-sdk/src/evm/weth.ts index df91376..aced099 100644 --- a/packages/agent-sdk/src/evm/weth.ts +++ b/packages/agent-sdk/src/evm/weth.ts @@ -1,4 +1,4 @@ -import { Network, type MetaTransaction, type SignRequestData } from "near-safe"; +import { Network, type MetaTransaction } from "near-safe"; import { type Address, encodeFunctionData, @@ -7,7 +7,6 @@ import { parseEther, toHex, } from "viem"; -import { signRequestFor } from "."; type NativeAsset = { address: Address; @@ -51,16 +50,6 @@ export function validateWethInput(params: URLSearchParams): { }; } -export function unwrapSignRequest( - chainId: number, - amount: bigint, -): SignRequestData { - return signRequestFor({ - chainId, - metaTransactions: [unwrapMetaTransaction(chainId, amount)], - }); -} - export const unwrapMetaTransaction = ( chainId: number, amount: bigint, @@ -76,16 +65,6 @@ export const unwrapMetaTransaction = ( }; }; -export function wrapSignRequest( - chainId: number, - amount: bigint, -): SignRequestData { - return signRequestFor({ - chainId, - metaTransactions: [wrapMetaTransaction(chainId, amount)], - }); -} - export const wrapMetaTransaction = ( chainId: number, amount: bigint, diff --git a/packages/agent-sdk/tests/evm/erc20.spec.ts b/packages/agent-sdk/tests/evm/erc20.spec.ts index 59f1d0e..812ae0b 100644 --- a/packages/agent-sdk/tests/evm/erc20.spec.ts +++ b/packages/agent-sdk/tests/evm/erc20.spec.ts @@ -18,27 +18,17 @@ describe("ERC20 Utilities", () => { const mockAmount = 1000n; describe("erc20Transfer", () => { - it("creates correct transfer transaction", async () => { + it("creates correct transfer transaction", () => { const params = { - chainId: mockChainId, token: mockAddress, to: mockAddress, amount: mockAmount, }; - const signRequest = await erc20Transfer(params); - - expect(signRequest).toEqual({ - chainId: 1, - method: "eth_sendTransaction", - params: [ - { - data: "0xa9059cbb000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000003e8", - from: "0x0000000000000000000000000000000000000000", - to: "0x1234567890123456789012345678901234567890", - value: "0x", - }, - ], + expect(erc20Transfer(params)).toEqual({ + data: "0xa9059cbb000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000003e8", + to: "0x1234567890123456789012345678901234567890", + value: "0x0", }); }); }); @@ -46,48 +36,28 @@ describe("ERC20 Utilities", () => { describe("erc20Approve", () => { it("creates approval transaction with specific amount", async () => { const params = { - chainId: mockChainId, token: mockAddress, spender: mockAddress, amount: mockAmount, }; - const signRequest = await erc20Approve(params); - - expect(signRequest).toEqual({ - chainId: 1, - method: "eth_sendTransaction", - params: [ - { - data: "0x095ea7b3000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000003e8", - from: "0x0000000000000000000000000000000000000000", - to: "0x1234567890123456789012345678901234567890", - value: "0x", - }, - ], + expect(erc20Approve(params)).toEqual({ + data: "0x095ea7b3000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000003e8", + to: "0x1234567890123456789012345678901234567890", + value: "0x0", }); }); - it("creates approval transaction with max amount when amount not specified", async () => { + it("creates approval transaction with max amount when amount not specified", () => { const params = { - chainId: mockChainId, token: mockAddress, spender: mockAddress, }; - const signRequest = await erc20Approve(params); - - expect(signRequest).toEqual({ - chainId: 1, - method: "eth_sendTransaction", - params: [ - { - data: "0x095ea7b30000000000000000000000001234567890123456789012345678901234567890ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - from: "0x0000000000000000000000000000000000000000", - to: "0x1234567890123456789012345678901234567890", - value: "0x", - }, - ], + expect(erc20Approve(params)).toEqual({ + data: "0x095ea7b30000000000000000000000001234567890123456789012345678901234567890ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + to: "0x1234567890123456789012345678901234567890", + value: "0x0", }); }); }); diff --git a/packages/agent-sdk/tests/evm/index.spec.ts b/packages/agent-sdk/tests/evm/index.spec.ts new file mode 100644 index 0000000..5856342 --- /dev/null +++ b/packages/agent-sdk/tests/evm/index.spec.ts @@ -0,0 +1,164 @@ +import { signRequestFor, createResponse, validateRequest } from "../../src/evm"; +import { zeroAddress } from "viem"; +import type { BaseRequest } from "../../src/evm"; + +// Mock external dependencies +jest.mock("viem", () => ({ + getAddress: jest.fn().mockImplementation((address) => address), + zeroAddress: "0x0000000000000000000000000000000000000000", +})); + +jest.mock("near-safe", () => ({ + NearSafe: { + create: jest.fn().mockImplementation(async () => ({ + address: "0x123", + })), + }, +})); + +describe("evm/index", () => { + describe("signRequestFor", () => { + it("creates a sign request with default from address", () => { + const metaTransactions = [{ to: "0x123", value: "0x0", data: "0xabc" }]; + + const result = signRequestFor({ + chainId: 1, + metaTransactions, + }); + + expect(result).toEqual({ + method: "eth_sendTransaction", + chainId: 1, + params: [ + { + from: zeroAddress, + to: "0x123", + value: "0x0", + data: "0xabc", + }, + ], + }); + }); + + it("creates a sign request with specified from address", () => { + const metaTransactions = [{ to: "0x123", value: "0x0", data: "0xabc" }]; + + const result = signRequestFor({ + from: "0x456", + chainId: 1, + metaTransactions, + }); + + expect(result).toEqual({ + method: "eth_sendTransaction", + chainId: 1, + params: [ + { + from: "0x456", + to: "0x123", + value: "0x0", + data: "0xabc", + }, + ], + }); + }); + }); + + describe("createResponse", () => { + it("creates a response with default status", () => { + const responseData = { message: "Success" }; + + const response = createResponse(responseData); + + expect(response.json({}, {})).toEqual({ + data: responseData, + }); + }); + + it("creates a response with specified status", () => { + const responseData = { message: "Error" }; + + const response = createResponse(responseData, { status: 400 }); + expect(response.json({}, {})).toEqual({ + data: responseData, + status: 400, + }); + }); + }); + + describe("validateRequest", () => { + const mockAddress = "0x1111111111111111111111111111111111111111"; + const mockGetAdapterAddress = jest.fn(); + jest.mock("../../src/evm", () => ({ + ...jest.requireActual("../../src/evm"), + getAdapterAddress: mockGetAdapterAddress, + })); + + it("returns null for valid request", async () => { + const req = { + headers: { + get: jest.fn().mockReturnValue( + JSON.stringify({ + accountId: "testAccount", + evmAddress: "0x123", + }), + ), + }, + } as BaseRequest; + + mockGetAdapterAddress.mockResolvedValue("0x123"); + + const result = await validateRequest(req, "safeSaltNonce"); + + expect(result).toBeNull(); + }); + + it("returns error response for missing accountId or evmAddress", async () => { + const req = { + headers: { + get: jest.fn().mockReturnValue("{}"), + }, + } as BaseRequest; + + const result = await validateRequest(req, "safeSaltNonce"); + + expect(result).toEqual({ + json: expect.any(Function), + }); + + const jsonResponse = result?.json({}, {}); + expect(jsonResponse).toEqual({ + data: { error: "Missing accountId or evmAddress in metadata" }, + status: 400, + }); + }); + + it("returns error response for invalid safeAddress", async () => { + const req = { + headers: { + get: jest.fn().mockReturnValue( + JSON.stringify({ + accountId: "testAccount", + evmAddress: mockAddress, + }), + ), + }, + } as BaseRequest; + + mockGetAdapterAddress.mockResolvedValue(mockAddress); + + const result = await validateRequest(req, "0"); + expect(result).toEqual({ + json: expect.any(Function), + }); + + const jsonResponse = result?.json({}, {}); + expect(jsonResponse).toEqual({ + data: { + error: `Invalid safeAddress in metadata: 0x123 !== ${mockAddress}`, + }, + status: 401, + }); + }); + }); +}); diff --git a/packages/agent-sdk/tests/evm/weth.spec.ts b/packages/agent-sdk/tests/evm/weth.spec.ts index 248d0fb..6644999 100644 --- a/packages/agent-sdk/tests/evm/weth.spec.ts +++ b/packages/agent-sdk/tests/evm/weth.spec.ts @@ -2,8 +2,6 @@ import { Network } from "near-safe"; import { parseEther } from "viem"; import { validateWethInput, - unwrapSignRequest, - wrapSignRequest, getNativeAsset, unwrapMetaTransaction, wrapMetaTransaction, @@ -96,42 +94,6 @@ describe("evm/weth", () => { }); }); - describe("unwrapSignRequest", () => { - it("creates correct unwrap sign request", () => { - const signRequest = unwrapSignRequest(100, 25n); - expect(signRequest).toEqual({ - method: "eth_sendTransaction", - chainId: 100, - params: [ - { - from: "0x0000000000000000000000000000000000000000", - to: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", - value: "0x0", - data: "0x2e1a7d4d0000000000000000000000000000000000000000000000000000000000000019", - }, - ], - }); - }); - }); - - describe("wrapSignRequest", () => { - it("creates correct wrap sign request", () => { - const signRequest = wrapSignRequest(100, 25n); - expect(signRequest).toEqual({ - method: "eth_sendTransaction", - chainId: 100, - params: [ - { - from: "0x0000000000000000000000000000000000000000", - to: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", - value: "0x19", - data: "0xd0e30db0", - }, - ], - }); - }); - }); - describe("getNativeAsset", () => { it("returns correct native asset info for known chain", () => { const result = getNativeAsset(100);