import debug from 'debug' import EventEmitter3, { type EventEmitter } from 'eventemitter3' import { type JSONRPCError, type JSONRPCErrorResponse, type JSONRPCParams, JSONRPCServer, type JSONRPCSuccessResponse } from 'json-rpc-2.0' import { v4 as uuidv4 } from 'uuid' import { arrayChanged, cleanupAccounts } from '.' const serverLog = debug('upProvider:server') // Unique identifier for UP Provider JSONRPC messages const UP_PROVIDER_JSONRPC_TYPE = 'upProvider:jsonrpc' as const interface UPClientChannelEvents { connect: (args: { chainId: `0x${string}` }) => void disconnect: () => void contextAccountsChanged: (accounts: `0x${string}`[]) => void accountsChanged: (accounts: `0x${string}`[]) => void requestAccounts: (accounts: `0x${string}`[]) => void chainChanged: (chainId: number) => void injected: (accounts: `0x${string}`[]) => void sentTransaction: (tx: { from: `0x${string}`; to: `0x${string}`; value?: bigint; error?: JSONRPCError; result?: any }) => void } /** * API for client channel, each time an iframe's UPClientProvider is allocated and connected * the UPProviderConnector will create and emit a new UPClientChannel. * The UPClientChannel will have the API to control that channel. * The configuration will default to values from the UPProviderConnector but enable will be false. */ interface UPClientChannel { readonly window: Window readonly element: HTMLIFrameElement | null readonly id: string /** * Return an array listing the events for which the emitter has registered * listeners. */ eventNames(): Array> /** * Return the listeners registered for a given event. */ listeners>(event: T): Array> /** * Return the number of listeners listening to a given event. */ listenerCount(event: EventEmitter.EventNames): number /** * Calls each of the listeners registered for a given event. */ emit>(event: T, ...args: EventEmitter.EventArgs): boolean /** * Add a listener for a given event. */ on>(event: T, fn: EventEmitter.EventListener, context?: any): this addListener>(event: T, fn: EventEmitter.EventListener, context?: any): this /** * Add a one-time listener for a given event. */ once>(event: T, fn: EventEmitter.EventListener, context?: any): this /** * Remove the listeners of a given event. */ removeListener>(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean): this off>(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean): this /** * Remove all listeners, or those of the specified event. */ removeAllListeners(event?: EventEmitter.EventNames): this /** * Resume after a delay * @param delay - delay in milliseconds */ resume(delay: number): void /** * Send message to dapp. * @param method - method name/event * @param params - parameters */ send(method: string, params: unknown[]): Promise /** * This represents the normal "eth_accounts" method list. * @param accounts - list of addresses */ setAllowedAccounts(accounts: `0x${string}`[]): Promise /** * This represents the normal "eth_accounts" method list. (setter mirrors setAllowedAccounts) */ set allowedAccounts(accounts: `0x${string}`[]) /** * This represents the normal "eth_accounts" method list. * @returns list of accounts */ get allowedAccounts(): `0x${string}`[] /** * These are extra accounts sent to each provider. In the ue.io grid, this is used to * represent the account that is the grid owner. * @param accounts - list of addresses */ setContextAccounts(contextAccounts: `0x${string}`[]): Promise /** * These are extra accounts sent to each provider. In the ue.io grid, this is used to * represent the account that is the grid owner. (setter mirrors setContextAccounts) * @param accounts - list of addresses */ set contextAccounts(contextAccounts: `0x${string}`[]) /** * These are extra accounts sent to each provider. In the ue.io grid, this is used to * represent the account that is the grid owner. * @returns list of addresses */ get contextAccounts(): `0x${string}`[] /** * ChainId * @param chainId - chain id */ setChainId(chainId: number): Promise /** * ChainId * @param chainId - chain id */ set chainId(chainId: number) /** * ChainId * @returns chain id */ get chainId(): number /** * Enable or disable the channel. * @param enable - enable or disable the channel */ setEnable(enable: boolean): Promise /** * Enable or disable the channel. * @returns is channel enabled */ get enable(): boolean /** * Enable or disable the channel. * @param enable - enable or disable the channel */ set enable(value: boolean) /** * RPC urls * @param rpcUrls - list of rpc urls (used by client provider to short circuit requests) */ setRpcUrls(rpcUrls: string[]): Promise /** * RPC urls * @param rpcUrls - list of rpc urls (used by client provider to short circuit requests) */ set rpcUrls(rpcUrls: string[]) /** * RPC urls * @returns list of rpc urls (used by client provider to short circuit requests) */ get rpcUrls(): string[] /** * Helper to setup the channel with all the necessary information. * @param enable - enable * @param accounts - accounts (allowed accounts) * @param contextAccounts - context accounts * @param chainId - chainId */ setupChannel(enable: boolean, accounts: `0x${string}`[], contextAccounts: `0x${string}`[], chainId: number): Promise /** * Show or hide the popup/modal * @param show - true to show, false to hide */ showPopup(show: boolean): Promise /** * Close the channel */ close(): void _serverChannel: MessagePort } function createUPClientChannel( serverChannel: MessagePort, window: Window, element: HTMLIFrameElement | null, id: string, server: JSONRPCServer, getter: () => boolean, setter: (value: boolean) => void ): UPClientChannel { let accounts: `0x${string}`[] = [] let contextAccounts: `0x${string}`[] = [] let chainId = 0 let rpcUrls: string[] = [] let bufferedEvents: Array<[keyof UPClientChannelEvents, unknown[]]> | undefined = [] const emitter = new EventEmitter3() const channel: UPClientChannel = { _serverChannel: serverChannel, window, element, id, eventNames: () => emitter.eventNames(), listeners: >(event: T) => emitter.listeners(event), listenerCount: (event: EventEmitter.EventNames) => emitter.listenerCount(event), emit: >(event: T, ...args: EventEmitter.EventArgs): boolean => { if (bufferedEvents) { bufferedEvents.push([event, args]) return false } return emitter.emit(event, ...args) }, on: >(event: T, fn: EventEmitter.EventListener, context?: any) => { channel.resume(100) emitter.on(event, fn, context) return channel }, addListener: >(event: T, fn: EventEmitter.EventListener, context?: any) => { channel.resume(100) emitter.addListener(event, fn, context) return channel }, once: >(event: T, fn: EventEmitter.EventListener, context?: any) => { emitter.once(event, fn, context) return channel }, removeListener: >(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean) => { emitter.removeListener(event, fn, context, once) return channel }, off: >(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean) => { emitter.off(event, fn, context, once) return channel }, removeAllListeners: (event?: EventEmitter.EventNames) => { emitter.removeAllListeners(event) return channel }, resume: (delay = 0) => { const buffered = bufferedEvents if (!buffered) { return } bufferedEvents = undefined setTimeout(() => { while (buffered.length > 0) { const val = buffered.shift() if (val) { const [event, args] = val emitter.emit(event, ...(args as any)) } } }, delay) }, send: async (method: string, params: unknown[]): Promise => { const message = { jsonrpc: '2.0', id: uuidv4(), method, params, } serverChannel.postMessage(message) }, setAllowedAccounts: async (newAccounts: `0x${string}`[]): Promise => { serverLog('allowedAccounts', newAccounts) const accountsChanged = arrayChanged(accounts, newAccounts) if (accountsChanged) { const wasEmpty = accounts.length === 0 accounts = [...newAccounts] if (getter()) { await channel.send('accountsChanged', cleanupAccounts([...accounts])) if (wasEmpty !== (accounts.length === 0)) { if (getter() && accounts.length > 0) { const hexChainId = `0x${chainId.toString(16)}` as `0x${string}` channel.emit('connect', { chainId: hexChainId }) channel.send('connect', [{ chainId: hexChainId }]) } else { channel.emit('disconnect') channel.send('disconnect', []) } } } } }, set allowedAccounts(newAccounts: `0x${string}`[]) { channel.setAllowedAccounts(newAccounts) }, get allowedAccounts(): `0x${string}`[] { return [...accounts] }, setContextAccounts: async (newContextAccounts: `0x${string}`[]): Promise => { const accountsChanged = arrayChanged(contextAccounts, newContextAccounts) if (accountsChanged) { serverLog('contextAccounts', newContextAccounts) contextAccounts = [...newContextAccounts] await channel.send('contextAccountsChanged', cleanupAccounts([...contextAccounts])) } }, set contextAccounts(newContextAccounts: `0x${string}`[]) { channel.setContextAccounts(newContextAccounts) }, get contextAccounts(): `0x${string}`[] { return [...contextAccounts] }, setupChannel: async (enable: boolean, newAccounts: `0x${string}`[], newContextAccounts: `0x${string}`[], newChainId: number): Promise => { const accountsChanged = arrayChanged(accounts, newAccounts) let sendAccountsChanged = false if (accountsChanged) { serverLog('allowedAccounts', newAccounts) accounts = [...newAccounts] sendAccountsChanged = enable } const contextAccountsChanged = arrayChanged(contextAccounts, newContextAccounts) let sendContextAccountsChanged = false if (contextAccountsChanged) { serverLog('contextAccounts', newContextAccounts) contextAccounts = [...newContextAccounts] sendContextAccountsChanged = true } let sendChainChanged = false if (chainId !== newChainId) { serverLog('chainId', newChainId) chainId = newChainId sendChainChanged = true } if (enable !== channel.enable) { serverLog('enable', enable) setter(enable) sendAccountsChanged = true } if (sendChainChanged) { await channel.send('chainChanged', [newChainId]) channel.emit('chainChanged', newChainId) } if (sendContextAccountsChanged) { await channel.send('contextAccountsChanged', cleanupAccounts([...contextAccounts])) } if (sendAccountsChanged) { await channel.send('accountsChanged', cleanupAccounts(getter() ? [...accounts] : [])) if (getter() && accounts.length > 0) { const hexChainId = `0x${chainId.toString(16)}` as `0x${string}` channel.emit('connect', { chainId: hexChainId }) channel.send('connect', [{ chainId: hexChainId }]) } else { channel.emit('disconnect') channel.send('disconnect', []) } } }, setEnable: async (value: boolean): Promise => { if (value !== channel.enable) { setter(value) channel.send('accountsChanged', cleanupAccounts(getter() ? [...accounts] : [])) if (getter() && accounts.length > 0) { const hexChainId = `0x${chainId.toString(16)}` as `0x${string}` channel.emit('connect', { chainId: hexChainId }) channel.send('connect', [{ chainId: hexChainId }]) } else { channel.emit('disconnect') channel.send('disconnect', []) } } }, set enable(value: boolean) { channel.setEnable(value) }, get enable(): boolean { return getter() }, setChainId: async (newChainId: number): Promise => { if (chainId !== newChainId) { chainId = newChainId await channel.send('chainChanged', [newChainId]) channel.emit('chainChanged', newChainId) } }, get chainId(): number { return chainId }, set chainId(newChainId: number) { channel.setChainId(newChainId) }, setRpcUrls: async (newRpcUrls: string[]): Promise => { if (arrayChanged(newRpcUrls, rpcUrls)) { rpcUrls = newRpcUrls await channel.send('rpcUrlsChanged', newRpcUrls) } }, get rpcUrls(): string[] { return [...rpcUrls] }, set rpcUrls(newRpcUrls: string[]) { channel.setRpcUrls(newRpcUrls) }, showPopup: async (show: boolean): Promise => { serverLog('showPopup requested:', show) await channel.send('showPopup', [show]) }, close: () => { const el: any = element || window try { if (el.upChannel === channel) { el.upChannel = undefined } } catch { // ignore } serverChannel.close() } } return channel } interface UPProviderEndpointEvents { accountsChanged: (accounts: `0x${string}`[]) => void chainChanged: (chainId: number) => void connect: ({ chainId }: { chainId: number }) => void disconnect: (error: Error) => void } interface UPProviderEndpoint { on>(event: T, fn: EventEmitter.EventListener, context?: any): this off>(event: T, fn: EventEmitter.EventListener, context?: any): this request(message: { method: string; params: JSONRPCParams }, clientParams?: any): Promise request(method: string | { method: string; params: JSONRPCParams }, params?: JSONRPCParams, clientParams?: any): Promise } type UPProviderConnectorOptions = { providerHandler?: (e: MessageEvent) => void allowedAccounts: `0x${string}`[] contextAccounts: `0x${string}`[] provider: UPProviderEndpoint providerAccountsChangedCallback?: (accounts: `0x${string}`[]) => void promise: Promise rpcUrls: string[] chainId: number } interface UPProviderConnectorEvents { channelCreated: (id: HTMLIFrameElement | Window | string, channel: UPClientChannel) => void } /** * API for provider connector */ interface UPProviderConnector { /** * Return an array listing the events for which the emitter has registered * listeners. */ eventNames(): Array> /** * Return the listeners registered for a given event. */ listeners>(event: T): Array> /** * Return the number of listeners listening to a given event. */ listenerCount(event: EventEmitter.EventNames): number /** * Calls each of the listeners registered for a given event. */ emit>(event: T, ...args: EventEmitter.EventArgs): boolean /** * Add a listener for a given event. */ on>(event: T, fn: EventEmitter.EventListener, context?: any): this addListener>(event: T, fn: EventEmitter.EventListener, context?: any): this /** * Add a one-time listener for a given event. */ once>(event: T, fn: EventEmitter.EventListener, context?: any): this /** * Remove the listeners of a given event. */ removeListener>(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean): this off>(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean): this /** * Remove all listeners, or those of the specified event. */ removeAllListeners(event?: EventEmitter.EventNames): this close(): void get provider(): UPProviderEndpoint /** * Get a map of all clients by their ID. */ get channels(): Map /** * Find the client for the element, window or proxy object of the client. * @param id * @returns actual UPClientChannel */ getChannel(id: string | Window | HTMLIFrameElement | UPClientChannel | null): UPClientChannel | null /** * Inject additional addresses into the client's accountsChanged event. * Account[0] will be linked to the signed when making transactions. * Starting at Account[1] is where additional addresses are injected. * This routine injects on all connections. You can also inject using * the channel's allowedAccounts method. * @param page list of addresses */ setContextAccounts(accounts: `0x${string}`[]): Promise set contextAccounts(accounts: `0x${string}`[]) get contextAccounts(): `0x${string}`[] setAllowedAccounts(accounts: `0x${string}`[]): Promise set allowedAccounts(accounts: `0x${string}`[]) get allowedAccounts(): `0x${string}`[] setChainId(chainId: number): Promise set chainId(chainId: number) get chainId(): number /** * Connect this provider externally. This will be called during initial construction * but can be called at a later time if desired to re-initialize or tear down * the connection. * @param provider * @param rpcUrls */ setupProvider(provider: UPProviderEndpoint, rpcUrls: string | string[]): Promise } function _createUPProviderConnector( channels: Map, options: UPProviderConnectorOptions ): UPProviderConnector { const emitter = new EventEmitter3() const getChannels = (): Map => channels const connector: UPProviderConnector = { eventNames: () => emitter.eventNames(), listeners: >(event: T) => emitter.listeners(event), listenerCount: (event: EventEmitter.EventNames) => emitter.listenerCount(event), emit: >(event: T, ...args: EventEmitter.EventArgs): boolean => { return emitter.emit(event, ...args) }, on: >(event: T, fn: EventEmitter.EventListener, context?: any) => { emitter.on(event, fn, context) return connector }, addListener: >(event: T, fn: EventEmitter.EventListener, context?: any) => { emitter.addListener(event, fn, context) return connector }, once: >(event: T, fn: EventEmitter.EventListener, context?: any) => { emitter.once(event, fn, context) return connector }, removeListener: >(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean) => { emitter.removeListener(event, fn, context, once) return connector }, off: >(event: T, fn?: EventEmitter.EventListener, context?: any, once?: boolean) => { emitter.off(event, fn, context, once) return connector }, removeAllListeners: (event?: EventEmitter.EventNames) => { emitter.removeAllListeners(event) return connector }, close: () => { if (options.providerHandler) { window.removeEventListener('message', options.providerHandler as any) options.providerHandler = undefined } }, get provider(): UPProviderEndpoint { return options.provider }, setAllowedAccounts: async (accounts: `0x${string}`[]): Promise => { const allowedAccountsChanged = arrayChanged(options.allowedAccounts, accounts) if (allowedAccountsChanged) { options.allowedAccounts = [...accounts] for (const item of connector.channels.values()) { await item.setAllowedAccounts(item.enable ? cleanupAccounts(options.allowedAccounts) : []) } } }, get allowedAccounts(): `0x${string}`[] { return cleanupAccounts(options.allowedAccounts) }, set allowedAccounts(accounts: `0x${string}`[]) { connector.setAllowedAccounts(accounts) }, setChainId: async (chainId: number): Promise => { if (options.chainId !== chainId) { options.chainId = chainId for (const item of connector.channels.values()) { await item.setChainId(options.chainId) } } }, get chainId(): number { return options.chainId }, set chainId(chainId: number) { connector.setChainId(chainId) }, get channels(): Map { return new Map(getChannels()) }, getChannel: (id: string | Window | HTMLIFrameElement | UPClientChannel | null): UPClientChannel | null => { let _id = id if (typeof _id === 'string') { return getChannels().get(_id) || null } // Special handling when running inside an iframe and looking up parent window // Do this BEFORE any property access that might fail on cross-origin objects if (window.parent !== window && _id === window.parent) { serverLog('getChannel: Looking up parent window channel') // Check if we already have a channel for the parent window for (const item of getChannels().values()) { // For parent window, we can't compare directly due to cross-origin // Instead, check if this is our special parent window channel if (item.window === window.parent) { return item } } // No existing channel for parent window return null } if (window.opener !== window && _id === window.opener) { serverLog('getChannel: Looking up parent window channel') // Check if we already have a channel for the parent window for (const item of getChannels().values()) { // For parent window, we can't compare directly due to cross-origin // Instead, check if this is our special parent window channel if (item.window === window.opener) { return item } } // No existing channel for parent window return null } // Now safe to check if it's a UPClientChannel (only for non-parent windows) if ('element' in (_id as any) || 'window' in (_id as any)) { _id = (_id as UPClientChannel).element || (_id as UPClientChannel).window } for (const item of getChannels().values()) { if (item.window === _id || item.element === _id) { return item } } return null }, setContextAccounts: async (contextAccounts: `0x${string}`[]) => { const contextAccountsChanged = arrayChanged(options.contextAccounts, contextAccounts) if (contextAccountsChanged) { options.contextAccounts = [...contextAccounts] for (const item of connector.channels.values()) { await item.setContextAccounts(cleanupAccounts(options.contextAccounts)) } } }, get contextAccounts(): `0x${string}`[] { return cleanupAccounts(options.contextAccounts) }, set contextAccounts(contextAccounts: `0x${string}`[]) { connector.setContextAccounts(contextAccounts) }, setupProvider: async (provider: any, rpcUrls: string | string[]): Promise => { // Create a new promise that will be awaited by new connections const previousPromise = options.promise options.promise = new Promise((resolve, reject) => { ;(async () => { try { // Wait for any previous initialization to complete await previousPromise const oldCallback = options.providerAccountsChangedCallback if (options.provider && oldCallback && typeof options.provider.off === 'function') { options.provider.off('accountsChanged', oldCallback) options.providerAccountsChangedCallback = undefined } options.provider = provider const newRpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls] if (arrayChanged(newRpcUrls, options.rpcUrls)) { options.rpcUrls = newRpcUrls for (const item of connector.channels.values()) { await item.setRpcUrls(options.rpcUrls) } } const _chainId = Number( (await options.provider?.request({ method: 'eth_chainId', params: [], })) || options.chainId ) if (_chainId !== options.chainId) { options.chainId = _chainId for (const item of connector.channels.values()) { await item.setChainId(options.chainId) } } const _accounts = (await options.provider?.request({ method: 'eth_accounts', params: [], })) || [] const accountsChanged = arrayChanged(options.allowedAccounts, _accounts) if (accountsChanged) { options.allowedAccounts = [..._accounts] for (const item of connector.channels.values()) { await item.setAllowedAccounts(cleanupAccounts(options.allowedAccounts)) } } const accountsChangedCallback = async (_accounts: `0x${string}`[]) => { const accountsChanged = arrayChanged(options.allowedAccounts, _accounts) if (accountsChanged) { options.allowedAccounts = [..._accounts] for (const item of connector.channels.values()) { await item.setAllowedAccounts(cleanupAccounts(options.allowedAccounts)) } } } if (options.provider && typeof options.provider.on === 'function') { options.providerAccountsChangedCallback = accountsChangedCallback options.provider.on('accountsChanged', accountsChangedCallback) } resolve() } catch (err) { reject(err) } })() }) } } return connector } let globalUPProvider: UPProviderConnector | null = null /** * Global method to find channel in case `up-channel-connected` event was missed. * * @param id how to find the UPClientChannel instance (this can be the id, frame (not the frame's element id) or window) * @returns UPClientChannel */ function getUPProviderChannel(id: string | Window | HTMLIFrameElement | UPClientChannel | null): UPClientChannel | null { if (id == null) { return null } if (!globalUPProvider) { throw new Error('Global UP Provider not set up') } return globalUPProvider.getChannel(id) } /** * Install a global UPProvider inside of the particular window which will listen for client * connections and establish them. It will fire `up-channel-connected` on the particular iframe if it's reachable. * It will fire a local `channelCreated` event as well. * * @param provider the initial provider to proxy * @param rpcUrls rpc urls to give to the clients to locally connect for non eth_sendTransaction and so on. * @returns The global provider and event sing for `channelCreated` events. */ function createUPProviderConnector(provider?: any, rpcUrls?: string | string[]): UPProviderConnector { if (globalUPProvider) { return globalUPProvider } const channels = new Map() // Allow for late initialization of class properties. const options: UPProviderConnectorOptions = { provider: provider ?? null, rpcUrls: Array.isArray(rpcUrls) ? rpcUrls : rpcUrls != null ? [rpcUrls] : [], chainId: 0, allowedAccounts: [], contextAccounts: [], promise: Promise.resolve(), } globalUPProvider = _createUPProviderConnector(channels, options) serverLog('server listen', window) // Server handler to accept new client provider connections options.providerHandler = (event: MessageEvent) => { // Handle wrapped JSONRPC messages for cross-origin scenarios (when no channel exists yet) if (event.data?.type === UP_PROVIDER_JSONRPC_TYPE) { // Find the channel for this source for (const [channelId, channel] of channels) { if (channel.window === event.source) { // Forward to the channel's handler const serverChannel = (channel as any)._serverChannel if (serverChannel) { // Unwrap and send through the channel serverChannel.postMessage(event.data.payload) } return } } // If no channel found, ignore (might be from another wallet) return } // Handle both regular provider requests and iframe-specific requests if (event.data === 'upProvider:hasProvider' || event.data === 'upProvider:requestIframeProvider') { // If we're running inside an iframe, only respond to iframe-specific requests const isInIframe = window.parent !== window if (isInIframe && event.data === 'upProvider:hasProvider') { serverLog('Ignoring regular provider request while in iframe') return } let iframe: HTMLIFrameElement | null = null // Log all iframes found const allIframes = document.querySelectorAll('iframe') serverLog('Found iframes:', allIframes.length) // Try to find the iframe that sent this message // For cross-origin iframes, we can't access contentWindow directly for (const element of document.querySelectorAll('iframe')) { try { if (element.contentWindow === event.source) { serverLog('server hasProvider - matched iframe', element) iframe = element break } } catch (e) { // Cross-origin access denied - this is expected for cross-origin iframes // We'll handle this case below by using the Window reference directly serverLog('Cross-origin iframe detected, cannot access contentWindow for', element.src) } } if (!iframe) { serverLog('No matching iframe found, using Window reference directly') } const previous = iframe ? getUPProviderChannel(iframe) : getUPProviderChannel(event.source as Window) let channelId: string let serverChannel = event.ports?.[0] const server = new JSONRPCServer() // Enable by default for iframe/popup mode (when requestIframeProvider is used) let enabled = event.data === 'upProvider:requestIframeProvider' // If no port was provided (cross-origin case), create our own MessageChannel let createdChannel: MessageChannel | undefined if (!serverChannel) { serverLog('No port received, creating MessageChannel in server') createdChannel = new MessageChannel() serverChannel = createdChannel.port1 } // Server handler to forward requests to the provider if (previous) { channelId = previous.id } else { channelId = uuidv4() } // Wrapper for representation of client connection inside of global provider space. const channel_ = createUPClientChannel( serverChannel, event.source as Window, iframe, channelId, server, () => enabled, (value: boolean) => { enabled = value } ) server.applyMiddleware(async (_, request) => { await options.promise const { method: _method, params: _params, id, jsonrpc } = request const method = typeof _method === 'string' ? _method : ( _method as unknown as { method: string params: unknown[] } ).method const params = typeof _method === 'string' ? _params : ( _method as unknown as { method: string params: unknown[] } ).params switch (method) { case 'chainChanged': serverLog('short circuit response', request, [options.chainId]) channel_.emit('chainChanged', options.chainId) return { ...request, result: [options.chainId], } as JSONRPCSuccessResponse case 'accounts': { const accounts = cleanupAccounts(enabled ? [...channel_.allowedAccounts] : []) serverLog('short circuit response', request) channel_.emit('requestAccounts', accounts) return { ...request, result: accounts, } as JSONRPCSuccessResponse } case 'contextAccountsChanged': { const accounts = cleanupAccounts([...channel_.contextAccounts]) serverLog('short circuit response', request) channel_.emit('contextAccountsChanged', accounts) return { ...request, result: accounts, } as JSONRPCSuccessResponse } case 'wallet_switchEthereumChain': { serverLog('short circuit response', request) globalUPProvider?.setChainId(Number(params[0]?.chainId ?? options.chainId)) return { ...request, result: null, } as JSONRPCSuccessResponse } case 'eth_requestAccounts': { const accounts = cleanupAccounts(enabled ? [...channel_.allowedAccounts] : []) serverLog('short circuit response', request, accounts) channel_.emit('requestAccounts', accounts) return { ...request, result: accounts, } as JSONRPCSuccessResponse } case 'eth_chainId': return { ...request, result: `0x${options.chainId.toString(16)}`, } as JSONRPCSuccessResponse case 'eth_accounts': { const accounts = cleanupAccounts(enabled ? [...channel_.allowedAccounts] : []) channel_.emit('accountsChanged', accounts) return { ...request, result: accounts, } as JSONRPCSuccessResponse } } try { if (!options.provider) { throw new Error('Global Provider not connected') } const response = await options.provider.request({ method, params }) serverLog('response', request, response) return { id, jsonrpc, result: response, } as JSONRPCSuccessResponse } catch (error) { console.error(error) const response = { id, jsonrpc, error, } as JSONRPCErrorResponse serverLog('response error', request, response) return response } }) const channelHandler = (event: MessageEvent) => { // Handle wrapped JSONRPC messages for cross-origin scenarios if (event.data?.type === UP_PROVIDER_JSONRPC_TYPE) { const jsonrpcMessage = event.data.payload if (jsonrpcMessage) { // Process as regular JSONRPC message const mockEvent = { data: jsonrpcMessage } channelHandler(mockEvent as MessageEvent) } return } if (event.data.type === 'upProvider:windowInitialized') { serverLog('channel created', event.data.type, event.data) globalUPProvider?.emit('channelCreated', channel_.element || channel_.window || null, channel_) const destination = channel_.element || channel_.window || null if (destination != null) { let usePostMessage = false try { ;(destination as any).upChannel = channel_ } catch { // Ignore usePostMessage = true } const detail = { channel: channel_, chainId: options.chainId, allowedAccounts: [], contextAccounts: options.contextAccounts, rpcUrls: options.rpcUrls, enable: false, } serverLog('channel receipt', detail) const event = new CustomEvent('up-channel-connected', { detail, }) if (usePostMessage) { // Need to specify target origin for cross-origin communication ;(destination as Window)?.postMessage({ ...detail, channel: undefined }, '*') } else { try { destination.dispatchEvent(event) } catch {} } } return } try { const request = { ...event.data, id: `${channelId}:${event.data.id}`, } server.receive(request).then( response => { serverLog('server response', response) if (response && typeof response.id === 'string') { if (request.method === 'eth_sendTransaction') { if (response.error) { console.error('Error sending transaction', response.error) } channel_.emit('sentTransaction', { from: request.params[0]?.from, to: request.params[0]?.to, value: request.params[0]?.value, result: response.result, error: response.error, }) } // Handle wallet_requestPermissions response for popup/iframe modes // return [{ // parentCapability: 'eth_accounts', // invoker: window.location.origin, // caveats: [{ // type: 'restrictReturnedAccounts', // value: accounts // }] // }] if (request.method === 'wallet_requestPermissions' && response.result) { // Only update accounts automatically for iframe/popup modes const isIframeOrPopup = channel_.element !== null || (channel_.window && channel_.window !== window) if (isIframeOrPopup) { const permissions = response.result[0] if (permissions?.caveats?.[0]?.type === 'restrictReturnedAccounts' && Array.isArray(permissions.caveats[0].value)) { const newAccounts = permissions.caveats[0].value as `0x${string}`[] const currentAccounts = channel_.allowedAccounts // Check if accounts changed using same logic as elsewhere if ((currentAccounts?.length ?? 0) !== (newAccounts?.length ?? 0) || currentAccounts?.some((account, index) => account !== newAccounts[index])) { serverLog('wallet_requestPermissions returned new accounts, updating and sending accountsChanged', { enabled, isIframeOrPopup, newAccounts, currentAccounts, }) // Update the accounts - this will trigger accountsChanged event channel_.setAllowedAccounts(newAccounts) } } } } if (!response.id.startsWith(`${channelId}:`)) { console.error(`Invalid response id ${response.id} on channel ${channelId}`) return } // Check if we need to wrap the response for cross-origin const responseMessage = { ...response, id: JSON.parse(response.id.replace(`${channelId}:`, '')), } // Check if this is a cross-origin scenario where we can't use MessageChannel // We should use MessageChannel if we have serverChannel, regardless of window comparison const needsWrapping = !serverChannel && channel_.window if (needsWrapping && channel_.window) { // Wrap and send via window.postMessage channel_.window.postMessage( { type: UP_PROVIDER_JSONRPC_TYPE, payload: responseMessage, }, '*' ) } else { // Use MessageChannel serverLog('Sending response via MessageChannel, serverChannel exists:', !!serverChannel, 'responseMessage:', responseMessage) if (serverChannel) { serverChannel.postMessage(responseMessage) } else { console.error('serverChannel is null/undefined, cannot send response!') } } } }, (error: any) => { if (request.method === 'eth_sendTransaction') { if (error) { console.error('Error sending transaction', error) } channel_.emit('sentTransaction', { from: request.params[0]?.from, to: request.params[0]?.to, value: request.params[0]?.value, error, }) } const errorMessage = { error, id: JSON.parse(request.id.replace(`${channelId}:`, '')), } // Check if this is a cross-origin scenario where we can't use MessageChannel // We should use MessageChannel if we have serverChannel, regardless of window comparison const needsWrapping = !serverChannel && channel_.window if (needsWrapping && channel_.window) { // Wrap and send via window.postMessage channel_.window.postMessage( { type: UP_PROVIDER_JSONRPC_TYPE, payload: errorMessage, }, '*' ) } else { // Use MessageChannel serverChannel?.postMessage(errorMessage) } } ) } catch (error) { console.error('Error parsing JSON RPC request', error, event) } } channels.set(channelId, channel_) serverLog('server hasProvider', event.data, event.ports) serverChannel.addEventListener('message', channelHandler) serverChannel.start() serverLog('server accept', serverChannel) // Wait for provider to be fully initialized before sending windowInitialize options.promise.then(() => { // For iframe/popup mode (enabled=true), send the allowed accounts // For extension mode (enabled=false), send empty accounts until explicitly connected const initMessage = { type: 'upProvider:windowInitialize', chainId: options.chainId, allowedAccounts: cleanupAccounts(enabled ? options.allowedAccounts : []), contextAccounts: cleanupAccounts(options.contextAccounts), rpcUrls: options.rpcUrls, } serverLog('Sending windowInitialize after provider ready', initMessage) if (createdChannel) { // Send the port along with the init message via postMessage serverLog('Sending created port back to client') ;(event.source as Window).postMessage(initMessage, event.origin, [createdChannel.port2]) } else { // Normal flow - send via the channel serverChannel?.postMessage(initMessage) } if (enabled && options.allowedAccounts.length > 0) { channel_.emit('connect', { chainId: `0x${options.chainId.toString(16)}` }) } }) } } window.addEventListener('message', options.providerHandler) // If provider was passed, set it up after handler is registered if (provider) { globalUPProvider.setupProvider(provider, rpcUrls || []).catch(console.error) } return globalUPProvider } export { type UPClientChannel, type UPClientChannelEvents, type UPProviderConnector, type UPProviderConnectorEvents, type UPProviderEndpoint, type UPProviderEndpointEvents, getUPProviderChannel, createUPProviderConnector }