Skip to content

Commit

Permalink
Implement Wallet Discovery For Multichain API #2970 (#395)
Browse files Browse the repository at this point in the history
* feat: support CAIP294 (standardized messaging transport for browser extension wallets)
  • Loading branch information
ffmcgee725 authored Dec 13, 2024
1 parent 157d24f commit 53e3f81
Show file tree
Hide file tree
Showing 12 changed files with 633 additions and 26 deletions.
7 changes: 6 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ module.exports = {
},

{
files: ['EIP6963.test.ts', 'jest.setup.browser.js'],
files: [
'EIP6963.test.ts',
'CAIP294.test.ts',
'initializeInpageProvider.test.ts',
'jest.setup.browser.js',
],
rules: {
// We're mixing Node and browser environments in these files.
'no-restricted-globals': 'off',
Expand Down
9 changes: 5 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ const baseConfig = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 64.65,
functions: 65.65,
lines: 65.51,
statements: 65.61,
branches: 67.6,
functions: 69.91,
lines: 69.51,
statements: 69.52,
},
},

Expand Down Expand Up @@ -226,6 +226,7 @@ const browserConfig = {
'**/*InpageProvider.test.ts',
'**/*ExtensionProvider.test.ts',
'**/EIP6963.test.ts',
'**/CAIP294.test.ts',
],
setupFilesAfterEnv: ['./jest.setup.browser.js'],
};
Expand Down
200 changes: 200 additions & 0 deletions src/CAIP294.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
announceWallet,
CAIP294EventNames,
type CAIP294WalletData,
requestWallet,
} from './CAIP294';

const getWalletData = (): CAIP294WalletData => ({
uuid: '350670db-19fa-4704-a166-e52e178b59d2',
name: 'Example Wallet',
icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>',
rdns: 'com.example.wallet',
extensionId: 'abcdefghijklmnopqrstuvwxyz',
});

const walletDataValidationError = () =>
new Error(
`Invalid CAIP-294 WalletData object received from ${CAIP294EventNames.Prompt}. See https://github.com/ChainAgnostic/CAIPs/blob/bc4942857a8e04593ed92f7dc66653577a1c4435/CAIPs/caip-294.md for requirements.`,
);

describe('CAIP-294', () => {
describe('wallet data validation', () => {
it('throws if the wallet data is not a plain object', () => {
[null, undefined, Symbol('bar'), []].forEach((invalidInfo) => {
expect(() => announceWallet(invalidInfo as any)).toThrow(
walletDataValidationError(),
);
});
});

it('throws if the `icon` field is invalid', () => {
[
null,
undefined,
'',
'not-a-data-uri',
'https://example.com/logo.png',
'data:text/plain;blah',
Symbol('bar'),
].forEach((invalidIcon) => {
const walletInfo = getWalletData();
walletInfo.icon = invalidIcon as any;

expect(() => announceWallet(walletInfo)).toThrow(
walletDataValidationError(),
);
});
});

it('throws if the `name` field is invalid', () => {
[null, undefined, '', {}, [], Symbol('bar')].forEach((invalidName) => {
const walletInfo = getWalletData();
walletInfo.name = invalidName as any;

expect(() => announceWallet(walletInfo)).toThrow(
walletDataValidationError(),
);
});
});

it('throws if the `uuid` field is invalid', () => {
[null, undefined, '', 'foo', Symbol('bar')].forEach((invalidUuid) => {
const walletInfo = getWalletData();
walletInfo.uuid = invalidUuid as any;

expect(() => announceWallet(walletInfo)).toThrow(
walletDataValidationError(),
);
});
});

it('throws if the `rdns` field is invalid', () => {
[
null,
undefined,
'',
'not-a-valid-domain',
'..com',
'com.',
Symbol('bar'),
].forEach((invalidRdns) => {
const walletInfo = getWalletData();
walletInfo.rdns = invalidRdns as any;

expect(() => announceWallet(walletInfo)).toThrow(
walletDataValidationError(),
);
});
});

it('allows `extensionId` to be undefined or a string', () => {
const walletInfo = getWalletData();
expect(() => announceWallet(walletInfo)).not.toThrow();

delete walletInfo.extensionId;

expect(() => announceWallet(walletInfo)).not.toThrow();

walletInfo.extensionId = 'valid-string';
expect(() => announceWallet(walletInfo)).not.toThrow();
});
});

it('throws if the `extensionId` field is invalid', () => {
[null, '', 42, Symbol('bar')].forEach((invalidExtensionId) => {
const walletInfo = getWalletData();
walletInfo.extensionId = invalidExtensionId as any;

expect(() => announceWallet(walletInfo)).toThrow(
walletDataValidationError(),
);
});
});

it('wallet is announced before dapp requests', async () => {
const walletData = getWalletData();
const handleWallet = jest.fn();
const dispatchEvent = jest.spyOn(window, 'dispatchEvent');
const addEventListener = jest.spyOn(window, 'addEventListener');

announceWallet(walletData);
requestWallet(handleWallet);
await delay();

expect(dispatchEvent).toHaveBeenCalledTimes(3);
expect(dispatchEvent).toHaveBeenNthCalledWith(
1,
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
);
expect(dispatchEvent).toHaveBeenNthCalledWith(
2,
new CustomEvent(CAIP294EventNames.Prompt, expect.any(Object)),
);
expect(dispatchEvent).toHaveBeenNthCalledWith(
3,
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
);

expect(addEventListener).toHaveBeenCalledTimes(2);
expect(addEventListener).toHaveBeenCalledWith(
CAIP294EventNames.Announce,
expect.any(Function),
);
expect(addEventListener).toHaveBeenCalledWith(
CAIP294EventNames.Prompt,
expect.any(Function),
);

expect(handleWallet).toHaveBeenCalledTimes(1);
expect(handleWallet).toHaveBeenCalledWith(
expect.objectContaining({ params: walletData }),
);
});

it('dapp requests before wallet is announced', async () => {
const walletData = getWalletData();
const handleWallet = jest.fn();
const dispatchEvent = jest.spyOn(window, 'dispatchEvent');
const addEventListener = jest.spyOn(window, 'addEventListener');

requestWallet(handleWallet);
announceWallet(walletData);
await delay();

expect(dispatchEvent).toHaveBeenCalledTimes(2);
expect(dispatchEvent).toHaveBeenNthCalledWith(
1,
new CustomEvent(CAIP294EventNames.Prompt, expect.any(Object)),
);
expect(dispatchEvent).toHaveBeenNthCalledWith(
2,
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
);

expect(addEventListener).toHaveBeenCalledTimes(2);
expect(addEventListener).toHaveBeenCalledWith(
CAIP294EventNames.Announce,
expect.any(Function),
);
expect(addEventListener).toHaveBeenCalledWith(
CAIP294EventNames.Prompt,
expect.any(Function),
);

expect(handleWallet).toHaveBeenCalledTimes(1);
expect(handleWallet).toHaveBeenCalledWith(
expect.objectContaining({ params: walletData }),
);
});
});

/**
* Delay for a number of milliseconds by awaiting a promise
* resolved after the specified number of milliseconds.
*
* @param ms - The number of milliseconds to delay for.
*/
async function delay(ms = 1) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Loading

0 comments on commit 53e3f81

Please sign in to comment.