UNPKG

@account-kit/infra

Version:

adapters for @aa-sdk/core for interacting with alchemy services

197 lines 8.04 kB
import { ChainNotFoundError, ConnectionConfigSchema, split, } from "@aa-sdk/core"; import { createTransport, http, } from "viem"; import { mutateRemoveTrackingHeaders } from "./alchemyTrackerHeaders.js"; import { AlchemyChainSchema } from "./schema.js"; import { VERSION } from "./version.js"; const alchemyMethods = [ "eth_sendUserOperation", "eth_estimateUserOperationGas", "eth_getUserOperationReceipt", "eth_getUserOperationByHash", "eth_supportedEntryPoints", "rundler_maxPriorityFeePerGas", "pm_getPaymasterData", "pm_getPaymasterStubData", "alchemy_requestGasAndPaymasterAndData", ]; const chainAgnosticMethods = [ "wallet_prepareCalls", "wallet_sendPreparedCalls", "wallet_requestAccount", "wallet_createAccount", "wallet_listAccounts", "wallet_createSession", "wallet_getCallsStatus", "wallet_requestQuote_v0", ]; /** * A type guard for the transport to determine if it is an Alchemy transport. * Used in cases where we would like to do switching depending on the transport, where there used * to be two clients for an alchemy and a non-alchemy, and with this switch we don't need the two seperate clients. * * * @param {Transport} transport The transport to check * @param {Chain} chain Chain for the transport to run its function to return the transport config * @returns {boolean} `true` if the transport is an Alchemy transport, otherwise `false` */ export function isAlchemyTransport(transport, chain) { return transport({ chain }).config.type === "alchemy"; } /** * Creates an Alchemy transport with the specified configuration options. * When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. * If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl. * * @example * ### Basic Example * If the chain you're using is supported for both Bundler and Node RPCs, then you can do the following: * ```ts * import { alchemy } from "@account-kit/infra"; * * const transport = alchemy({ * // NOTE: you can also pass in an rpcUrl or jwt here or rpcUrl and jwt * apiKey: "your-api-key", * }); * ``` * * ### AA Only Chains * For AA-only chains, you need to specify the alchemyConnection and nodeRpcUrl since Alchemy only * handles the Bundler and Paymaster RPCs for these chains. * ```ts * import { alchemy } from "@account-kit/infra"; * * const transport = alchemy({ * alchemyConnection: { * apiKey: "your-api-key", * }, * nodeRpcUrl: "https://zora.rpc.url", * }); * ``` * * @param {AlchemyTransportConfig} config The configuration object for the Alchemy transport. * @param {number} config.retryDelay Optional The delay between retries, in milliseconds. * @param {number} config.retryCount Optional The number of retry attempts. * @param {string} [config.alchemyConnection] Optional Alchemy connection configuration (if this is passed in, nodeRpcUrl is required). * @param {string} [config.fetchOptions] Optional fetch options for HTTP requests. * @param {string} [config.nodeRpcUrl] Optional RPC URL for node (if this is passed in, alchemyConnection is required). * @param {string} [config.rpcUrl] Optional RPC URL. * @param {string} [config.apiKey] Optional API key for Alchemy. * @param {string} [config.jwt] Optional JSON Web Token for authorization. * @returns {AlchemyTransport} The configured Alchemy transport object. */ export function alchemy(config) { const { retryDelay, retryCount = 0 } = config; // we create a copy here in case we create a split transport down below // we don't want to add alchemy headers to 3rd party nodes const fetchOptions = { ...config.fetchOptions }; const connectionConfig = ConnectionConfigSchema.parse(config.alchemyConnection ?? config); const headersAsObject = convertHeadersToObject(fetchOptions.headers); // TODO: we probably should just log these headers during telemetry logging instead of doing this mutable header stuff fetchOptions.headers = { ...headersAsObject, "Alchemy-AA-Sdk-Version": VERSION, }; if (connectionConfig.jwt != null || connectionConfig.apiKey != null) { fetchOptions.headers = { ...fetchOptions.headers, Authorization: `Bearer ${connectionConfig.jwt ?? connectionConfig.apiKey}`, }; } const transport = (opts) => { const { chain: chain_ } = opts; if (!chain_) { throw new ChainNotFoundError(); } const chain = AlchemyChainSchema.parse(chain_); const rpcUrl = connectionConfig.rpcUrl == null ? chain.rpcUrls.alchemy.http[0] : connectionConfig.rpcUrl; const chainAgnosticRpcUrl = connectionConfig.rpcUrl == null ? "https://api.g.alchemy.com/v2" : (connectionConfig.chainAgnosticUrl ?? connectionConfig.rpcUrl); const innerTransport = (() => { mutateRemoveTrackingHeaders(config?.fetchOptions?.headers); if (config.alchemyConnection && config.nodeRpcUrl) { return split({ overrides: [ { methods: alchemyMethods, transport: http(rpcUrl, { fetchOptions, retryCount }), }, { methods: chainAgnosticMethods, transport: http(chainAgnosticRpcUrl, { fetchOptions, retryCount, retryDelay, }), }, ], fallback: http(config.nodeRpcUrl, { fetchOptions: config.fetchOptions, retryCount, retryDelay, }), }); } return split({ overrides: [ { methods: chainAgnosticMethods, transport: http(chainAgnosticRpcUrl, { fetchOptions, retryCount, retryDelay, }), }, ], fallback: http(rpcUrl, { fetchOptions, retryCount, retryDelay }), }); })(); return createTransport({ key: "alchemy", name: "Alchemy Transport", request: innerTransport({ ...opts, // Retries are already handled above within the split transport, // so `retryCount` must be 0 here for the expected behavior. retryCount: 0, }).request, // Retries are already handled above within the split transport, // so `retryCount` must be 0 here too for the expected behavior. retryCount: 0, retryDelay, type: "alchemy", }, { alchemyRpcUrl: rpcUrl, fetchOptions }); }; return Object.assign(transport, { dynamicFetchOptions: fetchOptions, updateHeaders(newHeaders_) { const newHeaders = convertHeadersToObject(newHeaders_); fetchOptions.headers = { ...fetchOptions.headers, ...newHeaders, }; }, config, }); } export const convertHeadersToObject = (headers) => { if (!headers) { return {}; } if (headers instanceof Headers) { const headersObject = {}; headers.forEach((value, key) => { headersObject[key] = value; }); return headersObject; } if (Array.isArray(headers)) { return headers.reduce((acc, header) => { acc[header[0]] = header[1]; return acc; }, {}); } return headers; }; //# sourceMappingURL=alchemyTransport.js.map