mirror of
https://github.com/pezkuwichain/pezkuwi-extension.git
synced 2026-07-16 07:55:45 +00:00
Update domain references to pezkuwichain.app and rebrand from polkadot
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user