@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
433 lines (432 loc) • 19.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const x402ActionProvider_1 = require("./x402ActionProvider");
const wallet_providers_1 = require("../../wallet-providers");
const fetch_1 = require("@x402/fetch");
const client_1 = require("@x402/evm/exact/client");
const client_2 = require("@x402/svm/exact/client");
const utils = __importStar(require("./utils"));
const schemas_1 = require("./schemas");
// Mock external modules
jest.mock("@x402/fetch");
jest.mock("@x402/evm/exact/client");
jest.mock("@x402/svm/exact/client");
jest.mock("./utils");
jest.mock("./schemas", () => ({
...jest.requireActual("./schemas"),
resolveFacilitatorUrl: jest.fn(),
}));
// Create mock functions
const mockFetch = jest.fn();
const mockFetchWithPayment = jest.fn();
// Mock x402 client
const mockX402Client = {
registerScheme: jest.fn(),
};
// Mock utils functions
const mockGetX402Networks = jest.fn();
const mockHandleHttpError = jest.fn();
const mockFormatPaymentOption = jest.fn();
const mockFetchAllDiscoveryResources = jest.fn();
const mockFilterByNetwork = jest.fn();
const mockFilterByDescription = jest.fn();
const mockFilterByX402Version = jest.fn();
const mockFilterByKeyword = jest.fn();
const mockFilterByMaxPrice = jest.fn();
const mockFormatSimplifiedResources = jest.fn();
const mockBuildUrlWithParams = jest.fn();
const mockResolveFacilitatorUrl = jest.fn();
// Setup mocks
jest
.mocked(fetch_1.x402Client)
.mockImplementation(() => mockX402Client);
jest.mocked(fetch_1.wrapFetchWithPayment).mockReturnValue(mockFetchWithPayment);
jest
.mocked(client_1.registerExactEvmScheme)
.mockImplementation(() => mockX402Client);
jest
.mocked(client_2.registerExactSvmScheme)
.mockImplementation(() => mockX402Client);
jest.mocked(utils.getX402Networks).mockImplementation(mockGetX402Networks);
jest.mocked(utils.handleHttpError).mockImplementation(mockHandleHttpError);
jest.mocked(utils.formatPaymentOption).mockImplementation(mockFormatPaymentOption);
jest.mocked(utils.fetchAllDiscoveryResources).mockImplementation(mockFetchAllDiscoveryResources);
jest.mocked(utils.filterByNetwork).mockImplementation(mockFilterByNetwork);
jest.mocked(utils.filterByDescription).mockImplementation(mockFilterByDescription);
jest.mocked(utils.filterByX402Version).mockImplementation(mockFilterByX402Version);
jest.mocked(utils.filterByKeyword).mockImplementation(mockFilterByKeyword);
jest.mocked(utils.filterByMaxPrice).mockImplementation(mockFilterByMaxPrice);
jest.mocked(utils.formatSimplifiedResources).mockImplementation(mockFormatSimplifiedResources);
jest.mocked(utils.buildUrlWithParams).mockImplementation(mockBuildUrlWithParams);
jest.mocked(schemas_1.resolveFacilitatorUrl).mockImplementation(mockResolveFacilitatorUrl);
// Mock global fetch
global.fetch = mockFetch;
// Mock wallet provider
const makeMockWalletProvider = (networkId) => {
const mockProvider = Object.create(wallet_providers_1.EvmWalletProvider.prototype);
mockProvider.toSigner = jest.fn().mockReturnValue("mock-signer");
mockProvider.getNetwork = jest.fn().mockReturnValue({ protocolFamily: "evm", networkId });
return mockProvider;
};
// Sample responses based on real examples
const MOCK_PAYMENT_INFO_RESPONSE = {
x402Version: 1,
error: "X-PAYMENT header is required",
accepts: [
{
scheme: "exact",
network: "base-sepolia",
maxAmountRequired: "10000",
resource: "https://www.x402.org/protected",
description: "Access to protected content",
mimeType: "application/json",
payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
maxTimeoutSeconds: 300,
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
extra: {
name: "USDC",
version: "2",
},
},
],
};
const MOCK_PAYMENT_PROOF = {
transaction: "0xcbc385789d3744b52af5106c32809534f64adcbe097e050ec03d6b53fed5d305",
network: "base-sepolia",
payer: "0xa8c1a5D3C372C65c04f91f87a43F549619A9483f",
};
// Helper to create mock Response
const createMockResponse = (options) => {
const headersMap = new Map(Object.entries(options.headers ?? {}));
return {
status: options.status,
statusText: options.statusText ?? "OK",
ok: options.status >= 200 && options.status < 300,
headers: {
get: (name) => headersMap.get(name.toLowerCase()) ?? null,
},
json: jest.fn().mockResolvedValue(options.data),
text: jest.fn().mockResolvedValue(JSON.stringify(options.data)),
};
};
describe("X402ActionProvider", () => {
let provider;
beforeEach(() => {
provider = new x402ActionProvider_1.X402ActionProvider();
jest.clearAllMocks();
// Setup default mock behaviors
mockGetX402Networks.mockImplementation(network => [network.networkId]);
mockBuildUrlWithParams.mockImplementation(url => url);
mockHandleHttpError.mockImplementation((error, url) => {
return JSON.stringify({
error: true,
message: error instanceof Error ? error.message : "Network error",
url: url,
});
});
mockFormatPaymentOption.mockResolvedValue("mocked payment option");
mockResolveFacilitatorUrl.mockReturnValue("https://api.cdp.coinbase.com/platform/v2/x402");
});
afterEach(() => {
jest.clearAllMocks();
});
describe("supportsNetwork", () => {
it("should support base-mainnet", () => {
const network = { protocolFamily: "evm", networkId: "base-mainnet" };
expect(provider.supportsNetwork(network)).toBe(true);
});
it("should support base-sepolia", () => {
const network = { protocolFamily: "evm", networkId: "base-sepolia" };
expect(provider.supportsNetwork(network)).toBe(true);
});
it("should not support unsupported EVM networks", () => {
const network = { protocolFamily: "evm", networkId: "ethereum" };
expect(provider.supportsNetwork(network)).toBe(false);
});
it("should support SVM networks", () => {
const network = { protocolFamily: "svm", networkId: "solana-mainnet" };
expect(provider.supportsNetwork(network)).toBe(true);
});
it("should not support non-EVM/SVM networks", () => {
const network = { protocolFamily: "bitcoin", networkId: "mainnet" };
expect(provider.supportsNetwork(network)).toBe(false);
});
});
describe("makeHttpRequest", () => {
it("should handle successful non-payment requests", async () => {
mockFetch.mockResolvedValue(createMockResponse({
status: 200,
data: { message: "Success" },
headers: { "content-type": "application/json" },
}));
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/free",
method: "GET",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(true);
expect(parsedResult.status).toBe(200);
expect(parsedResult.data).toEqual({ message: "Success" });
});
it("should handle 402 responses with payment options", async () => {
mockGetX402Networks.mockReturnValue(["base-sepolia"]);
mockFormatPaymentOption.mockResolvedValue("10000 USDC on base-sepolia network");
mockFetch.mockResolvedValue(createMockResponse({
status: 402,
data: MOCK_PAYMENT_INFO_RESPONSE,
headers: { "content-type": "application/json" },
}));
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
method: "GET",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("error_402_payment_required");
expect(parsedResult.acceptablePaymentOptions).toEqual(MOCK_PAYMENT_INFO_RESPONSE.accepts);
expect(parsedResult.nextSteps).toBeDefined();
});
it("should handle network errors", async () => {
const error = new TypeError("fetch failed");
mockFetch.mockRejectedValue(error);
const result = await provider.makeHttpRequest(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/endpoint",
method: "GET",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
expect(parsedResult.message).toBeDefined();
});
});
describe("discoverX402Services", () => {
it("should list services without filters", async () => {
const mockResources = [
{
resource: "https://example.com/service1",
x402Version: 1,
accepts: [
{
asset: "0xUSDC",
maxAmountRequired: "90000",
network: "base-sepolia",
scheme: "exact",
description: "Test service 1",
},
],
},
];
const mockSimplified = [
{
url: "https://example.com/service1",
price: "0.09 USDC on base-sepolia",
description: "Test service 1",
},
];
mockFetchAllDiscoveryResources.mockResolvedValue(mockResources);
mockFilterByNetwork.mockReturnValue(mockResources);
mockFilterByDescription.mockReturnValue(mockResources);
mockFilterByX402Version.mockReturnValue(mockResources);
mockFormatSimplifiedResources.mockResolvedValue(mockSimplified);
mockGetX402Networks.mockReturnValue(["base-sepolia"]);
const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
facilitator: "cdp",
x402Versions: [1, 2],
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.total).toBe(1);
expect(parsed.returned).toBe(1);
expect(parsed.services.length).toBe(1);
});
it("should filter services by maxPrice", async () => {
const mockResources = [
{
resource: "https://example.com/service1",
x402Version: 1,
accepts: [
{
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
maxAmountRequired: "90000",
network: "base-sepolia",
scheme: "exact",
description: "Test service 1",
},
],
},
{
resource: "https://example.com/service2",
x402Version: 1,
accepts: [
{
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
maxAmountRequired: "150000",
network: "base-sepolia",
scheme: "exact",
description: "Test service 2",
},
],
},
];
const filteredResources = [mockResources[0]];
const mockSimplified = [
{
url: "https://example.com/service1",
price: "0.09 USDC on base-sepolia",
description: "Test service 1",
},
];
mockFetchAllDiscoveryResources.mockResolvedValue(mockResources);
mockFilterByNetwork.mockReturnValue(mockResources);
mockFilterByDescription.mockReturnValue(mockResources);
mockFilterByX402Version.mockReturnValue(mockResources);
mockFilterByMaxPrice.mockResolvedValue(filteredResources);
mockFormatSimplifiedResources.mockResolvedValue(mockSimplified);
mockGetX402Networks.mockReturnValue(["base-sepolia"]);
const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
facilitator: "cdp",
maxUsdcPrice: 0.1,
x402Versions: [1, 2],
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.returned).toBe(1);
expect(parsed.services[0].url).toBe("https://example.com/service1");
});
it("should handle errors from discovery", async () => {
mockFetchAllDiscoveryResources.mockRejectedValue(new Error("boom"));
const result = await provider.discoverX402Services(makeMockWalletProvider("base-sepolia"), {
facilitator: "cdp",
x402Versions: [1, 2],
});
const parsed = JSON.parse(result);
expect(parsed.error).toBe(true);
expect(parsed.message).toContain("Failed to list x402 services");
});
});
describe("retryWithX402", () => {
it("should successfully retry with payment", async () => {
mockGetX402Networks.mockReturnValue(["base-sepolia"]);
// Encode the payment proof as base64
const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF));
mockFetchWithPayment.mockResolvedValue(createMockResponse({
status: 200,
data: { message: "Paid content" },
headers: {
"content-type": "application/json",
"x-payment-response": encodedPaymentProof,
},
}));
const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
method: "GET",
selectedPaymentOption: {
scheme: "exact",
network: "base-sepolia",
maxAmountRequired: "10000",
asset: "0x456",
},
});
expect(fetch_1.wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client);
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("success");
expect(parsedResult.details.paymentProof).toEqual(MOCK_PAYMENT_PROOF);
});
it("should handle network errors during payment", async () => {
const error = new TypeError("fetch failed");
mockGetX402Networks.mockReturnValue(["base-sepolia"]);
mockFetchWithPayment.mockRejectedValue(error);
const result = await provider.retryWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
method: "GET",
selectedPaymentOption: {
scheme: "exact",
network: "base-sepolia",
maxAmountRequired: "10000",
asset: "0x456",
},
});
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
});
});
describe("makeHttpRequestWithX402", () => {
it("should handle successful direct payment requests", async () => {
// Encode the payment proof as base64
const encodedPaymentProof = btoa(JSON.stringify(MOCK_PAYMENT_PROOF));
mockFetchWithPayment.mockResolvedValue(createMockResponse({
status: 200,
data: { message: "Paid content" },
headers: {
"content-type": "application/json",
"x-payment-response": encodedPaymentProof,
},
}));
const result = await provider.makeHttpRequestWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://www.x402.org/protected",
method: "GET",
});
expect(fetch_1.wrapFetchWithPayment).toHaveBeenCalledWith(fetch, mockX402Client);
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(true);
expect(parsedResult.data).toEqual({ message: "Paid content" });
expect(parsedResult.paymentProof).toEqual(MOCK_PAYMENT_PROOF);
});
it("should handle successful non-payment requests", async () => {
mockFetchWithPayment.mockResolvedValue(createMockResponse({
status: 200,
data: { message: "Free content" },
headers: { "content-type": "application/json" },
}));
const result = await provider.makeHttpRequestWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/free",
method: "GET",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(true);
expect(parsedResult.data).toEqual({ message: "Free content" });
expect(parsedResult.paymentProof).toBeNull();
});
it("should handle network errors", async () => {
const error = new TypeError("fetch failed");
mockFetchWithPayment.mockRejectedValue(error);
const result = await provider.makeHttpRequestWithX402(makeMockWalletProvider("base-sepolia"), {
url: "https://api.example.com/endpoint",
method: "GET",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.error).toBe(true);
});
});
});