Update domain references to pezkuwichain.app and rebrand from polkadot

This commit is contained in:
2026-01-06 12:21:11 +03:00
commit c2913a65d9
401 changed files with 23179 additions and 0 deletions
@@ -0,0 +1,28 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { TypeRegistry } from '@pezkuwi/types';
import type { SignerPayloadRaw } from '@pezkuwi/types/types';
import type { HexString } from '@pezkuwi/util/types';
import type { RequestSign } from './types.js';
import { u8aToHex, u8aWrapBytes } from '@pezkuwi/util';
export default class RequestBytesSign implements RequestSign {
public readonly payload: SignerPayloadRaw;
constructor (payload: SignerPayloadRaw) {
this.payload = payload;
}
sign (_registry: TypeRegistry, pair: KeyringPair): { signature: HexString } {
return {
signature: u8aToHex(
pair.sign(
u8aWrapBytes(this.payload.data)
)
)
};
}
}
@@ -0,0 +1,22 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { TypeRegistry } from '@pezkuwi/types';
import type { SignerPayloadJSON } from '@pezkuwi/types/types';
import type { HexString } from '@pezkuwi/util/types';
import type { RequestSign } from './types.js';
export default class RequestExtrinsicSign implements RequestSign {
public readonly payload: SignerPayloadJSON;
constructor (payload: SignerPayloadJSON) {
this.payload = payload;
}
sign (registry: TypeRegistry, pair: KeyringPair): { signature: HexString } {
return registry
.createType('ExtrinsicPayload', this.payload, { version: this.payload.version })
.sign(pair);
}
}
@@ -0,0 +1,478 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import '@pezkuwi/extension-mocks/chrome';
import type * as _ from '@pezkuwi/dev-test/globals.d.ts';
import type { ResponseSigning } from '@pezkuwi/extension-base/background/types';
import type { MetadataDef } from '@pezkuwi/extension-inject/types';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { ExtDef } from '@pezkuwi/types/extrinsic/signedExtensions/types';
import type { SignerPayloadJSON } from '@pezkuwi/types/types';
import type { KeypairType } from '@pezkuwi/util-crypto/types';
import { TypeRegistry } from '@pezkuwi/types';
import keyring from '@pezkuwi/ui-keyring';
import { cryptoWaitReady } from '@pezkuwi/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';
async function createExtension (): Promise<Extension> {
try {
await cryptoWaitReady();
keyring.loadAll({ store: new AccountsStore() });
state = new State({}, 0);
await state.init();
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
};
await 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
};
await 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
};
await 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);
});
});
});
@@ -0,0 +1,690 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import type { MetadataDef } from '@pezkuwi/extension-inject/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@pezkuwi/keyring/types';
import type { Registry, SignerPayloadJSON, SignerPayloadRaw } from '@pezkuwi/types/types';
import type { SubjectInfo } from '@pezkuwi/ui-keyring/observable/types';
import type { KeypairType } from '@pezkuwi/util-crypto/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types.js';
import type { AuthorizedAccountsDiff } from './State.js';
import type State from './State.js';
import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@pezkuwi/extension-base/defaults';
import { metadataExpand } from '@pezkuwi/extension-chains';
import { TypeRegistry } from '@pezkuwi/types';
import { keyring } from '@pezkuwi/ui-keyring';
import { accounts as accountsObservable } from '@pezkuwi/ui-keyring/observable/accounts';
import { assert, isHex } from '@pezkuwi/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@pezkuwi/util-crypto';
import { withErrorLog } from './helpers.js';
import { createSubscription, unsubscribe } from './subscriptions.js';
type CachedUnlocks = Record<string, number>;
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 15, 18, 21, 24];
const ETH_DERIVE_DEFAULT = "/m/44'/60'/0'/0/0";
function getSuri (seed: string, type?: KeypairType): string {
return type === 'ethereum'
? `${seed}${ETH_DERIVE_DEFAULT}`
: seed;
}
function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON {
return (value as SignerPayloadJSON).genesisHash !== undefined;
}
export default class Extension {
readonly #cachedUnlocks: CachedUnlocks;
readonly #state: State;
constructor (state: State) {
this.#cachedUnlocks = {};
this.#state = state;
}
private transformAccounts (accounts: SubjectInfo): AccountJson[] {
return Object.values(accounts).map(({ json: { address, meta }, type }): AccountJson => ({
address,
isDefaultAuthSelected: this.#state.defaultAuthAccountSelection.includes(address),
...meta,
type
}));
}
private accountsCreateExternal ({ address, genesisHash, name }: RequestAccountCreateExternal): boolean {
keyring.addExternal(address, { genesisHash, name });
return true;
}
private accountsCreateHardware ({ accountIndex, address, addressOffset, genesisHash, hardwareType, name, type }: RequestAccountCreateHardware): boolean {
keyring.addHardware(address, hardwareType, { accountIndex, addressOffset, genesisHash, name, type });
return true;
}
private accountsCreateSuri ({ genesisHash, name, password, suri, type }: RequestAccountCreateSuri): boolean {
keyring.addUri(getSuri(suri, type), password, { genesisHash, name }, type);
return true;
}
private accountsChangePassword ({ address, newPass, oldPass }: RequestAccountChangePassword): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
try {
if (!pair.isLocked) {
pair.lock();
}
pair.decodePkcs8(oldPass);
} catch {
throw new Error('oldPass is invalid');
}
keyring.encryptAccount(pair, newPass);
return true;
}
private accountsEdit ({ address, name }: RequestAccountEdit): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
keyring.saveAccountMeta(pair, { ...pair.meta, name });
return true;
}
private accountsExport ({ address, password }: RequestAccountExport): ResponseAccountExport {
return { exportedJson: keyring.backupAccount(keyring.getPair(address), password) };
}
private async accountsBatchExport ({ addresses, password }: RequestAccountBatchExport): Promise<ResponseAccountsExport> {
return {
exportedJson: await keyring.backupAccounts(addresses, password)
};
}
private async accountsForget ({ address }: RequestAccountForget): Promise<boolean> {
const authorizedAccountsDiff: AuthorizedAccountsDiff = [];
// cycle through authUrls and prepare the array of diff
Object.entries(this.#state.authUrls).forEach(([url, urlInfo]) => {
// Note that urlInfo.authorizedAccounts may be undefined if this website entry
// was created before the "account authorization per website" functionality was introduced
if (urlInfo.authorizedAccounts?.includes(address)) {
authorizedAccountsDiff.push([url, urlInfo.authorizedAccounts.filter((previousAddress) => previousAddress !== address)]);
}
});
await this.#state.updateAuthorizedAccounts(authorizedAccountsDiff);
// cycle through default account selection for auth and remove any occurrence of the account
const newDefaultAuthAccounts = this.#state.defaultAuthAccountSelection.filter((defaultSelectionAddress) => defaultSelectionAddress !== address);
await this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts);
keyring.forgetAccount(address);
return true;
}
private refreshAccountPasswordCache (pair: KeyringPair): number {
const { address } = pair;
const savedExpiry = this.#cachedUnlocks[address] || 0;
const remainingTime = savedExpiry - Date.now();
if (remainingTime < 0) {
this.#cachedUnlocks[address] = 0;
pair.lock();
return 0;
}
return remainingTime;
}
private accountsShow ({ address, isShowing }: RequestAccountShow): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
keyring.saveAccountMeta(pair, { ...pair.meta, isHidden: !isShowing });
return true;
}
private accountsTie ({ address, genesisHash }: RequestAccountTie): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
keyring.saveAccountMeta(pair, { ...pair.meta, genesisHash });
return true;
}
private accountsValidate ({ address, password }: RequestAccountValidate): boolean {
try {
keyring.backupAccount(keyring.getPair(address), password);
return true;
} catch {
return false;
}
}
private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(accounts.subscribe)'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void =>
cb(this.transformAccounts(accounts))
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private authorizeApprove ({ authorizedAccounts, id }: RequestAuthorizeApprove): boolean {
const queued = this.#state.getAuthRequest(id);
assert(queued, 'Unable to find request');
const { resolve } = queued;
resolve({ authorizedAccounts, result: true });
return true;
}
private async authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): Promise<void> {
return await this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]);
}
private getAuthList (): ResponseAuthorizeList {
return { list: this.#state.authUrls };
}
// FIXME This looks very much like what we have in accounts
private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(authorize.requests)'>(id, port);
const subscription = this.#state.authSubject.subscribe((requests: AuthorizeRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private async metadataApprove ({ id }: RequestMetadataApprove): Promise<boolean> {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { request, resolve } = queued;
await this.#state.saveMetadata(request);
resolve(true);
return true;
}
private metadataGet (genesisHash: string | null): MetadataDef | null {
return this.#state.knownMetadata.find((result) => result.genesisHash === genesisHash) || null;
}
private metadataList (): MetadataDef[] {
return this.#state.knownMetadata;
}
private metadataReject ({ id }: RequestMetadataReject): boolean {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Rejected'));
return true;
}
private metadataSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(metadata.requests)'>(id, port);
const subscription = this.#state.metaSubject.subscribe((requests: MetadataRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private jsonRestore ({ file, password }: RequestJsonRestore): void {
try {
keyring.restoreAccount(file, password);
} catch (error) {
throw new Error((error as Error).message);
}
}
private batchRestore ({ file, password }: RequestBatchRestore): void {
try {
keyring.restoreAccounts(file, password);
} catch (error) {
throw new Error((error as Error).message);
}
}
private jsonGetAccountInfo (json: KeyringPair$Json): ResponseJsonGetAccountInfo {
try {
const { address, meta: { genesisHash, name }, type } = keyring.createFromJson(json);
return {
address,
genesisHash,
name,
type
} as ResponseJsonGetAccountInfo;
} catch (e) {
console.error(e);
throw new Error((e as Error).message);
}
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, seed: _seed, type }: RequestSeedCreate): ResponseSeedCreate {
const seed = _seed || mnemonicGenerate(length);
return {
address: keyring.createFromUri(getSuri(seed, type), {}, type).address,
seed
};
}
private seedValidate ({ suri, type }: RequestSeedValidate): ResponseSeedValidate {
const { phrase } = keyExtractSuri(suri);
if (isHex(phrase)) {
assert(isHex(phrase, 256), 'Hex seed needs to be 256-bits');
} else {
// sadly isHex detects as string, so we need a cast here
assert(SEED_LENGTHS.includes((phrase).split(' ').length), `Mnemonic needs to contain ${SEED_LENGTHS.join(', ')} words`);
assert(mnemonicValidate(phrase), 'Not a valid mnemonic seed');
}
return {
address: keyring.createFromUri(getSuri(suri, type), {}, type).address,
suri
};
}
private signingApprovePassword ({ id, password, savePass }: RequestSigningApprovePassword): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { reject, request, resolve } = queued;
const pair = keyring.getPair(queued.account.address);
if (!pair) {
reject(new Error('Unable to find pair'));
return false;
}
this.refreshAccountPasswordCache(pair);
// if the keyring pair is locked, the password is needed
if (pair.isLocked && !password) {
reject(new Error('Password needed to unlock the account'));
}
if (pair.isLocked) {
pair.decodePkcs8(password);
}
// construct a new registry (avoiding pollution), between requests
let registry: Registry;
const { payload } = request;
if (isJsonPayload(payload)) {
// Get the metadata for the genesisHash
const metadata = this.#state.knownMetadata.find(({ genesisHash }) => genesisHash === payload.genesisHash);
if (metadata) {
// we have metadata, expand it and extract the info/registry
const expanded = metadataExpand(metadata, false);
registry = expanded.registry;
registry.setSignedExtensions(payload.signedExtensions, expanded.definition.userExtensions);
} else {
// we have no metadata, create a new registry
registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions);
}
} else {
// for non-payload, just create a registry to use
registry = new TypeRegistry();
}
const result = request.sign(registry, pair);
if (savePass) {
// unlike queued.account.address the following
// address is encoded with the default prefix
// which what is used for password caching mapping
this.#cachedUnlocks[pair.address] = Date.now() + PASSWORD_EXPIRY_MS;
} else {
pair.lock();
}
resolve({
id,
...result
});
return true;
}
private signingApproveSignature ({ id, signature, signedTransaction }: RequestSigningApproveSignature): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { resolve } = queued;
resolve({ id, signature, signedTransaction });
return true;
}
private signingCancel ({ id }: RequestSigningCancel): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Cancelled'));
return true;
}
private signingIsLocked ({ id }: RequestSigningIsLocked): ResponseSigningIsLocked {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const address = queued.request.payload.address;
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
const remainingTime = this.refreshAccountPasswordCache(pair);
return {
isLocked: pair.isLocked,
remainingTime
};
}
// FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(signing.requests)'>(id, port);
const subscription = this.#state.signSubject.subscribe((requests: SigningRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private windowOpen (path: AllowedPath): boolean {
const url = `${chrome.runtime.getURL('index.html')}#${path}`;
if (!ALLOWED_PATH.includes(path)) {
console.error('Not allowed to open the url:', url);
return false;
}
withErrorLog(() => chrome.tabs.create({ url }));
return true;
}
private derive (parentAddress: string, suri: string, password: string, metadata: KeyringPair$Meta): KeyringPair {
const parentPair = keyring.getPair(parentAddress);
try {
parentPair.decodePkcs8(password);
} catch {
throw new Error('invalid password');
}
try {
return parentPair.derive(suri, metadata);
} catch {
throw new Error(`"${suri}" is not a valid derivation path`);
}
}
private derivationValidate ({ parentAddress, parentPassword, suri }: RequestDeriveValidate): ResponseDeriveValidate {
const childPair = this.derive(parentAddress, suri, parentPassword, {});
return {
address: childPair.address,
suri
};
}
private derivationCreate ({ genesisHash, name, parentAddress, parentPassword, password, suri }: RequestDeriveCreate): boolean {
const childPair = this.derive(parentAddress, suri, parentPassword, {
genesisHash,
name,
parentAddress,
suri
});
keyring.addPair(childPair, password);
return true;
}
private async removeAuthorization (url: string): Promise<ResponseAuthorizeList> {
const remAuth = await this.#state.removeAuthorization(url);
return { list: remAuth };
}
// Reject the authorization request and add the URL to the authorized list with no keys.
// The site will not prompt for re-authorization on future visits.
private rejectAuthRequest (id: string): void {
const queued = this.#state.getAuthRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Rejected'));
}
// Cancel the authorization request and do not add the URL to the authorized list.
// The site will prompt for authorization on future visits.
private cancelAuthRequest (id: string): void {
const queued = this.#state.getAuthRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Cancelled'));
}
private updateCurrentTabs ({ urls }: RequestActiveTabsUrlUpdate) {
this.#state.updateCurrentTabsUrl(urls);
}
private getConnectedTabsUrl () {
return this.#state.getConnectedTabsUrl();
}
// Weird thought, the eslint override is not needed in Tabs
// eslint-disable-next-line @typescript-eslint/require-await
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port?: chrome.runtime.Port): Promise<ResponseType<TMessageType>> {
switch (type) {
case 'pri(authorize.approve)':
return this.authorizeApprove(request as RequestAuthorizeApprove);
case 'pri(authorize.list)':
return this.getAuthList();
case 'pri(authorize.remove)':
return this.removeAuthorization(request as string);
case 'pri(authorize.reject)':
return this.rejectAuthRequest(request as string);
case 'pri(authorize.cancel)':
return this.cancelAuthRequest(request as string);
case 'pri(authorize.requests)':
return port && this.authorizeSubscribe(id, port);
case 'pri(authorize.update)':
return this.authorizeUpdate(request as RequestUpdateAuthorizedAccounts);
case 'pri(accounts.create.external)':
return this.accountsCreateExternal(request as RequestAccountCreateExternal);
case 'pri(accounts.create.hardware)':
return this.accountsCreateHardware(request as RequestAccountCreateHardware);
case 'pri(accounts.create.suri)':
return this.accountsCreateSuri(request as RequestAccountCreateSuri);
case 'pri(accounts.changePassword)':
return this.accountsChangePassword(request as RequestAccountChangePassword);
case 'pri(accounts.edit)':
return this.accountsEdit(request as RequestAccountEdit);
case 'pri(accounts.export)':
return this.accountsExport(request as RequestAccountExport);
case 'pri(accounts.batchExport)':
return this.accountsBatchExport(request as RequestAccountBatchExport);
case 'pri(accounts.forget)':
return this.accountsForget(request as RequestAccountForget);
case 'pri(accounts.show)':
return this.accountsShow(request as RequestAccountShow);
case 'pri(accounts.subscribe)':
return port && this.accountsSubscribe(id, port);
case 'pri(accounts.tie)':
return this.accountsTie(request as RequestAccountTie);
case 'pri(accounts.validate)':
return this.accountsValidate(request as RequestAccountValidate);
case 'pri(metadata.approve)':
return await this.metadataApprove(request as RequestMetadataApprove);
case 'pri(metadata.get)':
return this.metadataGet(request as string);
case 'pri(metadata.list)':
return this.metadataList();
case 'pri(metadata.reject)':
return this.metadataReject(request as RequestMetadataReject);
case 'pri(metadata.requests)':
return port && this.metadataSubscribe(id, port);
case 'pri(activeTabsUrl.update)':
return this.updateCurrentTabs(request as RequestActiveTabsUrlUpdate);
case 'pri(connectedTabsUrl.get)':
return this.getConnectedTabsUrl();
case 'pri(derivation.create)':
return this.derivationCreate(request as RequestDeriveCreate);
case 'pri(derivation.validate)':
return this.derivationValidate(request as RequestDeriveValidate);
case 'pri(json.restore)':
return this.jsonRestore(request as RequestJsonRestore);
case 'pri(json.batchRestore)':
return this.batchRestore(request as RequestBatchRestore);
case 'pri(json.account.info)':
return this.jsonGetAccountInfo(request as KeyringPair$Json);
case 'pri(ping)':
return Promise.resolve(true);
case 'pri(seed.create)':
return this.seedCreate(request as RequestSeedCreate);
case 'pri(seed.validate)':
return this.seedValidate(request as RequestSeedValidate);
case 'pri(settings.notification)':
return this.#state.setNotification(request as string);
case 'pri(signing.approve.password)':
return this.signingApprovePassword(request as RequestSigningApprovePassword);
case 'pri(signing.approve.signature)':
return this.signingApproveSignature(request as RequestSigningApproveSignature);
case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel);
case 'pri(signing.isLocked)':
return this.signingIsLocked(request as RequestSigningIsLocked);
case 'pri(signing.requests)':
return port && this.signingSubscribe(id, port);
case 'pri(window.open)':
return this.windowOpen(request as AllowedPath);
default:
throw new Error(`Unable to handle message of type ${type}`);
}
}
}
@@ -0,0 +1,664 @@
// Copyright 2019-2025 @pezkuwi/extension-bg authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import type { MetadataDef, ProviderMeta } from '@pezkuwi/extension-inject/types';
import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@pezkuwi/rpc-provider/types';
import type { AccountJson, AuthorizeRequest, AuthUrlInfo, AuthUrls, MetadataRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest } from '../types.js';
import { BehaviorSubject } from 'rxjs';
import { addMetadata, knownMetadata } from '@pezkuwi/extension-chains';
import { knownGenesis } from '@pezkuwi/networks/defaults';
import { settings } from '@pezkuwi/ui-settings';
import { assert } from '@pezkuwi/util';
import { MetadataStore } from '../../stores/index.js';
import { getId } from '../../utils/getId.js';
import { withErrorLog } from './helpers.js';
interface Resolver<T> {
reject: (error: Error) => void;
resolve: (result: T) => void;
}
interface AuthRequest extends Resolver<AuthResponse> {
id: string;
idStr: string;
request: RequestAuthorizeTab;
url: string;
}
export type AuthorizedAccountsDiff = [url: string, authorizedAccounts: AuthUrlInfo['authorizedAccounts']][]
interface MetaRequest extends Resolver<boolean> {
id: string;
request: MetadataDef;
url: string;
}
export interface AuthResponse {
result: boolean;
authorizedAccounts: string[];
}
// List of providers passed into constructor. This is the list of providers
// exposed by the extension.
type Providers = Record<string, {
meta: ProviderMeta;
// The provider is not running at init, calling this will instantiate the
// provider.
start: () => ProviderInterface;
}>
interface SignRequest extends Resolver<ResponseSigning> {
account: AccountJson;
id: string;
request: RequestSign;
url: string;
}
const NOTIFICATION_URL = chrome.runtime.getURL('notification.html');
const POPUP_WINDOW_OPTS: chrome.windows.CreateData = {
focused: true,
height: 621,
left: 150,
top: 150,
type: 'popup',
url: NOTIFICATION_URL,
width: 560
};
const NORMAL_WINDOW_OPTS: chrome.windows.CreateData = {
focused: true,
type: 'normal',
url: NOTIFICATION_URL
};
export enum NotificationOptions {
None,
Normal,
PopUp,
}
const AUTH_URLS_KEY = 'authUrls';
const DEFAULT_AUTH_ACCOUNTS = 'defaultAuthAccounts';
async function extractMetadata (store: MetadataStore): Promise<void> {
await store.allMap(async (map): Promise<void> => {
const knownEntries = Object.entries(knownGenesis);
const defs: Record<string, { def: MetadataDef, index: number, key: string }> = {};
const removals: string[] = [];
Object
.entries(map)
.forEach(([key, def]): void => {
const entry = knownEntries.find(([, hashes]) => hashes.includes(def.genesisHash));
if (entry) {
const [name, hashes] = entry;
const index = hashes.indexOf(def.genesisHash);
// flatten the known metadata based on the genesis index
// (lower is better/newer)
if (!defs[name] || (defs[name].index > index)) {
if (defs[name]) {
// remove the old version of the metadata
removals.push(defs[name].key);
}
defs[name] = { def, index, key };
}
} else {
// this is not a known entry, so we will just apply it
defs[key] = { def, index: 0, key };
}
});
for (const key of removals) {
await store.remove(key);
}
Object.values(defs).forEach(({ def }) => addMetadata(def));
});
}
export default class State {
#authUrls = new Map<string, AuthUrlInfo>();
#lastRequestTimestamps = new Map<string, number>();
#maxEntries = 10;
#rateLimitInterval = 3000; // 3 seconds
readonly #authRequests: Record<string, AuthRequest> = {};
readonly #metaStore = new MetadataStore();
// Map of providers currently injected in tabs
readonly #injectedProviders = new Map<chrome.runtime.Port, ProviderInterface>();
readonly #metaRequests: Record<string, MetaRequest> = {};
#notification = settings.notification;
// Map of all providers exposed by the extension, they are retrievable by key
readonly #providers: Providers;
readonly #signRequests: Record<string, SignRequest> = {};
#windows: number[] = [];
#connectedTabsUrl: string[] = [];
public readonly authSubject: BehaviorSubject<AuthorizeRequest[]> = new BehaviorSubject<AuthorizeRequest[]>([]);
public readonly metaSubject: BehaviorSubject<MetadataRequest[]> = new BehaviorSubject<MetadataRequest[]>([]);
public readonly signSubject: BehaviorSubject<SigningRequest[]> = new BehaviorSubject<SigningRequest[]>([]);
public readonly authUrlSubjects: Record<string, BehaviorSubject<AuthUrlInfo>> = {};
public defaultAuthAccountSelection: string[] = [];
constructor (providers: Providers = {}, rateLimitInterval = 3000) {
assert(rateLimitInterval >= 0, 'Expects non-negative number for rateLimitInterval');
this.#providers = providers;
this.#rateLimitInterval = rateLimitInterval;
}
public async init () {
await extractMetadata(this.#metaStore);
// retrieve previously set authorizations
const storageAuthUrls: Record<string, string> = await chrome.storage.local.get(AUTH_URLS_KEY);
const authString = storageAuthUrls?.[AUTH_URLS_KEY] || '{}';
const previousAuth = JSON.parse(authString) as AuthUrls;
this.#authUrls = new Map(Object.entries(previousAuth));
// Initialize authUrlSubjects for each URL
this.#authUrls.forEach((authInfo, url) => {
this.authUrlSubjects[url] = new BehaviorSubject<AuthUrlInfo>(authInfo);
});
// retrieve previously set default auth accounts
const storageDefaultAuthAccounts: Record<string, string> = await chrome.storage.local.get(DEFAULT_AUTH_ACCOUNTS);
const defaultAuthString: string = storageDefaultAuthAccounts?.[DEFAULT_AUTH_ACCOUNTS] || '[]';
const previousDefaultAuth = JSON.parse(defaultAuthString) as string[];
this.defaultAuthAccountSelection = previousDefaultAuth;
}
public get knownMetadata (): MetadataDef[] {
return knownMetadata();
}
public get numAuthRequests (): number {
return Object.keys(this.#authRequests).length;
}
public get numMetaRequests (): number {
return Object.keys(this.#metaRequests).length;
}
public get numSignRequests (): number {
return Object.keys(this.#signRequests).length;
}
public get allAuthRequests (): AuthorizeRequest[] {
return Object
.values(this.#authRequests)
.map(({ id, request, url }): AuthorizeRequest => ({ id, request, url }));
}
public get allMetaRequests (): MetadataRequest[] {
return Object
.values(this.#metaRequests)
.map(({ id, request, url }): MetadataRequest => ({ id, request, url }));
}
public get allSignRequests (): SigningRequest[] {
return Object
.values(this.#signRequests)
.map(({ account, id, request, url }): SigningRequest => ({ account, id, request, url }));
}
public get authUrls (): AuthUrls {
return Object.fromEntries(this.#authUrls);
}
private popupClose (): void {
this.#windows.forEach((id: number) =>
withErrorLog(() => chrome.windows.remove(id))
);
this.#windows = [];
}
private popupOpen (): void {
this.#notification !== 'extension' &&
chrome.windows.create(
this.#notification === 'window'
? NORMAL_WINDOW_OPTS
: POPUP_WINDOW_OPTS,
(window): void => {
if (window) {
this.#windows.push(window.id || 0);
}
});
}
private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void): Resolver<AuthResponse> => {
const complete = async (authorizedAccounts: string[] = []) => {
const { idStr, request: { origin }, url } = this.#authRequests[id];
const strippedUrl = this.stripUrl(url);
const authInfo: AuthUrlInfo = {
authorizedAccounts,
count: 0,
id: idStr,
origin,
url
};
this.#authUrls.set(strippedUrl, authInfo);
if (!this.authUrlSubjects[strippedUrl]) {
this.authUrlSubjects[strippedUrl] = new BehaviorSubject<AuthUrlInfo>(authInfo);
} else {
this.authUrlSubjects[strippedUrl].next(authInfo);
}
await this.saveCurrentAuthList();
await this.updateDefaultAuthAccounts(authorizedAccounts);
delete this.#authRequests[id];
this.updateIconAuth(true);
};
return {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
reject: async (error: Error): Promise<void> => {
if (error.message === 'Cancelled') {
delete this.#authRequests[id];
this.updateIconAuth(true);
reject(new Error('Connection request was cancelled by the user.'));
} else {
await complete();
reject(new Error('Connection request was rejected by the user.'));
}
},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
resolve: async ({ authorizedAccounts, result }: AuthResponse): Promise<void> => {
await complete(authorizedAccounts);
resolve({ authorizedAccounts, result });
}
};
};
/**
* @deprecated This method is deprecated in favor of {@link updateCurrentTabs} and will be removed in a future release.
*/
public udateCurrentTabsUrl (urls: string[]) {
this.updateCurrentTabsUrl(urls);
}
public updateCurrentTabsUrl (urls: string[]) {
const connectedTabs = urls.map((url) => {
let strippedUrl = '';
// the assert in stripUrl may throw for new tabs with "chrome://newtab/"
try {
strippedUrl = this.stripUrl(url);
} catch (e) {
console.error(e);
}
// return the stripped url only if this website is known
return !!strippedUrl && this.authUrls[strippedUrl]
? strippedUrl
: undefined;
})
.filter((value) => !!value) as string[];
this.#connectedTabsUrl = connectedTabs;
}
public getConnectedTabsUrl () {
return this.#connectedTabsUrl;
}
public deleteAuthRequest (requestId: string) {
delete this.#authRequests[requestId];
this.updateIconAuth(true);
}
private async saveCurrentAuthList () {
await chrome.storage.local.set({ [AUTH_URLS_KEY]: JSON.stringify(Object.fromEntries(this.#authUrls)) });
}
private async saveDefaultAuthAccounts () {
await chrome.storage.local.set({ [DEFAULT_AUTH_ACCOUNTS]: JSON.stringify(this.defaultAuthAccountSelection) });
}
public async updateDefaultAuthAccounts (newList: string[]) {
this.defaultAuthAccountSelection = newList;
await this.saveDefaultAuthAccounts();
}
private metaComplete = (id: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver<boolean> => {
const complete = (): void => {
delete this.#metaRequests[id];
this.updateIconMeta(true);
};
return {
reject: (error: Error): void => {
complete();
reject(error);
},
resolve: (result: boolean): void => {
complete();
resolve(result);
}
};
};
private signComplete = (id: string, resolve: (result: ResponseSigning) => void, reject: (error: Error) => void): Resolver<ResponseSigning> => {
const complete = (): void => {
delete this.#signRequests[id];
this.updateIconSign(true);
};
return {
reject: (error: Error): void => {
complete();
reject(error);
},
resolve: (result: ResponseSigning): void => {
complete();
resolve(result);
}
};
};
public stripUrl (url: string): string {
try {
const parsedUrl = new URL(url);
if (!['http:', 'https:', 'ipfs:', 'ipns:'].includes(parsedUrl.protocol)) {
throw new Error(`Invalid protocol ${parsedUrl.protocol}`);
}
// For ipfs/ipns which don't have a standard origin, we handle it differently.
if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {
// ipfs://<hash> | ipns://<hash>
return `${parsedUrl.protocol}//${parsedUrl.hostname}`;
}
return parsedUrl.origin;
} catch (e) {
console.error(e);
throw new Error('Invalid URL');
}
}
private updateIcon (shouldClose?: boolean): void {
const authCount = this.numAuthRequests;
const metaCount = this.numMetaRequests;
const signCount = this.numSignRequests;
const text = (
authCount
? 'Auth'
: metaCount
? 'Meta'
: (signCount ? `${signCount}` : '')
);
withErrorLog(() => chrome.action.setBadgeText({ text }));
if (shouldClose && text === '') {
this.popupClose();
}
}
public async removeAuthorization (url: string): Promise<AuthUrls> {
const entry = this.#authUrls.get(url);
assert(entry, `The source ${url} is not known`);
this.#authUrls.delete(url);
await this.saveCurrentAuthList();
if (this.authUrlSubjects[url]) {
entry.authorizedAccounts = [];
this.authUrlSubjects[url].next(entry);
}
return this.authUrls;
}
private updateIconAuth (shouldClose?: boolean): void {
this.authSubject.next(this.allAuthRequests);
this.updateIcon(shouldClose);
}
private updateIconMeta (shouldClose?: boolean): void {
this.metaSubject.next(this.allMetaRequests);
this.updateIcon(shouldClose);
}
private updateIconSign (shouldClose?: boolean): void {
this.signSubject.next(this.allSignRequests);
this.updateIcon(shouldClose);
}
public async updateAuthorizedAccounts (authorizedAccountsDiff: AuthorizedAccountsDiff): Promise<void> {
authorizedAccountsDiff.forEach(([url, authorizedAccountDiff]) => {
const authInfo = this.#authUrls.get(url);
if (authInfo) {
authInfo.authorizedAccounts = authorizedAccountDiff;
this.#authUrls.set(url, authInfo);
this.authUrlSubjects[url].next(authInfo);
}
});
await this.saveCurrentAuthList();
}
public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise<AuthResponse> {
const idStr = this.stripUrl(url);
// Do not enqueue duplicate authorization requests.
const isDuplicate = Object
.values(this.#authRequests)
.some((request) => request.idStr === idStr);
assert(!isDuplicate, `The source ${url} has a pending authorization request`);
if (this.#authUrls.has(idStr)) {
// this url was seen in the past
const authInfo = this.#authUrls.get(idStr);
assert(authInfo?.authorizedAccounts || authInfo?.isAllowed, `The source ${url} is not allowed to interact with this extension`);
return {
authorizedAccounts: [],
result: false
};
}
return new Promise((resolve, reject): void => {
const id = getId();
this.#authRequests[id] = {
...this.authComplete(id, resolve, reject),
id,
idStr,
request,
url
};
this.updateIconAuth();
this.popupOpen();
});
}
public ensureUrlAuthorized (url: string): boolean {
const entry = this.#authUrls.get(this.stripUrl(url));
assert(entry, `The source ${url} has not been enabled yet`);
return true;
}
public injectMetadata (url: string, request: MetadataDef): Promise<boolean> {
return new Promise((resolve, reject): void => {
const id = getId();
this.#metaRequests[id] = {
...this.metaComplete(id, resolve, reject),
id,
request,
url
};
this.updateIconMeta();
this.popupOpen();
});
}
public getAuthRequest (id: string): AuthRequest {
return this.#authRequests[id];
}
public getMetaRequest (id: string): MetaRequest {
return this.#metaRequests[id];
}
public getSignRequest (id: string): SignRequest {
return this.#signRequests[id];
}
// List all providers the extension is exposing
public rpcListProviders (): Promise<ResponseRpcListProviders> {
return Promise.resolve(Object.keys(this.#providers).reduce((acc, key) => {
acc[key] = this.#providers[key].meta;
return acc;
}, {} as ResponseRpcListProviders));
}
public rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise<JsonRpcResponse<unknown>> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set');
return provider.send(request.method, request.params);
}
// Start a provider, return its meta
public rpcStartProvider (key: string, port: chrome.runtime.Port): Promise<ProviderMeta> {
assert(Object.keys(this.#providers).includes(key), `Provider ${key} is not exposed by extension`);
if (this.#injectedProviders.get(port)) {
return Promise.resolve(this.#providers[key].meta);
}
// Instantiate the provider
this.#injectedProviders.set(port, this.#providers[key].start());
// Close provider connection when page is closed
port.onDisconnect.addListener((): void => {
const provider = this.#injectedProviders.get(port);
if (provider) {
withErrorLog(() => provider.disconnect());
}
this.#injectedProviders.delete(port);
});
return Promise.resolve(this.#providers[key].meta);
}
public rpcSubscribe ({ method, params, type }: RequestRpcSubscribe, cb: ProviderInterfaceCallback, port: chrome.runtime.Port): Promise<number | string> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set');
return provider.subscribe(type, method, params, cb);
}
public rpcSubscribeConnected (_request: null, cb: ProviderInterfaceCallback, port: chrome.runtime.Port): void {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.subscribeConnected) before provider is set');
cb(null, provider.isConnected); // Immediately send back current isConnected
provider.on('connected', () => cb(null, true));
provider.on('disconnected', () => cb(null, false));
}
public rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise<boolean> {
const provider = this.#injectedProviders.get(port);
assert(provider, 'Cannot call pub(rpc.unsubscribe) before provider is set');
return provider.unsubscribe(request.type, request.method, request.subscriptionId);
}
public async saveMetadata (meta: MetadataDef): Promise<void> {
await this.#metaStore.set(meta.genesisHash, meta);
addMetadata(meta);
}
public setNotification (notification: string): boolean {
this.#notification = notification;
return true;
}
private handleSignRequest (origin: string) {
const now = Date.now();
const lastTime = this.#lastRequestTimestamps.get(origin) || 0;
if (now - lastTime < this.#rateLimitInterval) {
throw new Error('Rate limit exceeded. Try again later.');
}
// If we're about to exceed max entries, evict the oldest
if (!this.#lastRequestTimestamps.has(origin) && this.#lastRequestTimestamps.size >= this.#maxEntries) {
const oldestKey = this.#lastRequestTimestamps.keys().next().value;
oldestKey && this.#lastRequestTimestamps.delete(oldestKey);
}
this.#lastRequestTimestamps.set(origin, now);
}
public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> {
const id = getId();
try {
this.handleSignRequest(url);
} catch (error) {
return Promise.reject(error);
}
return new Promise((resolve, reject): void => {
this.#signRequests[id] = {
...this.signComplete(id, resolve, reject),
account,
id,
request,
url
};
this.updateIconSign();
this.popupOpen();
});
}
}
@@ -0,0 +1,289 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import type { InjectedAccount, InjectedMetadataKnown, MetadataDef, ProviderMeta } from '@pezkuwi/extension-inject/types';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { JsonRpcResponse } from '@pezkuwi/rpc-provider/types';
import type { SignerPayloadJSON, SignerPayloadRaw } from '@pezkuwi/types/types';
import type { SubjectInfo } from '@pezkuwi/ui-keyring/observable/types';
import type { AuthUrlInfo, MessageTypes, RequestAccountList, RequestAccountUnsubscribe, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestTypes, ResponseRpcListProviders, ResponseSigning, ResponseTypes, SubscriptionMessageTypes } from '../types.js';
import type { AuthResponse } from './State.js';
import type State from './State.js';
import { combineLatest, type Subscription } from 'rxjs';
import { checkIfDenied } from '@pezkuwi/phishing';
import { keyring } from '@pezkuwi/ui-keyring';
import { accounts as accountsObservable } from '@pezkuwi/ui-keyring/observable/accounts';
import { assert, isNumber } from '@pezkuwi/util';
import { PHISHING_PAGE_REDIRECT } from '../../defaults.js';
import { canDerive } from '../../utils/index.js';
import RequestBytesSign from '../RequestBytesSign.js';
import RequestExtrinsicSign from '../RequestExtrinsicSign.js';
import { withErrorLog } from './helpers.js';
import { createSubscription, unsubscribe } from './subscriptions.js';
interface AccountSub {
subscription: Subscription;
url: string;
}
function transformAccounts (accounts: SubjectInfo, anyType = false): InjectedAccount[] {
return Object
.values(accounts)
.filter(({ json: { meta: { isHidden } } }) => !isHidden)
.filter(({ type }) => anyType ? true : canDerive(type))
.sort((a, b) => (a.json.meta.whenCreated || 0) - (b.json.meta.whenCreated || 0))
.map(({ json: { address, meta: { genesisHash, name } }, type }): InjectedAccount => ({
address,
genesisHash,
name,
type
}));
}
export default class Tabs {
readonly #accountSubs: Record<string, AccountSub> = {};
readonly #state: State;
constructor (state: State) {
this.#state = state;
}
private filterForAuthorizedAccounts (accounts: InjectedAccount[], url: string): InjectedAccount[] {
const auth = this.#state.authUrls[this.#state.stripUrl(url)];
if (!auth) {
return [];
}
return accounts.filter(
(allAcc) =>
auth.authorizedAccounts
// we have a list, use it
? auth.authorizedAccounts.includes(allAcc.address)
// if no authorizedAccounts and isAllowed return all - these are old converted urls
: auth.isAllowed
);
}
private authorize (url: string, request: RequestAuthorizeTab): Promise<AuthResponse> {
return this.#state.authorizeUrl(url, request);
}
private accountsListAuthorized (url: string, { anyType }: RequestAccountList): InjectedAccount[] {
const transformedAccounts = transformAccounts(accountsObservable.subject.getValue(), anyType);
return this.filterForAuthorizedAccounts(transformedAccounts, url);
}
private accountsSubscribeAuthorized (url: string, id: string, port: chrome.runtime.Port): string {
const cb = createSubscription<'pub(accounts.subscribe)'>(id, port);
const strippedUrl = this.#state.stripUrl(url);
const authUrlObservable = this.#state.authUrlSubjects[strippedUrl]?.asObservable();
if (!authUrlObservable) {
console.error(`No authUrlSubject found for ${strippedUrl}`);
return id;
}
this.#accountSubs[id] = {
subscription: combineLatest([accountsObservable.subject, authUrlObservable]).subscribe(([accounts, _authUrlInfo]: [SubjectInfo, AuthUrlInfo]): void => {
const transformedAccounts = transformAccounts(accounts);
cb(this.filterForAuthorizedAccounts(transformedAccounts, url));
}),
url
};
port.onDisconnect.addListener((): void => {
this.accountsUnsubscribe(url, { id });
});
return id;
}
private accountsUnsubscribe (url: string, { id }: RequestAccountUnsubscribe): boolean {
const sub = this.#accountSubs[id];
if (!sub || sub.url !== url) {
return false;
}
delete this.#accountSubs[id];
unsubscribe(id);
sub.subscription.unsubscribe();
return true;
}
private getSigningPair (address: string): KeyringPair {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find keypair');
return pair;
}
private bytesSign (url: string, request: SignerPayloadRaw): Promise<ResponseSigning> {
const address = request.address;
const pair = this.getSigningPair(address);
return this.#state.sign(url, new RequestBytesSign(request), { address, ...pair.meta });
}
private extrinsicSign (url: string, request: SignerPayloadJSON): Promise<ResponseSigning> {
const address = request.address;
const pair = this.getSigningPair(address);
return this.#state.sign(url, new RequestExtrinsicSign(request), { address, ...pair.meta });
}
private metadataProvide (url: string, request: MetadataDef): Promise<boolean> {
return this.#state.injectMetadata(url, request);
}
private metadataList (_url: string): InjectedMetadataKnown[] {
return this.#state.knownMetadata.map(({ genesisHash, specVersion }) => ({
genesisHash,
specVersion
}));
}
private rpcListProviders (): Promise<ResponseRpcListProviders> {
return this.#state.rpcListProviders();
}
private rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise<JsonRpcResponse<unknown>> {
return this.#state.rpcSend(request, port);
}
private rpcStartProvider (key: string, port: chrome.runtime.Port): Promise<ProviderMeta> {
return this.#state.rpcStartProvider(key, port);
}
private async rpcSubscribe (request: RequestRpcSubscribe, id: string, port: chrome.runtime.Port): Promise<boolean> {
const innerCb = createSubscription<'pub(rpc.subscribe)'>(id, port);
const cb = (_error: Error | null, data: SubscriptionMessageTypes['pub(rpc.subscribe)']): void => innerCb(data);
const subscriptionId = await this.#state.rpcSubscribe(request, cb, port);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
withErrorLog(() => this.rpcUnsubscribe({ ...request, subscriptionId }, port));
});
return true;
}
private rpcSubscribeConnected (request: null, id: string, port: chrome.runtime.Port): Promise<boolean> {
const innerCb = createSubscription<'pub(rpc.subscribeConnected)'>(id, port);
const cb = (_error: Error | null, data: SubscriptionMessageTypes['pub(rpc.subscribeConnected)']): void => innerCb(data);
this.#state.rpcSubscribeConnected(request, cb, port);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
});
return Promise.resolve(true);
}
private async rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise<boolean> {
return this.#state.rpcUnsubscribe(request, port);
}
private redirectPhishingLanding (phishingWebsite: string): void {
const nonFragment = phishingWebsite.split('#')[0];
const encodedWebsite = encodeURIComponent(nonFragment);
const url = `${chrome.runtime.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`;
chrome.tabs.query({ url: nonFragment }, (tabs) => {
tabs
.map(({ id }) => id)
.filter((id): id is number => isNumber(id))
.forEach((id) =>
withErrorLog(() => chrome.tabs.update(id, { url }))
);
});
}
private async redirectIfPhishing (url: string): Promise<boolean> {
const isInDenyList = await checkIfDenied(url);
if (isInDenyList) {
this.redirectPhishingLanding(url);
return true;
}
return false;
}
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port?: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
if (type === 'pub(phishing.redirectIfDenied)') {
return this.redirectIfPhishing(url);
}
if (type !== 'pub(authorize.tab)') {
this.#state.ensureUrlAuthorized(url);
}
switch (type) {
case 'pub(authorize.tab)':
return this.authorize(url, request as RequestAuthorizeTab);
case 'pub(accounts.list)':
return this.accountsListAuthorized(url, request as RequestAccountList);
case 'pub(accounts.subscribe)':
return port && this.accountsSubscribeAuthorized(url, id, port);
case 'pub(accounts.unsubscribe)':
return this.accountsUnsubscribe(url, request as RequestAccountUnsubscribe);
case 'pub(bytes.sign)':
return this.bytesSign(url, request as SignerPayloadRaw);
case 'pub(extrinsic.sign)':
return this.extrinsicSign(url, request as SignerPayloadJSON);
case 'pub(metadata.list)':
return this.metadataList(url);
case 'pub(metadata.provide)':
return this.metadataProvide(url, request as MetadataDef);
case 'pub(ping)':
return Promise.resolve(true);
case 'pub(rpc.listProviders)':
return this.rpcListProviders();
case 'pub(rpc.send)':
return port && this.rpcSend(request as RequestRpcSend, port);
case 'pub(rpc.startProvider)':
return port && this.rpcStartProvider(request as string, port);
case 'pub(rpc.subscribe)':
return port && this.rpcSubscribe(request as RequestRpcSubscribe, id, port);
case 'pub(rpc.subscribeConnected)':
return port && this.rpcSubscribeConnected(request as null, id, port);
case 'pub(rpc.unsubscribe)':
return port && this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port);
default:
throw new Error(`Unable to handle message of type ${type}`);
}
}
}
@@ -0,0 +1,14 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
export function withErrorLog (fn: () => unknown): void {
try {
const p = fn();
if (p && typeof p === 'object' && typeof (p as Promise<unknown>).catch === 'function') {
(p as Promise<unknown>).catch(console.error);
}
} catch (e) {
console.error(e);
}
}
@@ -0,0 +1,60 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import type { MessageTypes, TransportRequestMessage } from '../types.js';
import { assert } from '@pezkuwi/util';
import { PORT_EXTENSION } from '../../defaults.js';
import Extension from './Extension.js';
import State from './State.js';
import Tabs from './Tabs.js';
export { withErrorLog } from './helpers.js';
const state = new State();
await state.init();
const extension = new Extension(state);
const tabs = new Tabs(state);
export default function handler<TMessageType extends MessageTypes> ({ id, message, request }: TransportRequestMessage<TMessageType>, port?: chrome.runtime.Port, extensionPortName = PORT_EXTENSION): void {
const isExtension = !port || port?.name === extensionPortName;
const sender = port?.sender;
if (!isExtension && !sender) {
throw new Error('Unable to extract message sender');
}
const from = isExtension
? 'extension'
: sender?.url || sender?.tab?.url || '<unknown>';
const source = `${from}: ${id}: ${message}`;
console.log(` [in] ${source}`); // :: ${JSON.stringify(request)}`);
const promise = isExtension
? extension.handle(id, message, request, port)
: tabs.handle(id, message, request, from, port);
promise
.then((response: unknown): void => {
console.log(`[out] ${source}`); // :: ${JSON.stringify(response)}`);
// between the start and the end of the promise, the user may have closed
// the tab, in which case port will be undefined
assert(port, 'Port has been disconnected');
port.postMessage({ id, response });
})
.catch((error: Error): void => {
console.log(`[err] ${source}:: ${error.message}`);
// only send message back to port if it's still connected
if (port) {
port.postMessage({ error: error.message, id });
}
});
}
@@ -0,0 +1,32 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import type { MessageTypesWithSubscriptions, SubscriptionMessageTypes } from '../types.js';
type Subscriptions = Record<string, chrome.runtime.Port>;
const subscriptions: Subscriptions = {};
// return a subscription callback, that will send the data to the caller via the port
export function createSubscription<TMessageType extends MessageTypesWithSubscriptions> (id: string, port: chrome.runtime.Port): (data: SubscriptionMessageTypes[TMessageType]) => void {
subscriptions[id] = port;
return (subscription: unknown): void => {
if (subscriptions[id]) {
port.postMessage({ id, subscription });
}
};
}
// clear a previous subscriber
export function unsubscribe (id: string): void {
if (subscriptions[id]) {
console.log(`Unsubscribing from ${id}`);
delete subscriptions[id];
} else {
console.error(`Unable to unsubscribe from ${id}`);
}
}
@@ -0,0 +1,4 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { default as handlers, withErrorLog } from './handlers/index.js';
@@ -0,0 +1,432 @@
// Copyright 2019-2025 @pezkuwi/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable no-use-before-define */
import type { InjectedAccount, InjectedMetadataKnown, MetadataDef, ProviderList, ProviderMeta } from '@pezkuwi/extension-inject/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@pezkuwi/keyring/types';
import type { JsonRpcResponse } from '@pezkuwi/rpc-provider/types';
import type { Registry, SignerPayloadJSON, SignerPayloadRaw } from '@pezkuwi/types/types';
import type { KeyringPairs$Json } from '@pezkuwi/ui-keyring/types';
import type { HexString } from '@pezkuwi/util/types';
import type { KeypairType } from '@pezkuwi/util-crypto/types';
import type { ALLOWED_PATH } from '../defaults.js';
import type { AuthResponse } from './handlers/State.js';
type KeysWithDefinedValues<T> = {
[K in keyof T]: T[K] extends undefined ? never : K
}[keyof T];
type NoUndefinedValues<T> = {
[K in KeysWithDefinedValues<T>]: T[K]
};
type IsNull<T, K extends keyof T> = { [K1 in Exclude<keyof T, K>]: T[K1] } & T[K] extends null ? K : never;
type NullKeys<T> = { [K in keyof T]: IsNull<T, K> }[keyof T];
export type AuthUrls = Record<string, AuthUrlInfo>;
export interface AuthUrlInfo {
count: number;
id: string;
// this is from pre-0.44.1
isAllowed?: boolean;
origin: string;
url: string;
authorizedAccounts: string[];
}
export type SeedLengths = 12 | 24;
export interface AccountJson extends KeyringPair$Meta {
address: string;
}
export type AccountWithChildren = AccountJson & {
children?: AccountWithChildren[];
}
export interface AccountsContext {
accounts: AccountJson[];
hierarchy: AccountWithChildren[];
master?: AccountJson;
selectedAccounts?: AccountJson['address'][];
setSelectedAccounts?: (address: AccountJson['address'][]) => void;
}
export interface AuthorizeRequest {
id: string;
request: RequestAuthorizeTab;
url: string;
}
export interface MetadataRequest {
id: string;
request: MetadataDef;
url: string;
}
export interface SigningRequest {
account: AccountJson;
id: string;
request: RequestSign;
url: string;
}
export type ConnectedTabsUrlResponse = string[]
// [MessageType]: [RequestType, ResponseType, SubscriptionMessageType?]
export interface RequestSignatures {
// private/internal requests, i.e. from a popup
'pri(accounts.create.external)': [RequestAccountCreateExternal, boolean];
'pri(accounts.create.hardware)': [RequestAccountCreateHardware, boolean];
'pri(accounts.create.suri)': [RequestAccountCreateSuri, boolean];
'pri(accounts.edit)': [RequestAccountEdit, boolean];
'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport];
'pri(accounts.batchExport)': [RequestAccountBatchExport, ResponseAccountsExport]
'pri(accounts.forget)': [RequestAccountForget, boolean];
'pri(accounts.list)': [RequestAccountList, InjectedAccount[]];
'pri(accounts.show)': [RequestAccountShow, boolean];
'pri(accounts.tie)': [RequestAccountTie, boolean];
'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]];
'pri(accounts.validate)': [RequestAccountValidate, boolean];
'pri(accounts.changePassword)': [RequestAccountChangePassword, boolean];
'pri(authorize.approve)': [RequestAuthorizeApprove, boolean];
'pri(authorize.list)': [null, ResponseAuthorizeList];
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
'pri(authorize.remove)': [string, ResponseAuthorizeList];
'pri(authorize.reject)': [string, void];
'pri(authorize.cancel)': [string, void];
'pri(authorize.update)': [RequestUpdateAuthorizedAccounts, void];
'pri(activeTabsUrl.update)': [RequestActiveTabsUrlUpdate, void];
'pri(connectedTabsUrl.get)': [null, ConnectedTabsUrlResponse];
'pri(derivation.create)': [RequestDeriveCreate, boolean];
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
'pri(json.restore)': [RequestJsonRestore, void];
'pri(json.batchRestore)': [RequestBatchRestore, void];
'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo];
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
'pri(metadata.get)': [string | null, MetadataDef | null];
'pri(metadata.reject)': [RequestMetadataReject, boolean];
'pri(metadata.requests)': [RequestMetadataSubscribe, boolean, MetadataRequest[]];
'pri(metadata.list)': [null, MetadataDef[]];
'pri(ping)': [null, boolean];
'pri(seed.create)': [RequestSeedCreate, ResponseSeedCreate];
'pri(seed.validate)': [RequestSeedValidate, ResponseSeedValidate];
'pri(settings.notification)': [string, boolean];
'pri(signing.approve.password)': [RequestSigningApprovePassword, boolean];
'pri(signing.approve.signature)': [RequestSigningApproveSignature, boolean];
'pri(signing.cancel)': [RequestSigningCancel, boolean];
'pri(signing.isLocked)': [RequestSigningIsLocked, ResponseSigningIsLocked];
'pri(signing.requests)': [RequestSigningSubscribe, boolean, SigningRequest[]];
'pri(window.open)': [AllowedPath, boolean];
// public/external requests, i.e. from a page
'pub(accounts.list)': [RequestAccountList, InjectedAccount[]];
'pub(accounts.subscribe)': [RequestAccountSubscribe, string, InjectedAccount[]];
'pub(accounts.unsubscribe)': [RequestAccountUnsubscribe, boolean];
'pub(authorize.tab)': [RequestAuthorizeTab, Promise<AuthResponse>];
'pub(bytes.sign)': [SignerPayloadRaw, ResponseSigning];
'pub(extrinsic.sign)': [SignerPayloadJSON, ResponseSigning];
'pub(metadata.list)': [null, InjectedMetadataKnown[]];
'pub(metadata.provide)': [MetadataDef, boolean];
'pub(phishing.redirectIfDenied)': [null, boolean];
'pub(ping)': [null, boolean];
'pub(rpc.listProviders)': [void, ResponseRpcListProviders];
'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse<unknown>];
'pub(rpc.startProvider)': [string, ProviderMeta];
'pub(rpc.subscribe)': [RequestRpcSubscribe, number, JsonRpcResponse<unknown>];
'pub(rpc.subscribeConnected)': [null, boolean, boolean];
'pub(rpc.unsubscribe)': [RequestRpcUnsubscribe, boolean];
}
export type MessageTypes = keyof RequestSignatures;
// Requests
export type RequestTypes = {
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][0]
};
export type MessageTypesWithNullRequest = NullKeys<RequestTypes>
export interface TransportRequestMessage<TMessageType extends MessageTypes> {
id: string;
message: TMessageType;
origin: string;
request: RequestTypes[TMessageType];
}
export interface RequestAuthorizeTab {
origin: string;
}
export interface RequestAuthorizeApprove {
id: string;
authorizedAccounts: string[]
}
export interface RequestUpdateAuthorizedAccounts {
url: string;
authorizedAccounts: string[]
}
export type RequestAuthorizeSubscribe = null;
export interface RequestMetadataApprove {
id: string;
}
export interface RequestMetadataReject {
id: string;
}
export type RequestMetadataSubscribe = null;
export interface RequestAccountCreateExternal {
address: string;
genesisHash?: HexString | null;
name: string;
}
export interface RequestAccountCreateSuri {
name: string;
genesisHash?: HexString | null;
password: string;
suri: string;
type?: KeypairType;
}
export interface RequestAccountCreateHardware {
accountIndex: number;
address: string;
addressOffset: number;
genesisHash: HexString;
hardwareType: string;
name: string;
type: KeypairType;
}
export interface RequestAccountChangePassword {
address: string;
oldPass: string;
newPass: string;
}
export interface RequestAccountEdit {
address: string;
genesisHash?: HexString | null;
name: string;
}
export interface RequestAccountForget {
address: string;
}
export interface RequestAccountShow {
address: string;
isShowing: boolean;
}
export interface RequestAccountTie {
address: string;
genesisHash: HexString | null;
}
export interface RequestAccountValidate {
address: string;
password: string;
}
export interface RequestDeriveCreate {
name: string;
genesisHash?: HexString | null;
suri: string;
parentAddress: string;
parentPassword: string;
password: string;
}
export interface RequestDeriveValidate {
suri: string;
parentAddress: string;
parentPassword: string;
}
export interface RequestAccountExport {
address: string;
password: string;
}
export interface RequestAccountBatchExport {
addresses: string[];
password: string;
}
export interface RequestAccountList {
anyType?: boolean;
}
export type RequestAccountSubscribe = null;
export interface RequestActiveTabsUrlUpdate {
urls: string[];
}
export interface RequestAccountUnsubscribe {
id: string;
}
export interface RequestRpcSend {
method: string;
params: unknown[];
}
export interface RequestRpcSubscribe extends RequestRpcSend {
type: string;
}
export interface RequestRpcUnsubscribe {
method: string;
subscriptionId: number | string;
type: string;
}
export interface RequestSigningApprovePassword {
id: string;
password?: string;
savePass: boolean;
}
export interface RequestSigningApproveSignature {
id: string;
signature: HexString;
signedTransaction?: HexString;
}
export interface RequestSigningCancel {
id: string;
}
export interface RequestSigningIsLocked {
id: string;
}
export interface ResponseSigningIsLocked {
isLocked: boolean;
remainingTime: number;
}
export type RequestSigningSubscribe = null;
export interface RequestSeedCreate {
length?: SeedLengths;
seed?: string;
type?: KeypairType;
}
export interface RequestSeedValidate {
suri: string;
type?: KeypairType;
}
// Responses
export type ResponseTypes = {
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][1]
};
export type ResponseType<TMessageType extends keyof RequestSignatures> = RequestSignatures[TMessageType][1];
interface TransportResponseMessageSub<TMessageType extends MessageTypesWithSubscriptions> {
error?: string;
id: string;
response?: ResponseTypes[TMessageType];
subscription?: SubscriptionMessageTypes[TMessageType];
}
interface TransportResponseMessageNoSub<TMessageType extends MessageTypesWithNoSubscriptions> {
error?: string;
id: string;
response?: ResponseTypes[TMessageType];
}
export type TransportResponseMessage<TMessageType extends MessageTypes> =
TMessageType extends MessageTypesWithNoSubscriptions
? TransportResponseMessageNoSub<TMessageType>
: TMessageType extends MessageTypesWithSubscriptions
? TransportResponseMessageSub<TMessageType>
: never;
export interface ResponseSigning {
id: string;
signature: HexString;
signedTransaction?: HexString;
}
export interface ResponseDeriveValidate {
address: string;
suri: string;
}
export interface ResponseSeedCreate {
address: string;
seed: string;
}
export interface ResponseSeedValidate {
address: string;
suri: string;
}
export interface ResponseAccountExport {
exportedJson: KeyringPair$Json;
}
export interface ResponseAccountsExport {
exportedJson: KeyringPairs$Json;
}
export type ResponseRpcListProviders = ProviderList;
// Subscriptions
export type SubscriptionMessageTypes = NoUndefinedValues<{
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][2]
}>;
export type MessageTypesWithSubscriptions = keyof SubscriptionMessageTypes;
export type MessageTypesWithNoSubscriptions = Exclude<MessageTypes, keyof SubscriptionMessageTypes>
export interface RequestSign {
readonly payload: SignerPayloadJSON | SignerPayloadRaw;
sign (registry: Registry, pair: KeyringPair): { signature: HexString };
}
export interface RequestJsonRestore {
file: KeyringPair$Json;
password: string;
}
export interface RequestBatchRestore {
file: KeyringPairs$Json;
password: string;
}
export interface ResponseJsonRestore {
error: string | null;
}
export type AllowedPath = typeof ALLOWED_PATH[number];
export interface ResponseJsonGetAccountInfo {
address: string;
name: string;
genesisHash: HexString;
type: KeypairType;
}
export interface ResponseAuthorizeList {
list: AuthUrls;
}