diff --git a/docs/docs/guides/web3_providers_guide/eip6963.md b/docs/docs/guides/web3_providers_guide/eip6963.md index c6d9b676804..0965e440c9b 100644 --- a/docs/docs/guides/web3_providers_guide/eip6963.md +++ b/docs/docs/guides/web3_providers_guide/eip6963.md @@ -9,9 +9,11 @@ sidebar_label: 'EIP-6963: Multi Injected Provider Discovery' EIP-6963 proposes the "Multi Injected Provider Discovery" standard, which aims to enhance the discoverability and interaction with multiple injected Ethereum providers in a browser environment. Injected providers refer to browser extensions or other injected scripts that provide access to an Ethereum provider within the context of a web application. -Web3.js library has utility function for discovery of injected providers using `requestEIP6963Providers()` function. When `requestEIP6963Providers()` is used it returns `eip6963Providers` Map object. This Map object is in global scope so every time `requestEIP6963Providers()` function is called it will update Map object and return it. +Web3.js library has utility functions for discovery of injected providers using `requestEIP6963Providers()` and `onNewProviderDiscovered(eventDetails)`. -`eip6963Providers` Map object has provider's `UUID` as keys and `EIP6963ProviderDetail` as values. `EIP6963ProviderDetail` is: +`onNewProviderDiscovered(eventDetails)` can be used to subscribe to events of provider discovery & providers map update and `requestEIP6963Providers()` returns Promise object that resolves to `Map` object containing list of providers. For updated providers `eip6963:providersMapUpdated` event is emitted and it has updated Map object. This event can be subscribed as mentioned earlier using `onNewProviderDiscovered(eventDetails)` + +`eip6963ProvidersMap` object has provider's `UUID` as keys and `EIP6963ProviderDetail` as values. `EIP6963ProviderDetail` is: ```ts export interface EIP6963ProviderDetail { @@ -40,8 +42,15 @@ Following code snippet demonstrates usage of `requestEIP6963Providers()` functio import { Web3 } from 'web3'; -const providers = Web3.requestEIP6963Providers(); +// Following will subscribe to event that will be triggered when providers map is updated. + +Web3.onNewProviderDiscovered((provider) => { + console.log(provider.detail); // This will log the populated providers map object, provider.detail has Map of all providers yet discovered + // add logic here for updating UI of your DApp +}); +// Call the function and wait for the promise to resolve +let providers = await Web3.requestEIP6963Providers(); for (const [key, value] of providers) { console.log(value); diff --git a/packages/web3/src/providers.exports.ts b/packages/web3/src/providers.exports.ts index c0bbadc1b23..c3d8bfcba08 100644 --- a/packages/web3/src/providers.exports.ts +++ b/packages/web3/src/providers.exports.ts @@ -19,3 +19,4 @@ export { Eip1193Provider, SocketProvider } from 'web3-utils'; export * as http from 'web3-providers-http'; export * as ws from 'web3-providers-ws'; +export * from './web3_eip6963.js'; \ No newline at end of file diff --git a/packages/web3/src/web3.ts b/packages/web3/src/web3.ts index 0512110d32d..2953e67d364 100644 --- a/packages/web3/src/web3.ts +++ b/packages/web3/src/web3.ts @@ -44,7 +44,7 @@ import abi from './abi.js'; import { initAccountsForContext } from './accounts.js'; import { Web3EthInterface } from './types.js'; import { Web3PkgInfo } from './version.js'; -import { requestEIP6963Providers } from './web3_eip6963.js'; +import { onNewProviderDiscovered, requestEIP6963Providers } from './web3_eip6963.js'; export class Web3< CustomRegisteredSubscription extends { @@ -54,6 +54,7 @@ export class Web3< public static version = Web3PkgInfo.version; public static utils = utils; public static requestEIP6963Providers = requestEIP6963Providers; + public static onNewProviderDiscovered = onNewProviderDiscovered; public static modules = { Web3Eth, Iban, diff --git a/packages/web3/src/web3_eip6963.ts b/packages/web3/src/web3_eip6963.ts index 61b1a53e520..787502a4125 100644 --- a/packages/web3/src/web3_eip6963.ts +++ b/packages/web3/src/web3_eip6963.ts @@ -44,28 +44,48 @@ export interface EIP6963RequestProviderEvent extends Event { type: Eip6963EventName.eip6963requestProvider; } -export const eip6963Providers: Map = new Map(); +export const eip6963ProvidersMap: Map = new Map(); -export const requestEIP6963Providers = () => { +export const web3ProvidersMapUpdated = "web3:providersMapUpdated"; +export interface EIP6963ProvidersMapUpdateEvent extends CustomEvent { + type: string; + detail: Map; +} - if (typeof window === 'undefined') - throw new Error( - "window object not available, EIP-6963 is intended to be used within a browser" - ); +export const requestEIP6963Providers = async () => + new Promise((resolve, reject) => { + if (typeof window === 'undefined') { + reject(new Error("window object not available, EIP-6963 is intended to be used within a browser")); + } window.addEventListener( Eip6963EventName.eip6963announceProvider as any, (event: EIP6963AnnounceProviderEvent) => { - eip6963Providers.set( + eip6963ProvidersMap.set( event.detail.info.uuid, event.detail); + + const newEvent: EIP6963ProvidersMapUpdateEvent = new CustomEvent( + web3ProvidersMapUpdated, + { detail: eip6963ProvidersMap } + ); + + window.dispatchEvent(newEvent); + resolve(eip6963ProvidersMap); + } ); window.dispatchEvent(new Event(Eip6963EventName.eip6963requestProvider)); - return eip6963Providers; -} + }); +export const onNewProviderDiscovered = (callback: (providerEvent: EIP6963AnnounceProviderEvent) => void) => { + if (typeof window === 'undefined') { + throw new Error("window object not available, EIP-6963 is intended to be used within a browser"); + } + window.addEventListener(web3ProvidersMapUpdated as any, callback ); +} + diff --git a/packages/web3/test/unit/web3eip6963.test.ts b/packages/web3/test/unit/web3eip6963.test.ts index b584c91e934..1f465aba124 100644 --- a/packages/web3/test/unit/web3eip6963.test.ts +++ b/packages/web3/test/unit/web3eip6963.test.ts @@ -16,71 +16,76 @@ along with web3.js. If not, see . */ import { - EIP6963AnnounceProviderEvent, - EIP6963ProviderDetail, - Eip6963EventName, - eip6963Providers, + onNewProviderDiscovered, requestEIP6963Providers } from "../../src/web3_eip6963"; describe('requestEIP6963Providers', () => { + it('should reject with an error if window object is not available', async () => { + // Mocking window object absence + (global as any).window = undefined; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await expect(requestEIP6963Providers()).rejects.toThrow("window object not available, EIP-6963 is intended to be used within a browser"); + }); + + it('should resolve with updated providers map when events are triggered', async () => { + class CustomEventPolyfill extends Event { + public detail: any; + public constructor(eventType: string, eventInitDict: any) { + super(eventType, eventInitDict); + this.detail = eventInitDict.detail; + } + } + + (global as any).CustomEvent = CustomEventPolyfill; + + const mockProviderDetail = { + info: { uuid: 'test-uuid', name: 'Test Provider', icon: 'test-icon', rdns: 'test-rdns' }, + provider: {} // Mock provider object + }; + + const mockEvent = { + type: 'eip6963:announceProvider', + detail: mockProviderDetail + }; + + // Mock window methods + (global as any).window = { + addEventListener: jest.fn().mockImplementation( + + (_event, callback) => callback(mockEvent)), // eslint-disable-line + dispatchEvent: jest.fn() + }; + + const result = await requestEIP6963Providers(); + + expect(result).toEqual(new Map([['test-uuid', mockProviderDetail]])); + }); + + it('onNewProviderDiscovered should throw an error if window object is not available', () => { + // Mock the window object not being available + (global as any).window = undefined; + + // Expect an error to be thrown + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + onNewProviderDiscovered((_providerEvent) => {}); + }).toThrow("window object not available, EIP-6963 is intended to be used within a browser"); + + + }); + + it('onNewProviderDiscovered should add an event listener when window object is available', () => { + (global as any).window = { + addEventListener: jest.fn(), + }; + + const callback = jest.fn(); + onNewProviderDiscovered(callback); + + // Expect the callback to have been called when the event listener is added + expect(global.window.addEventListener).toHaveBeenCalledWith('web3:providersMapUpdated', callback); + }); - it('should request EIP6963 providers and store them in eip6963Providers', () => { - - const mockProviderDetail: EIP6963ProviderDetail = { - info: { - uuid: '1', - name: 'MockProvider', - icon: 'icon-path', - rdns: 'mock.rdns' - }, - - provider: {} as any - }; - - const mockAnnounceEvent: EIP6963AnnounceProviderEvent = { - type: Eip6963EventName.eip6963announceProvider, - detail: mockProviderDetail - } as any; - - // Mock the window object - (global as any).window = { - addEventListener: jest.fn(), - dispatchEvent: jest.fn() - }; - - // Call the function - requestEIP6963Providers(); - - // Validate event listener setup and event dispatch - expect((global as any).window.addEventListener) - .toHaveBeenCalledWith(Eip6963EventName.eip6963announceProvider, expect.any(Function)); - - expect((global as any).window.dispatchEvent).toHaveBeenCalled(); - - // Simulate the announce event - // Access the mock function calls for addEventListener - const addEventListenerMockCalls = (global as any).window.addEventListener.mock.calls; - - // Retrieve the first call to addEventListener and access its second argument - const eventListenerArg = addEventListenerMockCalls[0][1]; - - // Now "eventListenerArg" represents the function to be called when the event occurs - const announceEventListener = eventListenerArg; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - announceEventListener(mockAnnounceEvent); - - // Validate if the provider detail is stored in the eip6963Providers map - expect(eip6963Providers.get('1')).toEqual(mockProviderDetail); - }); - - it('should throw an error if window object is not available', () => { - // Remove the window object - delete (global as any).window; - - // Call the function and expect it to throw an error - expect(() => { - requestEIP6963Providers(); - }).toThrow("window object not available, EIP-6963 is intended to be used within a browser"); - }); }); \ No newline at end of file