packages/extension-base/src/background/handlers/Extension.spec.ts
// Copyright 2019-2024 @polkadot/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals" />
/* global chrome */
import '@polkadot/extension-mocks/chrome';
import type { AuthUrls, ResponseSigning } from '@polkadot/extension-base/background/types';
import type { MetadataDef } from '@polkadot/extension-inject/types';
import type { KeyringPair } from '@polkadot/keyring/types';
import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
import type { SignerPayloadJSON } from '@polkadot/types/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import { TypeRegistry } from '@polkadot/types';
import keyring from '@polkadot/ui-keyring';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { AccountsStore } from '../../stores/index.js';
import Extension from './Extension.js';
import State from './State.js';
import Tabs from './Tabs.js';
describe('Extension', () => {
let extension: Extension;
let state: State;
let tabs: Tabs;
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const password = 'passw0rd';
const address = '5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX';
async function createExtension (): Promise<Extension> {
try {
await cryptoWaitReady();
keyring.loadAll({ store: new AccountsStore() });
const authUrls: AuthUrls = {};
authUrls['localhost:3000'] = {
authorizedAccounts: [address],
count: 0,
id: '11',
origin: 'example.com',
url: 'http://localhost:3000'
};
localStorage.setItem('authUrls', JSON.stringify(authUrls));
state = new State();
tabs = new Tabs(state);
return new Extension(state);
} catch (e) {
console.error(e);
throw e;
}
}
const createAccount = async (type?: KeypairType): Promise<string> => {
await extension.handle('id', 'pri(accounts.create.suri)', type && type === 'ethereum'
? {
name: 'parent',
password,
suri,
type
}
: {
name: 'parent',
password,
suri
}, {} as chrome.runtime.Port);
const { address } = await extension.handle('id', 'pri(seed.validate)', type && type === 'ethereum'
? {
suri,
type
}
: {
suri
}, {} as chrome.runtime.Port);
return address;
};
beforeAll(async () => {
extension = await createExtension();
});
it('exports account from keyring', async () => {
const { pair: { address } } = keyring.addUri(suri, password);
const result = await extension.handle('id', 'pri(accounts.export)', {
address,
password
}, {} as chrome.runtime.Port);
expect(result.exportedJson.address).toBe(address);
expect(result.exportedJson.encoded).toBeDefined();
});
describe('account derivation', () => {
let address: string;
beforeEach(async () => {
address = await createAccount();
});
it('pri(derivation.validate) passes for valid suri', async () => {
const result = await extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(result).toEqual({
address: '5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN',
suri: '//path'
});
});
it('pri(derivation.validate) throws for invalid suri', async () => {
await expect(extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: password,
suri: 'invalid-path'
}, {} as chrome.runtime.Port)).rejects.toThrow(/is not a valid derivation path/);
});
it('pri(derivation.validate) throws for invalid password', async () => {
await expect(extension.handle('id', 'pri(derivation.validate)', {
parentAddress: address,
parentPassword: 'invalid-password',
suri: '//path'
}, {} as chrome.runtime.Port)).rejects.toThrow(/invalid password/);
});
it('pri(derivation.create) adds a derived account', async () => {
await extension.handle('id', 'pri(derivation.create)', {
name: 'child',
parentAddress: address,
parentPassword: password,
password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(keyring.getAccounts()).toHaveLength(2);
});
it('pri(derivation.create) saves parent address in meta', async () => {
await extension.handle('id', 'pri(derivation.create)', {
name: 'child',
parentAddress: address,
parentPassword: password,
password,
suri: '//path'
}, {} as chrome.runtime.Port);
expect(keyring.getAccount('5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN')?.meta.parentAddress).toEqual(address);
});
});
describe('account management', () => {
let address: string;
beforeEach(async () => {
address = await createAccount();
});
it('pri(accounts.changePassword) changes account password', async () => {
const newPass = 'pa55word';
const wrongPass = 'ZZzzZZzz';
await expect(extension.handle('id', 'pri(accounts.changePassword)', {
address,
newPass,
oldPass: wrongPass
}, {} as chrome.runtime.Port)).rejects.toThrow(/oldPass is invalid/);
const res = await extension.handle('id', 'pri(accounts.changePassword)', {
address,
newPass,
oldPass: password
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
const pair = keyring.getPair(address);
expect(pair.decodePkcs8(newPass)).toEqual(undefined);
expect(() => {
pair.decodePkcs8(password);
}).toThrow(/Unable to decode using the supplied passphrase/);
});
});
describe('custom user extension', () => {
let address: string, payload: SignerPayloadJSON, pair: KeyringPair;
beforeEach(async () => {
address = await createAccount();
pair = keyring.getPair(address);
pair.decodePkcs8(password);
payload = {
address,
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
blockNumber: '0x00000393',
era: '0x3601',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
nonce: '0x0000000000000000',
signedExtensions: ['CheckSpecVersion', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'],
specVersion: '0x00000026',
tip: '0x00000000000000000000000000000000',
transactionVersion: '0x00000005',
version: 4
};
});
it('signs with default signed extensions', async () => {
const registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions);
const signatureExpected = registry
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
// eslint-disable-next-line jest/valid-expect-in-promise
tabs.handle('1615191860871.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
.then((result) => {
// eslint-disable-next-line jest/no-conditional-expect
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
}).catch((err) => console.log(err));
const res = await extension.handle('1615192072290.7', 'pri(signing.approve.password)', {
id: state.allSignRequests[0].id,
password,
savePass: false
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
});
it('signs with default signed extensions - ethereum', async () => {
const ethAddress = await createAccount('ethereum');
const ethPair = keyring.getPair(ethAddress);
ethPair.decodePkcs8(password);
const ethPayload: SignerPayloadJSON = {
address: ethAddress,
blockHash: '0xf9fc354edc3ff49f43d5e2c14e3c609a0c4ba469ed091edf893d672993dc9bc0',
blockNumber: '0x00000393',
era: '0x3601',
genesisHash: '0xf9fc354edc3ff49f43d5e2c14e3c609a0c4ba469ed091edf893d672993dc9bc0',
method: '0x03003cd0a705a2dc65e5b1e1205896baa2be8a07c6e0070010a5d4e8',
nonce: '0x00000000',
signedExtensions: [
'CheckSpecVersion',
'CheckTxVersion',
'CheckGenesis',
'CheckMortality',
'CheckNonce',
'CheckWeight',
'ChargeTransactionPayment'
],
specVersion: '0x000003e9',
tip: '0x00000000000000000000000000000000',
transactionVersion: '0x00000002',
version: 4
};
const registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions);
const signatureExpected = registry
.createType('ExtrinsicPayload', ethPayload, { version: ethPayload.version }).sign(ethPair);
// eslint-disable-next-line jest/valid-expect-in-promise
tabs.handle('1615191860871.5', 'pub(extrinsic.sign)', ethPayload, 'http://localhost:3000', {} as chrome.runtime.Port)
.then((result) => {
// eslint-disable-next-line jest/no-conditional-expect
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
}).catch((err) => console.log(err));
const res = await extension.handle('1615192072290.7', 'pri(signing.approve.password)', {
id: state.allSignRequests[0].id,
password,
savePass: false
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
});
it('signs with user extensions, known types', async () => {
const types = {} as unknown as Record<string, string>;
const userExtensions = {
MyUserExtension: {
extrinsic: {
assetId: 'AssetId'
},
payload: {}
}
} as unknown as ExtDef;
const meta: MetadataDef = {
chain: 'Development',
color: '#191a2e',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
icon: '',
specVersion: 38,
ss58Format: 0,
tokenDecimals: 12,
tokenSymbol: '',
types,
userExtensions
};
state.saveMetadata(meta);
const payload: SignerPayloadJSON = {
address,
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
blockNumber: '0x00000393',
era: '0x3601',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
nonce: '0x0000000000000000',
signedExtensions: ['MyUserExtension'],
specVersion: '0x00000026',
tip: '0x00000000000000000000000000000000',
transactionVersion: '0x00000005',
version: 4
};
const registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
registry.register(types);
const signatureExpected = registry
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
// eslint-disable-next-line jest/valid-expect-in-promise
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
.then((result) => {
// eslint-disable-next-line jest/no-conditional-expect
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
}).catch((err) => console.log(err));
const res = await extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
id: state.allSignRequests[0].id,
password,
savePass: false
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
});
it('override default signed extension', async () => {
const types = {
FeeExchangeV1: {
assetId: 'Compact<AssetId>',
maxPayment: 'Compact<Balance>'
},
PaymentOptions: {
feeExchange: 'FeeExchangeV1',
tip: 'Compact<Balance>'
}
} as unknown as Record<string, string>;
const userExtensions = {
ChargeTransactionPayment: {
extrinsic: {
transactionPayment: 'PaymentOptions'
},
payload: {}
}
} as unknown as ExtDef;
const meta: MetadataDef = {
chain: 'Development',
color: '#191a2e',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
icon: '',
specVersion: 38,
ss58Format: 0,
tokenDecimals: 12,
tokenSymbol: '',
types,
userExtensions
};
state.saveMetadata(meta);
const registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
registry.register(types);
const signatureExpected = registry
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
// eslint-disable-next-line jest/valid-expect-in-promise
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
.then((result) => {
// eslint-disable-next-line jest/no-conditional-expect
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
}).catch((err) => console.log(err));
const res = await extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
id: state.allSignRequests[0].id,
password,
savePass: false
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
});
it('signs with user extensions, additional types', async () => {
const types = {
myCustomType: {
feeExchange: 'Compact<AssetId>',
tip: 'Compact<Balance>'
}
} as unknown as Record<string, string>;
const userExtensions = {
MyUserExtension: {
extrinsic: {
myCustomType: 'myCustomType'
},
payload: {}
}
} as unknown as ExtDef;
const meta: MetadataDef = {
chain: 'Development',
color: '#191a2e',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
icon: '',
specVersion: 38,
ss58Format: 0,
tokenDecimals: 12,
tokenSymbol: '',
types,
userExtensions
};
state.saveMetadata(meta);
const payload = {
address,
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
blockNumber: '0x00000393',
era: '0x3601',
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
nonce: '0x0000000000000000',
signedExtensions: ['MyUserExtension', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'],
specVersion: '0x00000026',
tip: null,
transactionVersion: '0x00000005',
version: 4
} as unknown as SignerPayloadJSON;
const registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
registry.register(types);
const signatureExpected = registry
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
// eslint-disable-next-line jest/valid-expect-in-promise
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
.then((result) => {
// eslint-disable-next-line jest/no-conditional-expect
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
}).catch((err) => console.log(err));
const res = await extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
id: state.allSignRequests[0].id,
password,
savePass: false
}, {} as chrome.runtime.Port);
expect(res).toEqual(true);
});
});
});