@walletconnect/universal-provider
Version:
Universal Provider for WalletConnect Protocol
1 lines • 79 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/constants/values.ts","../src/constants/events.ts","../src/utils/misc.ts","../src/utils/globals.ts","../src/utils/caip25.ts","../src/utils/storage.ts","../src/utils/eip5792.ts","../src/providers/eip155.ts","../src/providers/generic.ts","../src/UniversalProvider.ts","../src/index.ts"],"sourcesContent":["export const LOGGER = \"error\";\n\nexport const RELAY_URL = \"wss://relay.walletconnect.org\";\n\nexport const PROTOCOL = \"wc\";\nexport const WC_VERSION = 2;\nexport const CONTEXT = \"universal_provider\";\n\nexport const STORAGE = `${PROTOCOL}@${WC_VERSION}:${CONTEXT}:`;\n\nexport const RPC_URL = \"https://rpc.walletconnect.org/v1/\";\n\nexport const GENERIC_SUBPROVIDER_NAME = \"generic\";\n\nexport const BUNDLER_URL = `${RPC_URL}bundler`;\n\nexport const CALL_STATUS_STORAGE_KEY = \"call_status\";\n\nexport const CALL_STATUS_RESULT_EXPIRY = 86400; // 24 hours in seconds\n","export const PROVIDER_EVENTS = {\n DEFAULT_CHAIN_CHANGED: \"default_chain_changed\",\n};\n","import { merge } from \"es-toolkit/compat\";\nimport { SessionTypes } from \"@walletconnect/types\";\nimport {\n isCaipNamespace,\n isValidObject,\n mergeArrays,\n parseChainId,\n parseNamespaceKey,\n} from \"@walletconnect/utils\";\nimport { RPC_URL } from \"../constants/index.js\";\nimport { Namespace, NamespaceConfig } from \"../types/index.js\";\n\nexport function getRpcUrl(chainId: string, rpc: Namespace, projectId?: string): string | undefined {\n const chain = parseChainId(chainId);\n return (\n rpc.rpcMap?.[chain.reference] ||\n `${RPC_URL}?chainId=${chain.namespace}:${chain.reference}&projectId=${projectId}`\n );\n}\n\nexport function getChainId(chain: string): string {\n return chain.includes(\":\") ? chain.split(\":\")[1] : chain;\n}\n\nexport function validateChainApproval(chain: string, chains: string[]): void {\n if (!chains.includes(chain)) {\n throw new Error(\n `Chain '${chain}' not approved. Please use one of the following: ${chains.toString()}`,\n );\n }\n}\n\nexport function getChainsFromApprovedSession(accounts: string[]): string[] {\n return accounts.map((address) => `${address.split(\":\")[0]}:${address.split(\":\")[1]}`);\n}\n\nexport function getAccountsFromSession(namespace: string, session: SessionTypes.Struct): string[] {\n // match namespaces e.g. eip155 with eip155:1\n const matchedNamespaceKeys = Object.keys(session.namespaces).filter((key) =>\n key.includes(namespace),\n );\n if (!matchedNamespaceKeys.length) return [];\n const accounts: string[] = [];\n matchedNamespaceKeys.forEach((key) => {\n const accountsForNamespace = session.namespaces[key].accounts;\n accounts.push(...accountsForNamespace);\n });\n return accounts;\n}\n\nexport function filterNamespacesWithNoChains(namespaces: NamespaceConfig): NamespaceConfig {\n return Object.fromEntries(\n Object.entries(namespaces).filter(([_, ns]) => ns?.chains?.length && ns?.chains?.length > 0),\n );\n}\n\nexport function mergeRequiredOptionalNamespaces(\n required: NamespaceConfig = {},\n optional: NamespaceConfig = {},\n) {\n const requiredNamespaces = filterNamespacesWithNoChains(normalizeNamespaces(required));\n const optionalNamespaces = filterNamespacesWithNoChains(normalizeNamespaces(optional));\n return merge(requiredNamespaces, optionalNamespaces);\n}\n\n/**\n * Converts\n * {\n * \"eip155:1\": {...},\n * \"eip155:2\": {...},\n * }\n * into\n * {\n * \"eip155\": {\n * chains: [\"eip155:1\", \"eip155:2\"],\n * ...\n * }\n * }\n *\n */\nexport function normalizeNamespaces(namespaces: NamespaceConfig): NamespaceConfig {\n const normalizedNamespaces: NamespaceConfig = {};\n if (!isValidObject(namespaces)) return normalizedNamespaces;\n\n for (const [key, values] of Object.entries(namespaces)) {\n const chains = isCaipNamespace(key) ? [key] : values.chains;\n const methods = values.methods || [];\n const events = values.events || [];\n const rpcMap = values.rpcMap || {};\n const normalizedKey = parseNamespaceKey(key);\n normalizedNamespaces[normalizedKey] = {\n ...normalizedNamespaces[normalizedKey],\n ...values,\n chains: mergeArrays(chains, normalizedNamespaces[normalizedKey]?.chains),\n methods: mergeArrays(methods, normalizedNamespaces[normalizedKey]?.methods),\n events: mergeArrays(events, normalizedNamespaces[normalizedKey]?.events),\n };\n // avoid adding empty `rpcMap: {}` if there are no values for it\n if (isValidObject(rpcMap) || isValidObject(normalizedNamespaces[normalizedKey]?.rpcMap || {})) {\n normalizedNamespaces[normalizedKey].rpcMap = {\n ...rpcMap,\n ...normalizedNamespaces[normalizedKey]?.rpcMap,\n };\n }\n }\n return normalizedNamespaces;\n}\n\nexport function parseCaip10Account(caip10Account: string): string {\n return caip10Account.includes(\":\") ? caip10Account.split(\":\")[2] : caip10Account;\n}\n\n/**\n * Populates the chains array for each namespace with the chains extracted from the accounts if are otherwise missing\n */\nexport function populateNamespacesChains(\n namespaces: SessionTypes.Namespaces,\n): Record<string, SessionTypes.Namespace> {\n const parsedNamespaces: Record<string, SessionTypes.Namespace> = {};\n for (const [key, values] of Object.entries(namespaces)) {\n const methods = values.methods || [];\n const events = values.events || [];\n const accounts = values.accounts || [];\n // If the key includes a CAIP separator `:` we know it's a namespace + chainId (e.g. `eip155:1`)\n const chains = isCaipNamespace(key)\n ? [key]\n : values.chains\n ? values.chains\n : getChainsFromApprovedSession(values.accounts);\n parsedNamespaces[key] = {\n chains,\n methods,\n events,\n accounts,\n };\n }\n return parsedNamespaces;\n}\n\nexport function convertChainIdToNumber(chainId: string | number): number | string {\n if (typeof chainId === \"number\") return chainId;\n if (chainId.includes(\"0x\")) {\n return parseInt(chainId, 16);\n }\n\n chainId = chainId.includes(\":\") ? chainId.split(\":\")[1] : chainId;\n return isNaN(Number(chainId)) ? chainId : Number(chainId);\n}\n\nexport function isValidJSONObject(str: string): boolean {\n try {\n const parsed = JSON.parse(str);\n return typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed);\n } catch {\n return false;\n }\n}\n","const globals = {};\nexport const getGlobal = (key: string) => {\n return globals[key];\n};\n\nexport const setGlobal = (key: string, value: unknown) => {\n globals[key] = value;\n};\n","import { SessionTypes } from \"@walletconnect/types\";\nimport { isValidObject } from \"@walletconnect/utils\";\n\nimport { isValidJSONObject } from \"./misc.js\";\n\nconst EIP155_PREFIX = \"eip155\";\nconst CAPABILITIES_KEYS = [\n \"atomic\",\n \"flow-control\",\n \"paymasterService\",\n \"sessionKeys\",\n \"auxiliaryFunds\",\n];\n\nconst hexToDecimal = (hex?: string) => {\n return hex && hex.startsWith(\"0x\") ? BigInt(hex).toString(10) : hex;\n};\n\nconst decimalToHex = (decimal: string) => {\n return decimal && decimal.startsWith(\"0x\") ? decimal : `0x${BigInt(decimal).toString(16)}`;\n};\n\nconst getCapabilitiesFromObject = (object: Record<string, any>) => {\n const capabilitiesKeys = Object.keys(object).filter((item) => CAPABILITIES_KEYS.includes(item));\n\n return capabilitiesKeys.reduce(\n (acc, key) => {\n acc[key] = parseCapabilityValue(object[key]);\n return acc;\n },\n {} as Record<string, any>,\n );\n};\n\nconst parseCapabilityValue = (value: any) => {\n if (typeof value === \"string\" && isValidJSONObject(value)) {\n return JSON.parse(value);\n }\n return value;\n};\n\nexport const extractCapabilitiesFromSession = (\n session: SessionTypes.Struct,\n address: string,\n chainIds: string[],\n) => {\n const { sessionProperties = {}, scopedProperties = {} } = session;\n const result: Record<string, any> = {};\n\n if (!isValidObject(scopedProperties) && !isValidObject(sessionProperties)) {\n return;\n }\n\n // get all capabilities from sessionProperties as they apply to all chains/addresses\n const globalCapabilities = getCapabilitiesFromObject(sessionProperties);\n\n for (const chain of chainIds) {\n const chainId = hexToDecimal(chain);\n if (!chainId) {\n continue;\n }\n\n result[decimalToHex(chainId)] = globalCapabilities;\n\n const chainSpecific = scopedProperties?.[`${EIP155_PREFIX}:${chainId}`];\n\n if (chainSpecific) {\n const addressSpecific = chainSpecific?.[`${EIP155_PREFIX}:${chainId}:${address}`];\n\n // use the address specific capabilities if they exist, otherwise use the chain specific capabilities\n result[decimalToHex(chainId)] = {\n ...result[decimalToHex(chainId)],\n ...getCapabilitiesFromObject(addressSpecific || chainSpecific),\n };\n }\n }\n\n // remove any chains that have no capabilities\n for (const [key, value] of Object.entries(result)) {\n if (Object.keys(value).length === 0) {\n delete result[key];\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n","import { IKeyValueStorage } from \"@walletconnect/keyvaluestorage\";\n\nlet storage: Storage;\n\nexport class Storage {\n private storage: IKeyValueStorage;\n constructor(storage: IKeyValueStorage) {\n this.storage = storage;\n }\n\n async getItem<T>(key: string): Promise<T | undefined> {\n return await this.storage.getItem<T>(key);\n }\n\n async setItem<T>(key: string, value: T) {\n return await this.storage.setItem(key, value);\n }\n\n async removeItem(key: string) {\n return await this.storage.removeItem(key);\n }\n\n static getStorage(kvStorage: IKeyValueStorage) {\n if (!storage) {\n storage = new Storage(kvStorage);\n }\n return storage;\n }\n}\n","import { calcExpiry, isExpired, parseChainId } from \"@walletconnect/utils\";\nimport { formatJsonRpcRequest } from \"@walletconnect/jsonrpc-utils\";\nimport JsonRpcProvider from \"@walletconnect/jsonrpc-provider\";\n\nimport { StoredSendCalls, StoreSendCallsParams } from \"../types/index.js\";\nimport { CALL_STATUS_RESULT_EXPIRY, CALL_STATUS_STORAGE_KEY } from \"../constants/index.js\";\nimport { Storage } from \"./storage.js\";\n\nexport async function prepareCallStatusFromStoredSendCalls(\n storedSendCalls: StoredSendCalls,\n getHttpProvider: (chainId: number) => JsonRpcProvider,\n) {\n const chainId = parseChainId(storedSendCalls.result.capabilities.caip345.caip2);\n const hashes = storedSendCalls.result.capabilities.caip345.transactionHashes;\n const allPromises = await Promise.allSettled(\n hashes.map((hash) => getTransactionReceipt(chainId.reference, hash, getHttpProvider)),\n );\n const receipts = allPromises\n .filter((r) => r.status === \"fulfilled\")\n .map((r) => r.value)\n .filter((r) => r);\n\n // log failed transactions\n allPromises\n .filter((r) => r.status === \"rejected\")\n .forEach((r) => console.warn(\"Failed to fetch transaction receipt:\", r.reason));\n\n const someReceiptsPending = !receipts.length || receipts.some((r) => !r);\n const allReceiptsSuccessful = receipts.every((r) => r?.status === \"0x1\");\n const allReceiptsFailed = receipts.every((r) => r?.status === \"0x0\");\n const someReceiptsFailed = receipts.some((r) => r?.status === \"0x0\");\n\n let status;\n if (someReceiptsPending) {\n //100 = some pending\n status = 100;\n } else if (allReceiptsSuccessful) {\n // 200 = all successful\n status = 200;\n } else if (allReceiptsFailed) {\n // 500 = all failed\n status = 500;\n } else if (someReceiptsFailed) {\n // 600 = some failures\n status = 600;\n }\n\n return {\n id: storedSendCalls.result.id,\n version: storedSendCalls.request.version,\n atomic: storedSendCalls.request.atomicRequired,\n chainId: storedSendCalls.request.chainId,\n capabilities: storedSendCalls.result.capabilities,\n receipts,\n status,\n };\n}\n\nexport async function getTransactionReceipt(\n chainId: string,\n transactionHash: string,\n getHttpProvider: (chainId: number) => JsonRpcProvider,\n) {\n return await getHttpProvider(parseInt(chainId)).request(\n formatJsonRpcRequest(\"eth_getTransactionReceipt\", [transactionHash]),\n );\n}\n\nexport async function storeSendCalls({\n sendCalls,\n storage,\n}: {\n sendCalls: StoreSendCallsParams;\n storage: Storage;\n}) {\n const sendCallsStatusResults =\n await storage.getItem<Record<string, StoredSendCalls>>(CALL_STATUS_STORAGE_KEY);\n\n await storage.setItem(CALL_STATUS_STORAGE_KEY, {\n ...sendCallsStatusResults,\n [sendCalls.result.id]: {\n request: sendCalls.request,\n result: sendCalls.result,\n expiry: calcExpiry(CALL_STATUS_RESULT_EXPIRY),\n },\n });\n}\n\nexport async function deleteSendCallsResult({\n resultId,\n storage,\n}: {\n resultId: string;\n storage: Storage;\n}) {\n const sendCallsStatusResults =\n await storage.getItem<Record<string, StoredSendCalls>>(CALL_STATUS_STORAGE_KEY);\n if (!sendCallsStatusResults) return;\n\n delete sendCallsStatusResults[resultId];\n await storage.setItem(CALL_STATUS_STORAGE_KEY, sendCallsStatusResults);\n\n // delete old expired results\n for (const resultId in sendCallsStatusResults) {\n if (isExpired(sendCallsStatusResults[resultId].expiry)) {\n delete sendCallsStatusResults[resultId];\n }\n }\n await storage.setItem(CALL_STATUS_STORAGE_KEY, sendCallsStatusResults);\n}\n\nexport async function getStoredSendCalls({\n resultId,\n storage,\n}: {\n resultId: string;\n storage: Storage;\n}): Promise<StoredSendCalls | undefined> {\n const storedSendCalls =\n await storage.getItem<Record<string, StoredSendCalls>>(CALL_STATUS_STORAGE_KEY);\n\n const result = storedSendCalls?.[resultId];\n if (result && !isExpired(result.expiry)) {\n return result;\n } else {\n await deleteSendCallsResult({ resultId, storage });\n }\n\n return undefined;\n}\n","import Client from \"@walletconnect/sign-client\";\nimport { JsonRpcProvider } from \"@walletconnect/jsonrpc-provider\";\nimport { HttpConnection } from \"@walletconnect/jsonrpc-http-connection\";\nimport { EngineTypes, SessionTypes } from \"@walletconnect/types\";\nimport { formatJsonRpcRequest } from \"@walletconnect/jsonrpc-utils\";\n\nimport {\n IProvider,\n RpcProvidersMap,\n SubProviderOpts,\n RequestParams,\n SessionNamespace,\n SendCallsResult,\n} from \"../types/index.js\";\n\nimport {\n extractCapabilitiesFromSession,\n getChainId,\n getGlobal,\n getRpcUrl,\n getStoredSendCalls,\n prepareCallStatusFromStoredSendCalls,\n Storage,\n storeSendCalls,\n} from \"../utils/index.js\";\nimport EventEmitter from \"events\";\nimport { BUNDLER_URL, PROVIDER_EVENTS } from \"../constants/index.js\";\n\nclass Eip155Provider implements IProvider {\n public name = \"eip155\";\n public client: Client;\n // the active chainId on the dapp\n public chainId: number;\n public namespace: SessionNamespace;\n public httpProviders: RpcProvidersMap;\n public events: EventEmitter;\n public storage: Storage;\n\n constructor(opts: SubProviderOpts) {\n this.namespace = opts.namespace;\n this.events = getGlobal(\"events\");\n this.client = getGlobal(\"client\");\n this.httpProviders = this.createHttpProviders();\n this.chainId = parseInt(this.getDefaultChain());\n this.storage = Storage.getStorage(this.client.core.storage);\n }\n\n public async request<T = unknown>(args: RequestParams): Promise<T> {\n switch (args.request.method) {\n case \"eth_requestAccounts\":\n return this.getAccounts() as unknown as T;\n case \"eth_accounts\":\n return this.getAccounts() as unknown as T;\n case \"wallet_switchEthereumChain\": {\n return (await this.handleSwitchChain(args)) as unknown as T;\n }\n case \"eth_chainId\":\n return parseInt(this.getDefaultChain()) as unknown as T;\n case \"wallet_getCapabilities\":\n return (await this.getCapabilities(args)) as unknown as T;\n case \"wallet_getCallsStatus\":\n return (await this.getCallStatus(args)) as unknown as T;\n case \"wallet_sendCalls\":\n return (await this.sendCalls(args)) as unknown as T;\n default:\n break;\n }\n if (this.namespace.methods.includes(args.request.method)) {\n return await this.client.request(args as EngineTypes.RequestParams);\n }\n return this.getHttpProvider().request(args.request);\n }\n\n public updateNamespace(namespace: SessionTypes.Namespace) {\n this.namespace = Object.assign(this.namespace, namespace);\n }\n\n public setDefaultChain(chainId: string, rpcUrl?: string | undefined) {\n // http provider exists so just set the chainId\n if (!this.httpProviders[chainId]) {\n this.setHttpProvider(parseInt(chainId), rpcUrl);\n }\n const previous = this.chainId;\n this.chainId = parseInt(chainId);\n this.events.emit(PROVIDER_EVENTS.DEFAULT_CHAIN_CHANGED, {\n currentCaipChainId: `${this.name}:${chainId}`,\n previousCaipChainId: `${this.name}:${previous}`,\n });\n }\n\n public requestAccounts(): string[] {\n return this.getAccounts();\n }\n\n public getDefaultChain(): string {\n if (this.chainId) return this.chainId.toString();\n if (this.namespace.defaultChain) return this.namespace.defaultChain;\n\n const chainId = this.namespace.chains[0];\n if (!chainId) throw new Error(`ChainId not found`);\n\n return chainId.split(\":\")[1];\n }\n\n // ---------- Private ----------------------------------------------- //\n\n private createHttpProvider(\n chainId: number,\n rpcUrl?: string | undefined,\n ): JsonRpcProvider | undefined {\n const rpc =\n rpcUrl || getRpcUrl(`${this.name}:${chainId}`, this.namespace, this.client.core.projectId);\n if (!rpc) {\n throw new Error(`No RPC url provided for chainId: ${chainId}`);\n }\n const http = new JsonRpcProvider(new HttpConnection(rpc, getGlobal(\"disableProviderPing\")));\n return http;\n }\n\n private setHttpProvider(chainId: number, rpcUrl?: string): void {\n const http = this.createHttpProvider(chainId, rpcUrl);\n if (http) {\n this.httpProviders[chainId] = http;\n }\n }\n\n private createHttpProviders(): RpcProvidersMap {\n const http = {};\n this.namespace.chains.forEach((chain) => {\n const parsedChain = parseInt(getChainId(chain));\n http[parsedChain] = this.createHttpProvider(parsedChain, this.namespace.rpcMap?.[chain]);\n });\n return http;\n }\n\n private getAccounts(): string[] {\n const accounts = this.namespace.accounts;\n if (!accounts) {\n return [];\n }\n return [\n ...new Set(\n accounts\n // get the accounts from the active chain\n .filter((account) => account.split(\":\")[1] === this.chainId.toString())\n // remove namespace & chainId from the string\n .map((account) => account.split(\":\")[2]),\n ),\n ];\n }\n\n private getHttpProvider(chainId?: number): JsonRpcProvider {\n const chain = chainId || this.chainId;\n const http = this.httpProviders[chain];\n if (http) {\n return http;\n }\n\n this.httpProviders = {\n ...this.httpProviders,\n [chain]: this.createHttpProvider(chain),\n };\n return this.httpProviders[chain];\n }\n\n private async handleSwitchChain(args: RequestParams): Promise<any> {\n let hexChainId = args.request.params ? args.request.params[0]?.chainId : \"0x0\";\n hexChainId = hexChainId.startsWith(\"0x\") ? hexChainId : `0x${hexChainId}`;\n const parsedChainId = parseInt(hexChainId, 16);\n // if chainId is already approved, switch locally\n if (this.isChainApproved(parsedChainId)) {\n this.setDefaultChain(`${parsedChainId}`);\n } else if (this.namespace.methods.includes(\"wallet_switchEthereumChain\")) {\n // try to switch chain within the wallet\n await this.client.request({\n topic: args.topic,\n request: {\n method: args.request.method,\n params: [\n {\n chainId: hexChainId,\n },\n ],\n },\n chainId: this.namespace.chains?.[0], // Sending a previously unapproved chainId will cause namespace validation failure so we must set request chainId to the first chainId in the namespace to avoid it\n } as EngineTypes.RequestParams);\n this.setDefaultChain(`${parsedChainId}`);\n } else {\n throw new Error(\n `Failed to switch to chain 'eip155:${parsedChainId}'. The chain is not approved or the wallet does not support 'wallet_switchEthereumChain' method.`,\n );\n }\n return null;\n }\n\n private isChainApproved(chainId: number): boolean {\n return this.namespace.chains.includes(`${this.name}:${chainId}`);\n }\n\n /**\n * util method to get the capabilities for given address and chainIds from the wallet\n * 1. check if the capabilities are stored in the sessionProperties legacy way - address+chainIds for backwards compatibility\n * 2. check if the capabilities are stored in the sessionProperties\n * 3. check if the capabilities are stored in the scopedProperties\n * 4. if not, send the request to the wallet\n * 5. update the session with the capabilities so they can be retrieved later\n * 6. return the capabilities\n */\n private async getCapabilities(args: RequestParams) {\n // if capabilities are stored in the session, return them, else send the request to the wallet\n const address = args.request?.params?.[0];\n const chainIds: string[] = args.request?.params?.[1] || [];\n\n if (!address) throw new Error(\"Missing address parameter in `wallet_getCapabilities` request\");\n const session = this.client.session.get(args.topic);\n const sessionCapabilities = session?.sessionProperties?.capabilities || {};\n\n // cache key is address + chainIds to allow requests to be made to different chains\n // when no chainIds are provided, use the current active chainId to ensure\n // cache is invalidated when switching chains\n const cacheChainKey =\n chainIds.length > 0 ? chainIds.join(\",\") : `0x${this.chainId.toString(16)}`;\n const capabilitiesKey = `${address}${cacheChainKey}`;\n\n const legacyCapabilities = sessionCapabilities?.[capabilitiesKey];\n if (legacyCapabilities) {\n return legacyCapabilities;\n }\n let cachedCapabilities;\n try {\n cachedCapabilities = extractCapabilitiesFromSession(session, address, chainIds);\n } catch (error) {\n console.warn(\"Failed to extract capabilities from session\", error);\n }\n\n if (cachedCapabilities) {\n return cachedCapabilities;\n }\n\n // intentionally omit catching errors/rejection during `request` to allow the error to bubble up\n const capabilities = await this.client.request(args as EngineTypes.RequestParams);\n try {\n // update the session with the capabilities so they can be retrieved later\n await this.client.session.update(args.topic, {\n sessionProperties: {\n ...(session.sessionProperties || {}),\n capabilities: {\n ...(sessionCapabilities || {}),\n [capabilitiesKey]: capabilities,\n } as any, // by spec sessionProperties should be <string, string> but here are used as objects?\n },\n });\n } catch (error) {\n console.warn(\"Failed to update session with capabilities\", error);\n }\n return capabilities;\n }\n\n private async getCallStatus(args: RequestParams) {\n const session = this.client.session.get(args.topic);\n const bundlerName = session.sessionProperties?.bundler_name as string;\n if (bundlerName) {\n const bundlerUrl = this.getBundlerUrl(args.chainId, bundlerName);\n try {\n return await this.getUserOperationReceipt(bundlerUrl, args);\n } catch (error) {\n console.warn(\"Failed to fetch call status from bundler\", error, bundlerUrl);\n }\n }\n const customUrl = session.sessionProperties?.bundler_url as string;\n if (customUrl) {\n try {\n return await this.getUserOperationReceipt(customUrl, args);\n } catch (error) {\n console.warn(\"Failed to fetch call status from custom bundler\", error, customUrl);\n }\n }\n\n const storedSendCalls = await getStoredSendCalls({\n resultId: args.request.params?.[0] as string,\n storage: this.storage,\n });\n if (storedSendCalls) {\n try {\n return await prepareCallStatusFromStoredSendCalls(\n storedSendCalls,\n this.getHttpProvider.bind(this),\n );\n } catch (error) {\n console.warn(\"Failed to fetch call status from stored send calls\", error, storedSendCalls);\n }\n }\n\n if (this.namespace.methods.includes(args.request.method)) {\n return await this.client.request(args as EngineTypes.RequestParams);\n }\n\n throw new Error(\"Fetching call status not approved by the wallet.\");\n }\n\n private async getUserOperationReceipt(bundlerUrl: string, args: RequestParams) {\n const url = new URL(bundlerUrl);\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(\n formatJsonRpcRequest(\"eth_getUserOperationReceipt\", [args.request.params?.[0]]),\n ),\n });\n if (!response.ok) {\n throw new Error(`Failed to fetch user operation receipt - ${response.status}`);\n }\n return await response.json();\n }\n\n private getBundlerUrl(cap2ChainId: string, bundlerName: string) {\n return `${BUNDLER_URL}?projectId=${this.client.core.projectId}&chainId=${cap2ChainId}&bundler=${bundlerName}`;\n }\n\n private async sendCalls(args: RequestParams) {\n const result = await this.client.request<SendCallsResult>(args as EngineTypes.RequestParams);\n const sendCallsParams = args.request.params?.[0];\n const resultId = result?.id;\n const capabilities = result?.capabilities || {};\n const caip2 = capabilities?.caip345?.caip2;\n const transactionHashes = capabilities?.caip345?.transactionHashes;\n\n if (!resultId || !caip2 || !transactionHashes?.length) {\n return result;\n }\n\n await storeSendCalls({\n sendCalls: { request: sendCallsParams, result },\n storage: this.storage,\n });\n return result;\n }\n}\n\nexport default Eip155Provider;\n","import HttpConnection from \"@walletconnect/jsonrpc-http-connection\";\nimport { JsonRpcProvider } from \"@walletconnect/jsonrpc-provider\";\nimport Client from \"@walletconnect/sign-client\";\nimport { EngineTypes, SessionTypes } from \"@walletconnect/types\";\nimport EventEmitter from \"events\";\nimport { GENERIC_SUBPROVIDER_NAME, PROVIDER_EVENTS } from \"../constants/index.js\";\nimport {\n IProvider,\n RequestParams,\n RpcProvidersMap,\n SessionNamespace,\n SubProviderOpts,\n} from \"../types/index.js\";\nimport { getGlobal, getRpcUrl } from \"../utils/index.js\";\nimport { parseChainId } from \"@walletconnect/utils\";\n\nclass GenericProvider implements IProvider {\n public name = GENERIC_SUBPROVIDER_NAME;\n public client: Client;\n public httpProviders: RpcProvidersMap;\n public events: EventEmitter;\n public namespace: SessionNamespace;\n public chainId: string;\n\n constructor(opts: SubProviderOpts) {\n this.namespace = opts.namespace;\n this.events = getGlobal(\"events\");\n this.client = getGlobal(\"client\");\n this.chainId = this.getDefaultChain();\n this.name = this.getNamespaceName();\n this.httpProviders = this.createHttpProviders();\n }\n\n public updateNamespace(namespace: SessionTypes.Namespace) {\n this.namespace.chains = [\n ...new Set((this.namespace.chains || []).concat(namespace.chains || [])),\n ];\n this.namespace.accounts = [\n ...new Set((this.namespace.accounts || []).concat(namespace.accounts || [])),\n ];\n this.namespace.methods = [\n ...new Set((this.namespace.methods || []).concat(namespace.methods || [])),\n ];\n this.namespace.events = [\n ...new Set((this.namespace.events || []).concat(namespace.events || [])),\n ];\n this.httpProviders = this.createHttpProviders();\n }\n\n public requestAccounts(): string[] {\n return this.getAccounts();\n }\n\n public request<T = unknown>(args: RequestParams): Promise<T> {\n if (this.namespace.methods.includes(args.request.method)) {\n return this.client.request(args as EngineTypes.RequestParams);\n }\n return this.getHttpProvider(args.chainId).request(args.request);\n }\n\n public setDefaultChain(chainId: string, rpcUrl?: string | undefined) {\n // http provider exists so just set the chainId\n if (!this.httpProviders[chainId]) {\n this.setHttpProvider(chainId, rpcUrl);\n }\n const previous = this.chainId;\n this.chainId = chainId;\n this.events.emit(PROVIDER_EVENTS.DEFAULT_CHAIN_CHANGED, {\n currentCaipChainId: `${this.name}:${chainId}`,\n previousCaipChainId: `${this.name}:${previous}`,\n });\n }\n\n public getDefaultChain(): string {\n if (this.chainId) return this.chainId;\n if (this.namespace.defaultChain) return this.namespace.defaultChain;\n\n const chainId = this.namespace.chains[0];\n if (!chainId) throw new Error(`ChainId not found`);\n\n return chainId.split(\":\")[1];\n }\n\n public getNamespaceName(): string {\n const chainId = this.namespace.chains[0];\n if (!chainId) throw new Error(`ChainId not found`);\n\n return parseChainId(chainId).namespace;\n }\n\n // --------- PRIVATE --------- //\n\n private getAccounts(): string[] {\n const accounts = this.namespace.accounts;\n if (!accounts) {\n return [];\n }\n\n return [\n ...new Set(\n accounts\n // get the accounts from the active chain\n .filter((account) => account.split(\":\")[1] === this.chainId.toString())\n // remove namespace & chainId from the string\n .map((account) => account.split(\":\")[2]),\n ),\n ];\n }\n\n private createHttpProviders(): RpcProvidersMap {\n const http = {};\n this.namespace?.accounts?.forEach((account) => {\n const chain = parseChainId(account);\n const customRpcUrl = this.namespace?.rpcMap?.[`${chain.namespace}:${chain.reference}`];\n http[chain.reference] = this.createHttpProvider(account, customRpcUrl);\n });\n return http;\n }\n\n private getHttpProvider(chain: string): JsonRpcProvider {\n const chainReference = parseChainId(chain).reference;\n const http = this.httpProviders[chainReference];\n if (typeof http === \"undefined\") {\n throw new Error(`JSON-RPC provider for ${chain} not found`);\n }\n return http;\n }\n\n private setHttpProvider(chainId: string, rpcUrl?: string): void {\n const http = this.createHttpProvider(chainId, rpcUrl);\n if (http) {\n this.httpProviders[chainId] = http;\n }\n }\n\n private createHttpProvider(chainId: string, rpcUrl?: string): JsonRpcProvider | undefined {\n const rpc = rpcUrl || getRpcUrl(chainId, this.namespace, this.client.core.projectId);\n if (!rpc) {\n throw new Error(`No RPC url provided for chainId: ${chainId}`);\n }\n const http = new JsonRpcProvider(new HttpConnection(rpc, getGlobal(\"disableProviderPing\")));\n return http;\n }\n}\n\nexport default GenericProvider;\n","import { SignClient } from \"@walletconnect/sign-client\";\nimport { SessionTypes } from \"@walletconnect/types\";\nimport { JsonRpcResult } from \"@walletconnect/jsonrpc-types\";\nimport { createLogger, getSdkError, isValidArray, parseNamespaceKey } from \"@walletconnect/utils\";\nimport { Logger } from \"@walletconnect/logger\";\n\nimport {\n convertChainIdToNumber,\n getAccountsFromSession,\n getChainsFromApprovedSession,\n mergeRequiredOptionalNamespaces,\n parseCaip10Account,\n populateNamespacesChains,\n setGlobal,\n} from \"./utils/index.js\";\nimport Eip155Provider from \"./providers/eip155.js\";\nimport GenericProvider from \"./providers/generic.js\";\n\nimport {\n IUniversalProvider,\n IProvider,\n RpcProviderMap,\n ConnectParams,\n RequestArguments,\n UniversalProviderOpts,\n NamespaceConfig,\n PairingsCleanupOpts,\n ProviderAccounts,\n AuthenticateParams,\n DefaultChainChanged,\n OnChainChanged,\n EmitAccountsChangedOnChainChange,\n} from \"./types/index.js\";\n\nimport {\n RELAY_URL,\n LOGGER,\n STORAGE,\n PROVIDER_EVENTS,\n GENERIC_SUBPROVIDER_NAME,\n CONTEXT,\n} from \"./constants/index.js\";\nimport EventEmitter from \"events\";\nimport { formatJsonRpcResult } from \"@walletconnect/jsonrpc-utils\";\n\nexport class UniversalProvider implements IUniversalProvider {\n public client!: InstanceType<typeof SignClient>;\n public namespaces?: NamespaceConfig;\n public optionalNamespaces?: NamespaceConfig;\n public sessionProperties?: SessionTypes.SessionProperties;\n public scopedProperties?: SessionTypes.ScopedProperties;\n public events: EventEmitter = new EventEmitter();\n public rpcProviders: RpcProviderMap = {};\n public session?: SessionTypes.Struct;\n public providerOpts: UniversalProviderOpts;\n public logger: Logger;\n public uri: string | undefined;\n\n private disableProviderPing = false;\n private connectParams?: ConnectParams;\n\n static async init(opts: UniversalProviderOpts) {\n const provider = new UniversalProvider(opts);\n await provider.initialize();\n return provider;\n }\n\n constructor(opts: UniversalProviderOpts) {\n this.providerOpts = opts;\n this.logger = createLogger({\n logger: opts.logger ?? LOGGER,\n name: this.providerOpts.name ?? CONTEXT,\n });\n this.disableProviderPing = opts?.disableProviderPing || false;\n }\n\n public async request<T = unknown>(\n args: RequestArguments,\n chain?: string | undefined,\n expiry?: number | undefined,\n ): Promise<T> {\n const [namespace, chainId] = this.validateChain(chain);\n\n if (!this.session) {\n throw new Error(\"Please call connect() before request()\");\n }\n return (await this.getProvider(namespace).request({\n request: {\n ...args,\n },\n chainId: `${namespace}:${chainId}`,\n topic: this.session.topic,\n expiry,\n })) as T;\n }\n\n public sendAsync(\n args: RequestArguments,\n callback: (error: Error | null, response: JsonRpcResult) => void,\n chain?: string | undefined,\n expiry?: number | undefined,\n ): void {\n const id = new Date().getTime();\n this.request(args, chain, expiry)\n .then((response) => callback(null, formatJsonRpcResult(id, response)))\n .catch((error) => callback(error, undefined as any));\n }\n\n public async enable(): Promise<ProviderAccounts> {\n if (!this.client) {\n throw new Error(\"Sign Client not initialized\");\n }\n if (!this.session) {\n await this.connect({\n namespaces: this.namespaces,\n optionalNamespaces: this.optionalNamespaces,\n sessionProperties: this.sessionProperties,\n scopedProperties: this.scopedProperties,\n });\n }\n const accounts = await this.requestAccounts();\n return accounts as ProviderAccounts;\n }\n\n public async disconnect(): Promise<void> {\n if (!this.session) {\n throw new Error(\"Please call connect() before enable()\");\n }\n await this.client.disconnect({\n topic: this.session?.topic,\n reason: getSdkError(\"USER_DISCONNECTED\"),\n });\n await this.cleanup();\n }\n\n public async connect(opts: ConnectParams): Promise<SessionTypes.Struct | undefined> {\n if (!this.client) {\n throw new Error(\"Sign Client not initialized\");\n }\n this.connectParams = opts;\n this.setNamespaces(opts);\n // omit `await` to avoid delaying the pairing flow\n this.cleanupPendingPairings();\n if (opts.skipPairing) return;\n\n return await this.pair(opts.pairingTopic);\n }\n\n public async authenticate(opts: AuthenticateParams, walletUniversalLink?: string) {\n if (!this.client) {\n throw new Error(\"Sign Client not initialized\");\n }\n this.setNamespaces(opts);\n await this.cleanupPendingPairings();\n\n const { uri, response } = await this.client.authenticate(opts, walletUniversalLink);\n if (uri) {\n this.uri = uri;\n this.events.emit(\"display_uri\", uri);\n }\n const result = await response();\n this.session = result.session;\n if (this.session) {\n // assign namespaces from session if not already defined\n const approved = populateNamespacesChains(this.session.namespaces) as NamespaceConfig;\n this.namespaces = mergeRequiredOptionalNamespaces(this.namespaces, approved);\n await this.persist(\"namespaces\", this.namespaces);\n this.onConnect();\n }\n return result;\n }\n\n public on(event: any, listener: any): void {\n this.events.on(event, listener);\n }\n\n public once(event: string, listener: any): void {\n this.events.once(event, listener);\n }\n\n public removeListener(event: string, listener: any): void {\n this.events.removeListener(event, listener);\n }\n\n public off(event: string, listener: any): void {\n this.events.off(event, listener);\n }\n\n get isWalletConnect() {\n return true;\n }\n\n public async pair(pairingTopic: string | undefined): Promise<SessionTypes.Struct> {\n const { uri, approval } = await this.client.connect({\n pairingTopic,\n requiredNamespaces: this.namespaces,\n optionalNamespaces: this.optionalNamespaces,\n sessionProperties: this.sessionProperties,\n scopedProperties: this.scopedProperties,\n authentication: this.connectParams?.authentication,\n walletPay: this.connectParams?.walletPay,\n });\n\n if (uri) {\n this.uri = uri;\n this.events.emit(\"display_uri\", uri);\n }\n\n const session = await approval();\n this.session = session;\n // assign namespaces from session if not already defined\n const approved = populateNamespacesChains(session.namespaces) as NamespaceConfig;\n this.namespaces = mergeRequiredOptionalNamespaces(this.namespaces, approved);\n await this.persist(\"namespaces\", this.namespaces);\n await this.persist(\"optionalNamespaces\", this.optionalNamespaces);\n\n this.onConnect();\n return this.session;\n }\n\n public setDefaultChain(chain: string, rpcUrl?: string | undefined) {\n try {\n // ignore without active session\n if (!this.session) return;\n const [namespace, chainId] = this.validateChain(chain);\n const provider = this.getProvider(namespace);\n if (provider) {\n provider.setDefaultChain(chainId, rpcUrl);\n } else if (this.session) {\n this.logger.warn(`Provider for namespace '${namespace}' not found in setDefaultChain`);\n }\n // If session is undefined, we're in cleanup - silently ignore\n } catch (error) {\n // ignore the error if the fx is used prematurely before namespaces are set\n if (!/Please call connect/.test((error as Error).message)) throw error;\n }\n }\n\n public async cleanupPendingPairings(opts: PairingsCleanupOpts = {}): Promise<void> {\n try {\n this.logger.info(\"Cleaning up inactive pairings...\");\n const inactivePairings = this.client.pairing.getAll();\n\n if (!isValidArray(inactivePairings)) return;\n\n for (const pairing of inactivePairings) {\n if (opts.deletePairings) {\n this.client.core.expirer.set(pairing.topic, 0);\n } else {\n await this.client.core.relayer.subscriber.unsubscribe(pairing.topic);\n }\n }\n\n this.logger.info(`Inactive pairings cleared: ${inactivePairings.length}`);\n } catch (error) {\n this.logger.warn(error, \"Failed to cleanup pending pairings\");\n }\n }\n\n public abortPairingAttempt() {\n this.logger.warn(\"abortPairingAttempt is deprecated. This is now a no-op.\");\n }\n\n // ---------- Private ----------------------------------------------- //\n\n private async checkStorage() {\n this.namespaces = (await this.getFromStore(`namespaces`)) || {};\n this.optionalNamespaces = (await this.getFromStore(`optionalNamespaces`)) || {};\n if (this.session) this.createProviders();\n }\n\n private async initialize() {\n this.logger.trace(`Initialized`);\n await this.createClient();\n await this.checkStorage();\n this.registerEventListeners();\n }\n\n private async createClient() {\n this.client =\n this.providerOpts.client ||\n (await SignClient.init({\n core: this.providerOpts.core,\n logger: this.providerOpts.logger || LOGGER,\n relayUrl: this.providerOpts.relayUrl || RELAY_URL,\n projectId: this.providerOpts.projectId,\n metadata: this.providerOpts.metadata,\n storageOptions: this.providerOpts.storageOptions,\n storage: this.providerOpts.storage,\n name: this.providerOpts.name,\n customStoragePrefix: this.providerOpts.customStoragePrefix,\n telemetryEnabled: this.providerOpts.telemetryEnabled,\n }));\n\n if (this.providerOpts.session) {\n try {\n this.session = this.client.session.get(this.providerOpts.session.topic);\n } catch (error) {\n this.logger.error(error, \"Failed to get session\");\n throw new Error(\n `The provided session: ${this.providerOpts?.session?.topic} doesn't exist in the Sign client`,\n );\n }\n } else {\n const sessions = this.client.session.getAll();\n this.session = sessions[0];\n }\n this.logger.trace(`SignClient Initialized`);\n }\n\n private createProviders(): void {\n if (!this.client) {\n throw new Error(\"Sign Client not initialized\");\n }\n\n if (!this.session) {\n throw new Error(\"Session not initialized. Please call connect() before enable()\");\n }\n\n const providersToCreate = [\n ...new Set(\n Object.keys(this.session.namespaces).map((namespace) => parseNamespaceKey(namespace)),\n ),\n ];\n\n setGlobal(\"client\", this.client);\n setGlobal(\"events\", this.events);\n setGlobal(\"disableProviderPing\", this.disableProviderPing);\n\n providersToCreate.forEach((namespace) => {\n if (!this.session) return;\n const accounts = getAccountsFromSession(namespace, this.session);\n if (accounts?.length === 0) {\n return;\n }\n const approvedChains = getChainsFromApprovedSession(accounts);\n const mergedNamespaces = mergeRequiredOptionalNamespaces(\n this.namespaces,\n this.optionalNamespaces,\n );\n const combinedNamespace = {\n ...mergedNamespaces[namespace],\n accounts,\n chains: approvedChains,\n };\n switch (namespace) {\n case \"eip155\":\n this.rpcProviders[namespace] = new Eip155Provider({\n namespace: combinedNamespace,\n });\n break;\n default:\n this.rpcProviders[namespace] = new GenericProvider({\n namespace: combinedNamespace,\n });\n }\n });\n }\n\n private registerEventListeners(): void {\n if (typeof this.client === \"undefined\") {\n throw new Error(\"Sign Client is not initialized\");\n }\n\n this.client.on(\"session_ping\", (args) => {\n const { topic } = args;\n if (topic !== this.session?.topic) return;\n this.events.emit(\"session_ping\", args);\n });\n\n this.client.on(\"session_event\", (args) => {\n const { params, topic } = args;\n if (topic !== this.session?.topic) return;\n const { event } = params;\n if (event.name === \"accountsChanged\") {\n const accounts = event.data;\n if (accounts && isValidArray(accounts))\n this.events.emit(\"accountsChanged\", accounts.map(parseCaip10Account));\n } else if (event.name === \"chainChanged\") {\n const requestChainId = params.chainId;\n const payloadChainId = params.event.data as number;\n const namespace = parseNamespaceKey(requestChainId);\n // chainIds might differ between the request & payload - request is always in CAIP2 format, while payload might be string, number, CAIP2 or hex\n // take priority of the payload chainId\n const chainIdToProcess =\n convertChainIdToNumber(requestChainId) !== convertChainIdToNumber(payloadChainId)\n ? `${namespace}:${convertChainIdToNumber(payloadChainId)}`\n : requestChainId;\n\n this.onChainChanged({ currentCaipChainId: chainIdToProcess });\n } else {\n this.events.emit(event.name, event.data);\n }\n\n this.events.emit(\"session_event\", args);\n });\n\n this.client.on(\"session_update\", ({ topic, params }) => {\n if (topic !== this.session?.topic) return;\n const { namespaces } = params;\n const _session = this.client?.session.get(topic);\n this.session = { ..._session, namespaces } as SessionTypes.Struct;\n this.onSessionUpdate();\n this.events.emit(\"session_update\", { topic, params });\n });\n\n this.client.on(\"session_delete\", async (payload) => {\n if (payload.topic !== this.session?.topic) return;\n await this.cleanup();\n this.events.emit(\"session_delete\", payload);\n this.events.emit(\"disconnect\", {\n ...getSdkError(\"USER_DISCONNECTED\"),\n data: payload.topic,\n });\n });\n\n this.on(PROVIDER_EVENTS.DEFAULT_CHAIN_CHANGED, (params: DefaultChainChanged) => {\n this.onChainChanged({ ...params, internal: true });\n });\n }\n\n private getProvider(namespace: string): IProvider {\n return this.rpcProviders[namespace] || this.rpcProviders[GENERIC_SUBPROVIDER_NAME];\n }\n\n private onSessionUpdate(): void {\n Object.keys(this.rpcProviders).forEach((namespace: string) => {\n this.getProvider(namespace).updateNamespace(\n this.session?.namespaces[namespace] as SessionTypes.BaseNamespace,\n );\n });\n }\n\n private setNamespaces(params: ConnectParams): void {\n const {\n namespaces = {},\n optionalNamespaces = {},\n sessionProperties,\n scopedProperties,\n } = params;\n\n // requiredNamespaces are deprecated, assign them to optionalNamespaces\n this.optionalNamespaces = mergeRequiredOptionalNamespaces(namespaces, optionalNamespaces);\n this.sessionProperties = sessionProperties;\n this.scopedProperties = scopedProperties;\n }\n\n private validateChain(chain?: string): [string, string] {\n const [namespace, chainId] = chain?.split(\":\") || [\"\", \"\"];\n if (!this.namespaces || !Object.keys(this.namespaces).length) return [namespace, chainId];\n // validate namespace\n if (namespace) {\n if (\n // some namespaces might be defined with inline chainId e.g. eip155:1\n // and we need to parse them\n !Object.keys(this.namespaces || {})\n .map((key) => parseNamespaceKey(key))\n .includes(namespace)\n ) {\n throw new Error(\n `Namespace '${namespace}' is not configured. Please call connect() first with namespace config.`,\n );\n }\n }\n if (namespace && chainId) {\n return [namespace, chainId];\n }\n const defaultNamespace = parseNamespaceKey(Object.keys(this.namespaces)[0]);\n const defaultChain = this.rpcProviders[defaultNamespace].getDefaultChain();\n return [defaultNamespace, defaultChain];\n }\n\n private async requestAccounts(): Promise<string[]> {\n const [namespace] = this.validateChain();\n return await this.getProvider(namespace).requestAccounts();\n }\n\n private async onChainChanged({\n currentCaipChainId,\n previousCaipChainId,\n internal = false,\n }: OnChainChanged): Promise<void> {\n if (!this.namespaces) return;\n\n const [namespace, chainId] = this.validateChain(currentCaipChainId);\n\n if (!chainId) return;\n\n this.updateNamespaceChain(namespace, chainId);\n\n if (!internal) {\n const provider = this.getProvider(namespace);\n if (provider) {\n provider.setDefaultChain(chainId);\n } else if (this.session) {\n this.logger.warn(`Provider for namespace '${namespace}' not found during chain change`);\n }\n // If session is undefined, we're in cleanup - silently ignore\n } else {\n // emit the events during the `internal` cycle of chain change\n // otherwise events are emitted twice\n // once on the chainChanged event and once triggered by `this.getProvider(namespace).setDefaultChain(chainId);`\n this.events.emit(\"chainChanged\", chainId);\n this.emitAccountsChangedOnChainChange({\n namespace,\n currentCaipChainId,\n previousCaipChainId,\n });\n }\n\n await this.persist(\"namespaces\", this.namespaces);\n }\n\n /**\n * Emits `accountsChanged` event when a chain is changed and there are new accounts on the new chain\n */\n private emitAccountsChangedOnChainChange({\n namespace,\n currentCaipChainId,\n previousCaipChainId,\n }: EmitAccountsChangedOnChainChange): void {\n try {\n if (previousCaipChainId === currentCaipChainId) {\n return;\n }\n\n const accounts = this.session?.namespaces[namespace]?.accounts;\n if (!accounts) return;\n const newChainIdAccounts = accounts\n .filter((account) => account.includes(`${currentCaipChainId}:`))\n .map(parseCaip10Account);\n if (!isValidArray(newChainIdAccounts)) return;\n this.events