@account-kit/infra
Version:
adapters for @aa-sdk/core for interacting with alchemy services
197 lines • 8.04 kB
JavaScript
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