-
-
Notifications
You must be signed in to change notification settings - Fork 139
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support to EIP-6963 protocol (#423)
* feat: add support to eip6963 protocol * feat: fix PR comments * feat: fix PR comments * feat: fix unit tests * fix: the timeout logic * fix: pr comments * fix: pr comments * fix: tests
- Loading branch information
1 parent
a72a26c
commit b6c325d
Showing
8 changed files
with
220 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { SDKProvider } from '../provider/SDKProvider'; | ||
import { | ||
EIP6963EventNames, | ||
EIP6963ProviderDetail, | ||
EIP6963ProviderInfo, | ||
eip6963RequestProvider, | ||
} from './eip6963RequestProvider'; | ||
|
||
describe('eip6963RequestProvider', () => { | ||
beforeEach(() => { | ||
global.window = { | ||
addEventListener: jest.fn(), | ||
removeEventListener: jest.fn(), | ||
dispatchEvent: jest.fn(), | ||
} as any; | ||
|
||
jest.useFakeTimers(); | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('should reject after timeout', async () => { | ||
const requestProviderPromise = eip6963RequestProvider(); | ||
|
||
jest.advanceTimersByTime(501); | ||
|
||
await expect(requestProviderPromise).rejects.toThrow( | ||
'eip6963RequestProvider timed out', | ||
); | ||
}); | ||
|
||
it('should resolve with valid provider', async () => { | ||
const mockProvider: SDKProvider = {} as SDKProvider; | ||
const mockInfo: EIP6963ProviderInfo = { | ||
uuid: 'a1d2c588-106f-4d05-958d-5e7d6c57c822', | ||
name: 'MetaMask Main', | ||
icon: 'icon-path', | ||
rdns: 'io.metamask', | ||
}; | ||
const mockEventDetail: EIP6963ProviderDetail = { | ||
info: mockInfo, | ||
provider: mockProvider, | ||
}; | ||
|
||
(window.addEventListener as jest.Mock).mockImplementationOnce( | ||
(eventName, callback) => { | ||
if (eventName === EIP6963EventNames.Announce) { | ||
callback({ | ||
type: EIP6963EventNames.Announce, | ||
detail: mockEventDetail, | ||
}); | ||
} | ||
}, | ||
); | ||
|
||
const requestProviderPromise = await eip6963RequestProvider(); | ||
|
||
expect(requestProviderPromise).toBe(mockProvider); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { METAMASK_EIP_6369_PROVIDER_INFO, UUID_V4_REGEX } from '../constants'; | ||
import { SDKProvider } from '../provider/SDKProvider'; | ||
|
||
export enum EIP6963EventNames { | ||
Announce = 'eip6963:announceProvider', | ||
Request = 'eip6963:requestProvider', // eslint-disable-line @typescript-eslint/no-shadow | ||
} | ||
|
||
export interface EIP6963ProviderInfo { | ||
uuid: string; | ||
name: string; | ||
icon: string; | ||
rdns: string; | ||
} | ||
|
||
export interface EIP6963ProviderDetail { | ||
info: EIP6963ProviderInfo; | ||
provider: SDKProvider; | ||
} | ||
|
||
export type EIP6963AnnounceProviderEvent = CustomEvent & { | ||
type: EIP6963EventNames.Announce; | ||
detail: EIP6963ProviderDetail; | ||
}; | ||
|
||
export function eip6963RequestProvider(): Promise<SDKProvider> { | ||
return new Promise((resolve, reject) => { | ||
const timeoutId = setTimeout(() => { | ||
reject(new Error('eip6963RequestProvider timed out')); | ||
}, 500); | ||
|
||
window.addEventListener( | ||
EIP6963EventNames.Announce, | ||
(eip6963AnnounceProviderEvent) => { | ||
const event = | ||
eip6963AnnounceProviderEvent as EIP6963AnnounceProviderEvent; | ||
|
||
const { detail: { info, provider } = {} } = event; | ||
|
||
const { name, rdns, uuid } = info ?? {}; | ||
|
||
const isValid = | ||
UUID_V4_REGEX.test(uuid) && | ||
name === METAMASK_EIP_6369_PROVIDER_INFO.NAME && | ||
rdns === METAMASK_EIP_6369_PROVIDER_INFO.RDNS; | ||
|
||
if (isValid) { | ||
clearTimeout(timeoutId); | ||
|
||
resolve(provider); | ||
} | ||
}, | ||
); | ||
|
||
window.dispatchEvent(new Event(EIP6963EventNames.Request)); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,97 @@ | ||
import { getBrowserExtension } from './get-browser-extension'; | ||
import { eip6963RequestProvider } from './eip6963RequestProvider'; | ||
|
||
jest.mock('./eip6963RequestProvider'); | ||
|
||
describe('getBrowserExtension', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should throw an error if window is undefined', () => { | ||
it('should throw an error if window is undefined', async () => { | ||
global.window = undefined as any; | ||
|
||
expect(() => getBrowserExtension({ mustBeMetaMask: true })).toThrow( | ||
await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( | ||
'window not available', | ||
); | ||
}); | ||
|
||
it('should throw an error if ethereum is not found in window object', () => { | ||
it('should return baseProvider if eip6963RequestProvider resolves successfully', async () => { | ||
const mockProvider = { isMetaMask: true }; | ||
global.window = { ethereum: {} } as any; | ||
(eip6963RequestProvider as jest.Mock).mockResolvedValue(mockProvider); | ||
|
||
const res = await getBrowserExtension({ mustBeMetaMask: true }); | ||
|
||
expect(res).toStrictEqual(mockProvider); | ||
}); | ||
|
||
it('should throw an error if eip6963RequestProvider rejects and ethereum is not found in window object', async () => { | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = {} as any; | ||
|
||
expect(() => getBrowserExtension({ mustBeMetaMask: true })).toThrow( | ||
await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( | ||
'Ethereum not found in window object', | ||
); | ||
}); | ||
|
||
it('should throw an error if no suitable provider is found', () => { | ||
it('should throw an error if no suitable provider is found', async () => { | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = { ethereum: { providers: [] } } as any; | ||
|
||
expect(() => getBrowserExtension({ mustBeMetaMask: true })).toThrow( | ||
await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( | ||
'No suitable provider found', | ||
); | ||
}); | ||
|
||
it('should return MetaMask provider if mustBeMetaMask is true', () => { | ||
it('should return MetaMask provider if mustBeMetaMask is true', async () => { | ||
const mockProvider = { isMetaMask: true }; | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = { ethereum: { providers: [mockProvider] } } as any; | ||
|
||
expect(getBrowserExtension({ mustBeMetaMask: true })).toStrictEqual( | ||
mockProvider, | ||
); | ||
const res = await getBrowserExtension({ mustBeMetaMask: true }); | ||
|
||
expect(res).toStrictEqual(mockProvider); | ||
}); | ||
|
||
it('should return the first provider if mustBeMetaMask is false', () => { | ||
it('should return the first provider if mustBeMetaMask is false', async () => { | ||
const mockProvider = { isMetaMask: false }; | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = { ethereum: { providers: [mockProvider] } } as any; | ||
|
||
expect(getBrowserExtension({ mustBeMetaMask: false })).toStrictEqual( | ||
mockProvider, | ||
); | ||
const res = await getBrowserExtension({ mustBeMetaMask: false }); | ||
|
||
expect(res).toStrictEqual(mockProvider); | ||
}); | ||
|
||
it('should throw an error if mustBeMetaMask is true but MetaMask provider not found', () => { | ||
it('should throw an error if mustBeMetaMask is true but MetaMask provider not found', async () => { | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = { ethereum: { isMetaMask: false } } as any; | ||
|
||
expect(() => getBrowserExtension({ mustBeMetaMask: true })).toThrow( | ||
await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( | ||
'MetaMask provider not found in Ethereum', | ||
); | ||
}); | ||
|
||
it('should return ethereum object if mustBeMetaMask is false and ethereum object exists', () => { | ||
it('should return ethereum object if mustBeMetaMask is false and ethereum object exists', async () => { | ||
const ethereumObj = { isMetaMask: true }; | ||
(eip6963RequestProvider as jest.Mock).mockRejectedValue( | ||
new Error('Provider request failed'), | ||
); | ||
global.window = { ethereum: ethereumObj } as any; | ||
|
||
expect(getBrowserExtension({ mustBeMetaMask: false })).toStrictEqual( | ||
ethereumObj, | ||
); | ||
const res = await getBrowserExtension({ mustBeMetaMask: false }); | ||
|
||
expect(res).toStrictEqual(ethereumObj); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,51 @@ | ||
export function getBrowserExtension({ | ||
import { SDKProvider } from '../provider/SDKProvider'; | ||
import { eip6963RequestProvider } from './eip6963RequestProvider'; | ||
|
||
export async function getBrowserExtension({ | ||
mustBeMetaMask, | ||
}: { | ||
mustBeMetaMask: boolean; | ||
}) { | ||
}): Promise<SDKProvider> { | ||
if (typeof window === 'undefined') { | ||
throw new Error(`window not available`); | ||
} | ||
|
||
const { ethereum } = window as { ethereum: any }; | ||
|
||
if (!ethereum) { | ||
throw new Error('Ethereum not found in window object'); | ||
} | ||
try { | ||
const baseProvider = await eip6963RequestProvider(); | ||
|
||
// The `providers` field is populated when CoinBase Wallet extension is also installed | ||
// The expected object is an array of providers, the MetaMask provider is inside | ||
// See https://docs.cloud.coinbase.com/wallet-sdk/docs/injected-provider-guidance for | ||
if (Array.isArray(ethereum.providers)) { | ||
const provider = mustBeMetaMask | ||
? ethereum.providers.find((p: any) => p.isMetaMask) | ||
: ethereum.providers[0]; | ||
return baseProvider; | ||
} catch (e) { | ||
const { ethereum } = window as unknown as { | ||
ethereum: | ||
| (SDKProvider & { | ||
isMetaMask: boolean; | ||
}) | ||
| { providers: any[] }; | ||
}; | ||
|
||
if (!provider) { | ||
throw new Error('No suitable provider found'); | ||
if (!ethereum) { | ||
throw new Error('Ethereum not found in window object'); | ||
} | ||
|
||
return provider; | ||
} | ||
// The `providers` field is populated when CoinBase Wallet extension is also installed | ||
// The expected object is an array of providers, the MetaMask provider is inside | ||
// See https://docs.cloud.coinbase.com/wallet-sdk/docs/injected-provider-guidance for | ||
if ('providers' in ethereum) { | ||
if (Array.isArray(ethereum.providers)) { | ||
const provider = mustBeMetaMask | ||
? ethereum.providers.find((p: any) => p.isMetaMask) | ||
: ethereum.providers[0]; | ||
|
||
if (mustBeMetaMask && !ethereum.isMetaMask) { | ||
throw new Error('MetaMask provider not found in Ethereum'); | ||
} | ||
if (!provider) { | ||
throw new Error('No suitable provider found'); | ||
} | ||
|
||
return provider as SDKProvider; | ||
} | ||
} else if (mustBeMetaMask && !ethereum.isMetaMask) { | ||
throw new Error('MetaMask provider not found in Ethereum'); | ||
} | ||
|
||
return ethereum; | ||
return ethereum as SDKProvider; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters