UNPKG

@account-kit/infra

Version:

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

289 lines (262 loc) 8.94 kB
import { ChainNotFoundError, ConnectionConfigSchema, split, type ConnectionConfig, type NoUndefined, } from "@aa-sdk/core"; import { createTransport, http, type Chain, type EIP1193RequestFn, type HttpTransportConfig, type PublicRpcSchema, type Transport, type TransportConfig, } from "viem"; import { mutateRemoveTrackingHeaders } from "./alchemyTrackerHeaders.js"; import type { AlchemyRpcSchema } from "./client/types.js"; import { AlchemyChainSchema } from "./schema.js"; import { VERSION } from "./version.js"; type Never<T> = T extends object ? { [K in keyof T]?: never; } : never; type AlchemyConnectionConfig = ConnectionConfig; type SplitTransportConfig = { alchemyConnection: AlchemyConnectionConfig; nodeRpcUrl: string; }; 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", ]; export type AlchemyTransportConfig = ( | (AlchemyConnectionConfig & Never<SplitTransportConfig>) | (SplitTransportConfig & Never<AlchemyConnectionConfig>) ) & { /** The max number of times to retry. */ retryCount?: TransportConfig["retryCount"] | undefined; /** The base delay (in ms) between retries. */ retryDelay?: TransportConfig["retryDelay"] | undefined; fetchOptions?: NoUndefined<HttpTransportConfig["fetchOptions"]>; }; type AlchemyTransportBase = Transport< "alchemy", { alchemyRpcUrl: string; fetchOptions?: AlchemyTransportConfig["fetchOptions"]; }, EIP1193RequestFn<[...PublicRpcSchema, ...AlchemyRpcSchema]> >; export type AlchemyTransport = AlchemyTransportBase & { updateHeaders(newHeaders: HeadersInit): void; config: AlchemyTransportConfig; dynamicFetchOptions: AlchemyTransportConfig["fetchOptions"]; }; /** * 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: Transport, chain: Chain, ): transport is AlchemyTransport { 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: AlchemyTransportConfig): AlchemyTransport { 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: AlchemyTransportBase = (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_: HeadersInit) { const newHeaders = convertHeadersToObject(newHeaders_); fetchOptions.headers = { ...fetchOptions.headers, ...newHeaders, }; }, config, }); } export const convertHeadersToObject = ( headers?: HeadersInit, ): Record<string, string> => { if (!headers) { return {}; } if (headers instanceof Headers) { const headersObject = {} as Record<string, string>; 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; }, {} as Record<string, string>, ); } return headers; };