Skip to content

Commit

Permalink
EIP6963 web3 provider update event (#6855)
Browse files Browse the repository at this point in the history
* added web3:providersMapUpdated event and exports

* doc updated

* updated test

* lint fix

* doc update

* added onNewProviderDiscovered

* exporting onNewProviderDiscovered via web3 as static

* unit tests of onNewProviderDiscovered

* updated doc
  • Loading branch information
jdevcs committed Mar 5, 2024
1 parent 8ed041c commit ca31f6a
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 75 deletions.
15 changes: 12 additions & 3 deletions docs/docs/guides/web3_providers_guide/eip6963.md
Expand Up @@ -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<string, EIP6963ProviderDetail>` 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 {
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions packages/web3/src/providers.exports.ts
Expand Up @@ -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';
3 changes: 2 additions & 1 deletion packages/web3/src/web3.ts
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
38 changes: 29 additions & 9 deletions packages/web3/src/web3_eip6963.ts
Expand Up @@ -44,28 +44,48 @@ export interface EIP6963RequestProviderEvent extends Event {
type: Eip6963EventName.eip6963requestProvider;
}

export const eip6963Providers: Map<string, EIP6963ProviderDetail> = new Map();
export const eip6963ProvidersMap: Map<string, EIP6963ProviderDetail> = new Map();

export const requestEIP6963Providers = () => {
export const web3ProvidersMapUpdated = "web3:providersMapUpdated";
export interface EIP6963ProvidersMapUpdateEvent extends CustomEvent {
type: string;
detail: Map<string, EIP6963ProviderDetail>;
}

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 );
}

129 changes: 67 additions & 62 deletions packages/web3/test/unit/web3eip6963.test.ts
Expand Up @@ -16,71 +16,76 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/

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");
});
});

1 comment on commit ca31f6a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: ca31f6a Previous: 6c075db Ratio
processingTx 9002 ops/sec (±5.23%) 9301 ops/sec (±4.81%) 1.03
processingContractDeploy 35246 ops/sec (±7.41%) 39129 ops/sec (±7.62%) 1.11
processingContractMethodSend 19234 ops/sec (±5.93%) 19443 ops/sec (±5.19%) 1.01
processingContractMethodCall 38787 ops/sec (±5.35%) 38971 ops/sec (±6.34%) 1.00
abiEncode 41390 ops/sec (±8.11%) 44252 ops/sec (±6.92%) 1.07
abiDecode 29306 ops/sec (±10.20%) 30419 ops/sec (±8.89%) 1.04
sign 1593 ops/sec (±4.26%) 1656 ops/sec (±4.08%) 1.04
verify 373 ops/sec (±0.37%) 373 ops/sec (±0.78%) 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.