@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
465 lines (464 loc) • 19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getX402Networks = getX402Networks;
exports.getNetworkId = getNetworkId;
exports.fetchAllDiscoveryResources = fetchAllDiscoveryResources;
exports.filterByNetwork = filterByNetwork;
exports.filterByDescription = filterByDescription;
exports.filterByX402Version = filterByX402Version;
exports.filterByKeyword = filterByKeyword;
exports.filterByMaxPrice = filterByMaxPrice;
exports.formatSimplifiedResources = formatSimplifiedResources;
exports.handleHttpError = handleHttpError;
exports.formatPaymentOption = formatPaymentOption;
exports.isUsdcAsset = isUsdcAsset;
exports.convertWholeUnitsToAtomic = convertWholeUnitsToAtomic;
exports.buildUrlWithParams = buildUrlWithParams;
const utils_1 = require("../erc20/utils");
const constants_1 = require("../erc20/constants");
const viem_1 = require("viem");
const wallet_providers_1 = require("../../wallet-providers");
const constants_2 = require("./constants");
/**
* Returns array of matching network identifiers (both v1 and v2 CAIP-2 formats).
* Used for filtering discovery results that may contain either format.
*
* @param network - The network object
* @returns Array of network identifiers that match the wallet's network
*/
function getX402Networks(network) {
const networkId = network.networkId;
if (!networkId) {
return [];
}
return constants_2.NETWORK_MAPPINGS[networkId] ?? [networkId];
}
/**
* Gets network ID from a CAIP-2 or v1 network identifier.
*
* @param network - The x402 network identifier (e.g., "eip155:8453" for v2 or "base" for v1)
* @returns The network ID (e.g., "base-mainnet") or the original if not found
*/
function getNetworkId(network) {
for (const [agentKitId, formats] of Object.entries(constants_2.NETWORK_MAPPINGS)) {
if (formats.includes(network)) {
return agentKitId;
}
}
return network;
}
/**
* Fetches a URL with retry logic and exponential backoff for rate limiting.
*
* @param url - The URL to fetch
* @param maxRetries - Maximum number of retries (default 3)
* @param initialDelayMs - Initial delay in milliseconds (default 1000)
* @returns The fetch Response
*/
async function fetchWithRetry(url, maxRetries = 3, initialDelayMs = 1000) {
let lastError = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url);
if (response.ok) {
return response;
}
if (response.status === 429 && attempt < maxRetries) {
const delayMs = initialDelayMs * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
}
lastError = new Error(`Discovery API error: ${response.status} ${response.statusText}`);
break;
}
throw lastError ?? new Error("Failed to fetch after retries");
}
/**
* Fetches all resources from the discovery API with pagination.
*
* @param discoveryUrl - The base URL for discovery
* @param pageSize - Number of resources per page (default 100)
* @returns Array of all discovered resources
*/
async function fetchAllDiscoveryResources(discoveryUrl, pageSize = 1000) {
const allResources = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const url = new URL(discoveryUrl);
url.searchParams.set("limit", pageSize.toString());
url.searchParams.set("offset", offset.toString());
const response = await fetchWithRetry(url.toString());
const data = await response.json();
const resources = data.resources ?? data.items ?? [];
if (resources.length === 0) {
hasMore = false;
}
else {
allResources.push(...resources);
offset += resources.length;
// Stop when getting fewer resources than requested (last page)
if (resources.length < pageSize) {
hasMore = false;
}
}
}
return allResources;
}
/**
* Filters resources by network compatibility.
* Matches resources that accept any of the wallet's network identifiers (v1 or v2 format).
*
* @param resources - Array of discovery resources
* @param walletNetworks - Array of network identifiers to match
* @returns Filtered array of resources
*/
function filterByNetwork(resources, walletNetworks) {
return resources.filter(resource => {
const accepts = resource.accepts ?? [];
return accepts.some(option => walletNetworks.includes(option.network));
});
}
/**
* Extracts description from a resource based on its x402 version.
* - v1: description is in accepts[].description
* - v2: description is in metadata.description
*
* @param resource - The discovery resource
* @returns The description string or empty string if not found
*/
function getResourceDescription(resource) {
if (resource.x402Version === 2) {
const metadataDesc = resource.metadata?.description;
return typeof metadataDesc === "string" ? metadataDesc : "";
}
// v1: look in accepts[].description
const accepts = resource.accepts ?? [];
for (const option of accepts) {
if (option.description?.trim()) {
return option.description;
}
}
return "";
}
/**
* Filters resources by having a valid description.
* Removes resources with empty or default descriptions.
* Supports both v1 (accepts[].description) and v2 (metadata.description) formats.
*
* @param resources - Array of discovery resources
* @returns Filtered array of resources with valid descriptions
*/
function filterByDescription(resources) {
return resources.filter(resource => {
const desc = getResourceDescription(resource).trim();
return desc && desc !== "" && desc !== "Access to protected content";
});
}
/**
* Filters resources by x402 protocol version.
* Uses the x402Version field on the resource.
*
* @param resources - Array of discovery resources
* @param allowedVersions - Array of allowed versions (default: [1, 2])
* @returns Filtered array of resources matching the allowed versions
*/
function filterByX402Version(resources, allowedVersions = [1, 2]) {
return resources.filter(resource => {
const version = resource.x402Version;
if (version === undefined) {
return true; // Include resources without version info
}
return allowedVersions.includes(version);
});
}
/**
* Filters resources by keyword appearing in description or URL.
* Case-insensitive search.
* Supports both v1 (accepts[].description) and v2 (metadata.description) formats.
*
* @param resources - Array of discovery resources
* @param keyword - The keyword to search for in descriptions and URLs
* @returns Filtered array of resources with matching descriptions or URLs
*/
function filterByKeyword(resources, keyword) {
const lowerKeyword = keyword.toLowerCase();
return resources.filter(resource => {
// Check description (version-aware)
const desc = getResourceDescription(resource).toLowerCase();
if (desc.includes(lowerKeyword)) {
return true;
}
// Also check the URL for keyword matches
const url = (resource.resource ?? resource.url ?? "").toLowerCase();
if (url.includes(lowerKeyword)) {
return true;
}
return false;
});
}
/**
* Filters resources by maximum USDC price.
*
* @param resources - Array of discovery resources
* @param maxUsdcPrice - Maximum price in whole USDC units
* @param walletProvider - Wallet provider for asset identification
* @param walletNetworks - Array of network identifiers to match
* @returns Filtered array of resources within price limit
*/
async function filterByMaxPrice(resources, maxUsdcPrice, walletProvider, walletNetworks) {
const filtered = [];
for (const resource of resources) {
const accepts = resource.accepts ?? [];
let shouldInclude = false;
for (const option of accepts) {
if (!walletNetworks.includes(option.network)) {
continue;
}
if (!option.asset) {
continue;
}
// Check if this is a USDC asset
if (!isUsdcAsset(option.asset, walletProvider)) {
continue;
}
// Get the amount (supports both v1 maxAmountRequired and v2 amount/price)
const amountStr = option.maxAmountRequired ?? option.amount ?? option.price;
if (!amountStr) {
continue;
}
try {
const maxUsdcPriceAtomic = await convertWholeUnitsToAtomic(maxUsdcPrice, option.asset, walletProvider);
if (maxUsdcPriceAtomic) {
const resourceAmount = BigInt(amountStr);
const maxAmount = BigInt(maxUsdcPriceAtomic);
if (resourceAmount <= maxAmount) {
shouldInclude = true;
break;
}
}
}
catch {
// Skip if conversion fails
continue;
}
}
if (shouldInclude) {
filtered.push(resource);
}
}
return filtered;
}
/**
* Formats resources into simplified output for LLM consumption.
*
* @param resources - Array of discovery resources
* @param walletNetworks - Array of network identifiers to match for price extraction
* @param walletProvider - Wallet provider for formatting
* @returns Array of simplified resources with url, price, description
*/
async function formatSimplifiedResources(resources, walletNetworks, walletProvider) {
const simplified = [];
for (const resource of resources) {
const accepts = resource.accepts ?? [];
const matchingOption = accepts.find(opt => walletNetworks.includes(opt.network));
if (!matchingOption) {
continue;
}
// Extract URL: v1 and v2 both use resource.resource, but v2 docs show resource.url
const url = resource.resource ?? resource.url ?? "";
// Extract description (version-aware via helper)
const description = getResourceDescription(resource);
let price = "Unknown";
// Get the amount (supports both v1 and v2 formats)
const amountStr = matchingOption.maxAmountRequired ?? matchingOption.amount ?? matchingOption.price;
if (amountStr && matchingOption.asset) {
price = await formatPaymentOption({
asset: matchingOption.asset,
maxAmountRequired: amountStr,
network: matchingOption.network,
}, walletProvider);
}
simplified.push({
url,
price,
description,
});
}
return simplified;
}
/**
* Helper method to handle HTTP errors consistently.
*
* @param error - The error to handle
* @param url - The URL that was being accessed when the error occurred
* @returns A JSON string containing formatted error details
*/
function handleHttpError(error, url) {
if (error instanceof Response) {
return JSON.stringify({
error: true,
message: `HTTP ${error.status} error when accessing ${url}`,
details: error.statusText,
suggestion: "Check if the URL is correct and the API is available.",
}, null, 2);
}
if (error instanceof TypeError && error.message.includes("fetch")) {
return JSON.stringify({
error: true,
message: `Network error when accessing ${url}`,
details: error.message,
suggestion: "Check your internet connection and verify the API endpoint is accessible.",
}, null, 2);
}
const message = error instanceof Error ? error.message : String(error);
return JSON.stringify({
error: true,
message: `Error making request to ${url}`,
details: message,
suggestion: "Please check the request parameters and try again.",
}, null, 2);
}
/**
* Formats a payment option into a human-readable string.
*
* @param option - The payment option to format
* @param option.asset - The asset address or identifier
* @param option.maxAmountRequired - The maximum amount required for the payment
* @param option.network - The network identifier
* @param walletProvider - The wallet provider for token details lookup
* @returns A formatted string like "0.1 USDC on base"
*/
async function formatPaymentOption(option, walletProvider) {
const { asset, maxAmountRequired, network } = option;
// Check if this is an EVM network and we can use ERC20 helpers
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";
const isSvmNetwork = walletNetwork.protocolFamily === "svm";
if (isEvmNetwork && walletProvider instanceof wallet_providers_1.EvmWalletProvider) {
const networkId = walletNetwork.networkId;
const tokenSymbols = constants_1.TOKEN_ADDRESSES_BY_SYMBOLS[networkId];
if (tokenSymbols) {
for (const [symbol, address] of Object.entries(tokenSymbols)) {
if (asset.toLowerCase() === address.toLowerCase()) {
const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18;
const formattedAmount = (0, viem_1.formatUnits)(BigInt(maxAmountRequired), decimals);
return `${formattedAmount} ${symbol} on ${getNetworkId(network)}`;
}
}
}
// Fall back to getTokenDetails for unknown tokens
try {
const tokenDetails = await (0, utils_1.getTokenDetails)(walletProvider, asset);
if (tokenDetails) {
const formattedAmount = (0, viem_1.formatUnits)(BigInt(maxAmountRequired), tokenDetails.decimals);
return `${formattedAmount} ${tokenDetails.name} on ${getNetworkId(network)}`;
}
}
catch {
// If we can't get token details, fall back to raw format
}
}
if (isSvmNetwork && walletProvider instanceof wallet_providers_1.SvmWalletProvider) {
// Check if the asset is USDC on Solana networks
const networkId = walletNetwork.networkId;
const usdcAddress = constants_2.SOLANA_USDC_ADDRESSES[networkId];
if (usdcAddress && asset === usdcAddress) {
// USDC has 6 decimals on Solana
const formattedAmount = (0, viem_1.formatUnits)(BigInt(maxAmountRequired), 6);
return `${formattedAmount} USDC on ${getNetworkId(network)}`;
}
}
// Fallback to original format for non-EVM/SVM networks or when token details can't be fetched
return `${asset} ${maxAmountRequired} on ${getNetworkId(network)}`;
}
/**
* Checks if an asset is USDC on any supported network.
*
* @param asset - The asset address or identifier
* @param walletProvider - The wallet provider for network context
* @returns True if the asset is USDC, false otherwise
*/
function isUsdcAsset(asset, walletProvider) {
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";
const isSvmNetwork = walletNetwork.protocolFamily === "svm";
if (isEvmNetwork && walletProvider instanceof wallet_providers_1.EvmWalletProvider) {
const networkId = walletNetwork.networkId;
const tokenSymbols = constants_1.TOKEN_ADDRESSES_BY_SYMBOLS[networkId];
if (tokenSymbols && tokenSymbols.USDC) {
return asset.toLowerCase() === tokenSymbols.USDC.toLowerCase();
}
}
if (isSvmNetwork && walletProvider instanceof wallet_providers_1.SvmWalletProvider) {
const networkId = walletNetwork.networkId;
const usdcAddress = constants_2.SOLANA_USDC_ADDRESSES[networkId];
if (usdcAddress) {
return asset === usdcAddress;
}
}
return false;
}
/**
* Converts whole units to atomic units for a given asset.
*
* @param wholeUnits - The amount in whole units (e.g., 0.1 for 0.1 USDC)
* @param asset - The asset address or identifier
* @param walletProvider - The wallet provider for token details lookup
* @returns The amount in atomic units as a string, or null if conversion fails
*/
async function convertWholeUnitsToAtomic(wholeUnits, asset, walletProvider) {
// Check if this is an EVM network and we can use ERC20 helpers
const walletNetwork = walletProvider.getNetwork();
const isEvmNetwork = walletNetwork.protocolFamily === "evm";
const isSvmNetwork = walletNetwork.protocolFamily === "svm";
if (isEvmNetwork && walletProvider instanceof wallet_providers_1.EvmWalletProvider) {
const networkId = walletNetwork.networkId;
const tokenSymbols = constants_1.TOKEN_ADDRESSES_BY_SYMBOLS[networkId];
if (tokenSymbols) {
for (const [symbol, address] of Object.entries(tokenSymbols)) {
if (asset.toLowerCase() === address.toLowerCase()) {
const decimals = symbol === "USDC" || symbol === "EURC" ? 6 : 18;
return (0, viem_1.parseUnits)(wholeUnits.toString(), decimals).toString();
}
}
}
// Fall back to getTokenDetails for unknown tokens
try {
const tokenDetails = await (0, utils_1.getTokenDetails)(walletProvider, asset);
if (tokenDetails) {
return (0, viem_1.parseUnits)(wholeUnits.toString(), tokenDetails.decimals).toString();
}
}
catch {
// If we can't get token details, fall back to assuming 18 decimals
}
}
if (isSvmNetwork && walletProvider instanceof wallet_providers_1.SvmWalletProvider) {
// Check if the asset is USDC on Solana networks
const networkId = walletNetwork.networkId;
const usdcAddress = constants_2.SOLANA_USDC_ADDRESSES[networkId];
if (usdcAddress && asset === usdcAddress) {
// USDC has 6 decimals on Solana
return (0, viem_1.parseUnits)(wholeUnits.toString(), 6).toString();
}
}
// Fallback to 18 decimals for unknown tokens or non-EVM/SVM networks
return (0, viem_1.parseUnits)(wholeUnits.toString(), 18).toString();
}
/**
* Builds a URL with query parameters appended.
*
* @param baseUrl - The base URL
* @param queryParams - Optional query parameters to append
* @returns URL string with query parameters
*/
function buildUrlWithParams(baseUrl, queryParams) {
if (!queryParams || Object.keys(queryParams).length === 0) {
return baseUrl;
}
const url = new URL(baseUrl);
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}