Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Wallet Discovery For Multichain API #2970 #395

Merged
merged 9 commits into from
Dec 13, 2024
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.9,
functions: 68.46,
lines: 68.69,
statements: 68.72,
},
},

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
189 changes: 189 additions & 0 deletions src/CAIP294.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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();
});
ffmcgee725 marked this conversation as resolved.
Show resolved Hide resolved
});

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
Loading