Provide metadata from external source (#288)

* Provide metadata from external source

* Listing via metadata interface

* Updates

* UI update

* Storage & end-to-end
This commit is contained in:
Jaco Greeff 2020-03-26 11:01:33 +01:00 committed by GitHub
parent a2c0c8a433
commit c9f4c2e4a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 743 additions and 281 deletions

View File

@ -10,7 +10,7 @@ function copy_folder () {
cp -r $SRC $DST cp -r $SRC $DST
} }
yarn polkadot-dev-build-ts yarn build
copy_folder "extension-dapp" copy_folder "extension-dapp"
copy_folder "extension-inject" copy_folder "extension-inject"

View File

@ -24,7 +24,7 @@
"postinstall": "polkadot-dev-yarn-only", "postinstall": "polkadot-dev-yarn-only",
"start": "yarn watch", "start": "yarn watch",
"test": "polkadot-dev-run-test", "test": "polkadot-dev-run-test",
"watch": "cd packages/extension && webpack --config webpack.config.js --watch" "watch": "cd packages/extension && yarn polkadot-exec-webpack --config webpack.config.js --watch"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",

View File

@ -2,8 +2,9 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { MetadataDef } from '@polkadot/extension-inject/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { AccountJson, AuthorizeRequest, MessageTypes, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAuthorizeApprove, RequestAuthorizeReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, ResponseTypes, SigningRequest } from '../types'; import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAuthorizeApprove, RequestAuthorizeReject, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, ResponseTypes, SigningRequest } from '../types';
import extension from 'extensionizer'; import extension from 'extensionizer';
import keyring from '@polkadot/ui-keyring'; import keyring from '@polkadot/ui-keyring';
@ -108,7 +109,7 @@ export default class Extension {
// FIXME This looks very much like what we have in accounts // FIXME This looks very much like what we have in accounts
private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean { private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(authorize.subscribe)'>(id, port); const cb = createSubscription<'pri(authorize.requests)'>(id, port);
const subscription = this.#state.authSubject.subscribe((requests: AuthorizeRequest[]): void => const subscription = this.#state.authSubject.subscribe((requests: AuthorizeRequest[]): void =>
cb(requests) cb(requests)
); );
@ -121,6 +122,50 @@ export default class Extension {
return true; return true;
} }
private metadataApprove ({ id }: RequestMetadataApprove): boolean {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { resolve, request } = queued;
this.#state.saveMetadata(request);
resolve(true);
return true;
}
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 seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate { private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate {
const seed = mnemonicGenerate(length); const seed = mnemonicGenerate(length);
@ -199,7 +244,7 @@ export default class Extension {
// FIXME This looks very much like what we have in authorization // FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean { private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(signing.subscribe)'>(id, port); const cb = createSubscription<'pri(signing.requests)'>(id, port);
const subscription = this.#state.signSubject.subscribe((requests: SigningRequest[]): void => const subscription = this.#state.signSubject.subscribe((requests: SigningRequest[]): void =>
cb(requests) cb(requests)
); );
@ -228,9 +273,9 @@ export default class Extension {
return this.authorizeApprove(request as RequestAuthorizeApprove); return this.authorizeApprove(request as RequestAuthorizeApprove);
case 'pri(authorize.reject)': case 'pri(authorize.reject)':
return this.authorizeReject(request as RequestAuthorizeApprove); return this.authorizeReject(request as RequestAuthorizeReject);
case 'pri(authorize.subscribe)': case 'pri(authorize.requests)':
return this.authorizeSubscribe(id, port); return this.authorizeSubscribe(id, port);
case 'pri(accounts.create.external)': case 'pri(accounts.create.external)':
@ -251,6 +296,18 @@ export default class Extension {
case 'pri(accounts.subscribe)': case 'pri(accounts.subscribe)':
return this.accountsSubscribe(id, port); return this.accountsSubscribe(id, port);
case 'pri(metadata.approve)':
return this.metadataApprove(request as RequestMetadataApprove);
case 'pri(metadata.list)':
return this.metadataList();
case 'pri(metadata.reject)':
return this.metadataReject(request as RequestMetadataReject);
case 'pri(metadata.requests)':
return this.metadataSubscribe(id, port);
case 'pri(seed.create)': case 'pri(seed.create)':
return this.seedCreate(request as RequestSeedCreate); return this.seedCreate(request as RequestSeedCreate);
@ -266,7 +323,7 @@ export default class Extension {
case 'pri(signing.cancel)': case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel); return this.signingCancel(request as RequestSigningCancel);
case 'pri(signing.subscribe)': case 'pri(signing.requests)':
return this.signingSubscribe(id, port); return this.signingSubscribe(id, port);
case 'pri(window.open)': case 'pri(window.open)':

View File

@ -0,0 +1,55 @@
// Copyright 2019 @polkadot/extension-chains authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MetadataDef } from '@polkadot/extension-inject/types';
import extension from 'extensionizer';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type StoreValue = Record<string, any>;
const lastError = (type: string): void => {
const error = extension.runtime.lastError;
if (error) {
console.error(`ChainStore.${type}:: runtime.lastError:`, error);
}
};
export default class Store {
public all (cb: (key: string, value: MetadataDef) => void): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extension.storage.local.get(null, (result: StoreValue): void => {
lastError('all');
Object.entries(result).forEach(([key, value]): void => {
cb(key, value);
});
});
}
public get (key: string, cb: (value: MetadataDef) => void): void {
extension.storage.local.get([key], (result: StoreValue): void => {
lastError('get');
cb(result[key]);
});
}
public remove (key: string, cb?: () => void): void {
extension.storage.local.remove(key, (): void => {
lastError('remove');
cb && cb();
});
}
public set (key: string, value: MetadataDef, cb?: () => void): void {
extension.storage.local.set({ [key]: value }, (): void => {
lastError('set');
cb && cb();
});
}
}

View File

@ -2,15 +2,17 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { ProviderMeta } from '@polkadot/extension-inject/types'; import { knownMetadata, addMetadata } from '@polkadot/extension-chains';
import { ProviderMeta, MetadataDef } from '@polkadot/extension-inject/types';
import { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types'; import { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
import { AccountJson, AuthorizeRequest, MetadataRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest, RequestRpcUnsubscribe } from '../types';
import { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest, RequestRpcUnsubscribe } from '../types';
import extension from 'extensionizer'; import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { assert } from '@polkadot/util'; import { assert } from '@polkadot/util';
import MetadataStore from './MetadataStore';
interface AuthRequest { interface AuthRequest {
id: string; id: string;
idStr: string; idStr: string;
@ -28,6 +30,14 @@ type AuthUrls = Record<string, {
url: string; url: string;
}>; }>;
interface MetaRequest {
id: string;
request: MetadataDef;
resolve: (result: boolean) => void;
reject: (error: Error) => void;
url: string;
}
// List of providers passed into constructor. This is the list of providers // List of providers passed into constructor. This is the list of providers
// exposed by the extension. // exposed by the extension.
type Providers = Record<string, { type Providers = Record<string, {
@ -68,9 +78,13 @@ export default class State {
readonly #authRequests: Record<string, AuthRequest> = {}; readonly #authRequests: Record<string, AuthRequest> = {};
readonly #metaStore = new MetadataStore();
// Map of providers currently injected in tabs // Map of providers currently injected in tabs
readonly #injectedProviders: Map<chrome.runtime.Port, ProviderInterface> = new Map(); readonly #injectedProviders: Map<chrome.runtime.Port, ProviderInterface> = new Map();
readonly #metaRequests: Record<string, MetaRequest> = {};
// Map of all providers exposed by the extension, they are retrievable by key // Map of all providers exposed by the extension, they are retrievable by key
readonly #providers: Providers; readonly #providers: Providers;
@ -78,26 +92,34 @@ export default class State {
#windows: number[] = []; #windows: number[] = [];
public readonly authSubject: BehaviorSubject<AuthorizeRequest[]> = new BehaviorSubject([] as AuthorizeRequest[]); public readonly authSubject: BehaviorSubject<AuthorizeRequest[]> = new BehaviorSubject<AuthorizeRequest[]>([]);
public readonly signSubject: BehaviorSubject<SigningRequest[]> = new BehaviorSubject([] as SigningRequest[]); public readonly metaSubject: BehaviorSubject<MetadataRequest[]> = new BehaviorSubject<MetadataRequest[]>([]);
public readonly signSubject: BehaviorSubject<SigningRequest[]> = new BehaviorSubject<SigningRequest[]>([]);
constructor (providers: Providers = {}) { constructor (providers: Providers = {}) {
this.#providers = providers; this.#providers = providers;
this.#metaStore.all((key: string, def: MetadataDef): void => {
if (key.startsWith('metadata:')) {
addMetadata(def);
}
});
} }
public get hasAuthRequests (): boolean { public get knownMetadata (): MetadataDef[] {
return this.numAuthRequests === 0; return knownMetadata();
}
public get hasSignRequests (): boolean {
return this.numSignRequests === 0;
} }
public get numAuthRequests (): number { public get numAuthRequests (): number {
return Object.keys(this.#authRequests).length; return Object.keys(this.#authRequests).length;
} }
public get numMetaRequests (): number {
return Object.keys(this.#metaRequests).length;
}
public get numSignRequests (): number { public get numSignRequests (): number {
return Object.keys(this.#signRequests).length; return Object.keys(this.#signRequests).length;
} }
@ -108,6 +130,12 @@ export default class State {
.map(({ id, request, url }): AuthorizeRequest => ({ id, request, url })); .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[] { public get allSignRequests (): SigningRequest[] {
return Object return Object
.values(this.#signRequests) .values(this.#signRequests)
@ -149,6 +177,15 @@ export default class State {
}; };
} }
private metaComplete = (id: string, fn: Function): (result: boolean | Error) => void => {
return (result: boolean | Error): void => {
delete this.#metaRequests[id];
this.updateIconMeta(true);
fn(result);
};
}
private signComplete = (id: string, fn: Function): (result: ResponseSigning | Error) => void => { private signComplete = (id: string, fn: Function): (result: ResponseSigning | Error) => void => {
return (result: ResponseSigning | Error): void => { return (result: ResponseSigning | Error): void => {
delete this.#signRequests[id]; delete this.#signRequests[id];
@ -168,11 +205,14 @@ export default class State {
private updateIcon (shouldClose?: boolean): void { private updateIcon (shouldClose?: boolean): void {
const authCount = this.numAuthRequests; const authCount = this.numAuthRequests;
const metaCount = this.numMetaRequests;
const signCount = this.numSignRequests; const signCount = this.numSignRequests;
const text = ( const text = (
authCount authCount
? 'Auth' ? 'Auth'
: (signCount ? `${signCount}` : '') : metaCount
? 'Meta'
: (signCount ? `${signCount}` : '')
); );
extension.browserAction.setBadgeText({ text }); extension.browserAction.setBadgeText({ text });
@ -187,6 +227,11 @@ export default class State {
this.updateIcon(shouldClose); this.updateIcon(shouldClose);
} }
private updateIconMeta (shouldClose?: boolean): void {
this.metaSubject.next(this.allMetaRequests);
this.updateIcon(shouldClose);
}
private updateIconSign (shouldClose?: boolean): void { private updateIconSign (shouldClose?: boolean): void {
this.signSubject.next(this.allSignRequests); this.signSubject.next(this.allSignRequests);
this.updateIcon(shouldClose); this.updateIcon(shouldClose);
@ -198,7 +243,7 @@ export default class State {
if (this.#authUrls[idStr]) { if (this.#authUrls[idStr]) {
assert(this.#authUrls[idStr].isAllowed, `The source ${url} is not allowed to interact with this extension`); assert(this.#authUrls[idStr].isAllowed, `The source ${url} is not allowed to interact with this extension`);
return true; return false;
} }
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
@ -227,10 +272,31 @@ export default class State {
return true; return true;
} }
public injectMetadata (url: string, request: MetadataDef): Promise<boolean> {
return new Promise((resolve, reject): void => {
const id = getId();
this.#metaRequests[id] = {
id,
request,
resolve: this.metaComplete(id, resolve),
reject: this.metaComplete(id, reject),
url
};
this.updateIconMeta();
this.popupOpen();
});
}
public getAuthRequest (id: string): AuthRequest { public getAuthRequest (id: string): AuthRequest {
return this.#authRequests[id]; return this.#authRequests[id];
} }
public getMetaRequest (id: string): MetaRequest {
return this.#metaRequests[id];
}
public getSignRequest (id: string): SignRequest { public getSignRequest (id: string): SignRequest {
return this.#signRequests[id]; return this.#signRequests[id];
} }
@ -303,6 +369,12 @@ export default class State {
return provider.unsubscribe(request.type, request.method, request.subscriptionId); return provider.unsubscribe(request.type, request.method, request.subscriptionId);
} }
public saveMetadata (meta: MetadataDef): void {
this.#metaStore.set(`metadata:${meta.genesisHash}`, meta);
addMetadata(meta);
}
public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> { public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> {
const id = getId(); const id = getId();

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { InjectedAccount, ProviderMeta } from '@polkadot/extension-inject/types'; import { InjectedAccount, InjectedMetadataKnown, MetadataDef, ProviderMeta } from '@polkadot/extension-inject/types';
import { KeyringPair } from '@polkadot/keyring/types'; import { KeyringPair } from '@polkadot/keyring/types';
import { JsonRpcResponse } from '@polkadot/rpc-provider/types'; import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
@ -12,9 +12,9 @@ import { RequestAuthorizeTab, ResponseSigning, RequestTypes, ResponseTypes, Mess
import keyring from '@polkadot/ui-keyring'; import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts'; import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert } from '@polkadot/util'; import { assert } from '@polkadot/util';
import RequestBytesSign from '../RequestBytesSign'; import RequestBytesSign from '../RequestBytesSign';
import RequestExtrinsicSign from '../RequestExtrinsicSign'; import RequestExtrinsicSign from '../RequestExtrinsicSign';
import State from './State'; import State from './State';
import { createSubscription, unsubscribe } from './subscriptions'; import { createSubscription, unsubscribe } from './subscriptions';
@ -57,22 +57,38 @@ export default class Tabs {
private getSigningPair (address: string): KeyringPair { private getSigningPair (address: string): KeyringPair {
const pair = keyring.getPair(address); const pair = keyring.getPair(address);
assert(pair, 'Unable to find keypair'); assert(pair, 'Unable to find keypair');
return pair; return pair;
} }
private bytesSign (url: string, request: SignerPayloadRaw): Promise<ResponseSigning> { private bytesSign (url: string, request: SignerPayloadRaw): Promise<ResponseSigning> {
const address = request.address; const address = request.address;
const pair = this.getSigningPair(address); const pair = this.getSigningPair(address);
return this.#state.sign(url, new RequestBytesSign(request), { address, ...pair.meta }); return this.#state.sign(url, new RequestBytesSign(request), { address, ...pair.meta });
} }
private extrinsicSign (url: string, request: SignerPayloadJSON): Promise<ResponseSigning> { private extrinsicSign (url: string, request: SignerPayloadJSON): Promise<ResponseSigning> {
const address = request.address; const address = request.address;
const pair = this.getSigningPair(address); const pair = this.getSigningPair(address);
return this.#state.sign(url, new RequestExtrinsicSign(request), { address, ...pair.meta }); 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);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private metadataList (url: string): InjectedMetadataKnown[] {
return this.#state.knownMetadata.map(({ genesisHash, specVersion }) => ({
genesisHash,
specVersion
}));
}
private rpcListProviders (): Promise<ResponseRpcListProviders> { private rpcListProviders (): Promise<ResponseRpcListProviders> {
return this.#state.rpcListProviders(); return this.#state.rpcListProviders();
} }
@ -135,6 +151,12 @@ export default class Tabs {
case 'pub(extrinsic.sign)': case 'pub(extrinsic.sign)':
return this.extrinsicSign(url, request as SignerPayloadJSON); 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(rpc.listProviders)': case 'pub(rpc.listProviders)':
return this.rpcListProviders(); return this.rpcListProviders();

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { InjectedAccount, ProviderList, ProviderMeta } from '@polkadot/extension-inject/types'; import { InjectedAccount, MetadataDef, ProviderList, ProviderMeta, InjectedMetadataKnown } from '@polkadot/extension-inject/types';
import { JsonRpcResponse } from '@polkadot/rpc-provider/types'; import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
import { KeypairType } from '@polkadot/util-crypto/types'; import { KeypairType } from '@polkadot/util-crypto/types';
import { KeyringPair } from '@polkadot/keyring/types'; import { KeyringPair } from '@polkadot/keyring/types';
@ -36,6 +36,12 @@ export interface AuthorizeRequest {
url: string; url: string;
} }
export interface MetadataRequest {
id: string;
request: MetadataDef;
url: string;
}
export interface SigningRequest { export interface SigningRequest {
account: AccountJson; account: AccountJson;
id: string; id: string;
@ -54,13 +60,17 @@ export interface RequestSignatures {
'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]]; 'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]];
'pri(authorize.approve)': [RequestAuthorizeApprove, boolean]; 'pri(authorize.approve)': [RequestAuthorizeApprove, boolean];
'pri(authorize.reject)': [RequestAuthorizeReject, boolean]; 'pri(authorize.reject)': [RequestAuthorizeReject, boolean];
'pri(authorize.subscribe)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]]; 'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
'pri(metadata.reject)': [RequestMetadataReject, boolean];
'pri(metadata.requests)': [RequestMetadataSubscribe, boolean, MetadataRequest[]];
'pri(metadata.list)': [null, MetadataDef[]];
'pri(seed.create)': [RequestSeedCreate, ResponseSeedCreate]; 'pri(seed.create)': [RequestSeedCreate, ResponseSeedCreate];
'pri(seed.validate)': [RequestSeedValidate, ResponseSeedValidate]; 'pri(seed.validate)': [RequestSeedValidate, ResponseSeedValidate];
'pri(signing.approve.password)': [RequestSigningApprovePassword, boolean]; 'pri(signing.approve.password)': [RequestSigningApprovePassword, boolean];
'pri(signing.approve.signature)': [RequestSigningApproveSignature, boolean]; 'pri(signing.approve.signature)': [RequestSigningApproveSignature, boolean];
'pri(signing.cancel)': [RequestSigningCancel, boolean]; 'pri(signing.cancel)': [RequestSigningCancel, boolean];
'pri(signing.subscribe)': [RequestSigningSubscribe, boolean, SigningRequest[]]; 'pri(signing.requests)': [RequestSigningSubscribe, boolean, SigningRequest[]];
'pri(window.open)': [null, boolean]; 'pri(window.open)': [null, boolean];
// public/external requests, i.e. from a page // public/external requests, i.e. from a page
'pub(accounts.list)': [RequestAccountList, InjectedAccount[]]; 'pub(accounts.list)': [RequestAccountList, InjectedAccount[]];
@ -68,6 +78,8 @@ export interface RequestSignatures {
'pub(authorize.tab)': [RequestAuthorizeTab, null]; 'pub(authorize.tab)': [RequestAuthorizeTab, null];
'pub(bytes.sign)': [SignerPayloadRaw, ResponseSigning]; 'pub(bytes.sign)': [SignerPayloadRaw, ResponseSigning];
'pub(extrinsic.sign)': [SignerPayloadJSON, ResponseSigning]; 'pub(extrinsic.sign)': [SignerPayloadJSON, ResponseSigning];
'pub(metadata.list)': [null, InjectedMetadataKnown[]];
'pub(metadata.provide)': [MetadataDef, boolean];
'pub(rpc.listProviders)': [void, ResponseRpcListProviders]; 'pub(rpc.listProviders)': [void, ResponseRpcListProviders];
'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse]; 'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse];
'pub(rpc.startProvider)': [string, ProviderMeta]; 'pub(rpc.startProvider)': [string, ProviderMeta];
@ -107,6 +119,16 @@ export interface RequestAuthorizeReject {
export type RequestAuthorizeSubscribe = null; export type RequestAuthorizeSubscribe = null;
export interface RequestMetadataApprove {
id: string;
}
export interface RequestMetadataReject {
id: string;
}
export type RequestMetadataSubscribe = null;
export interface RequestAccountCreateExternal { export interface RequestAccountCreateExternal {
address: string; address: string;
genesisHash?: string | null; genesisHash?: string | null;

View File

@ -5,6 +5,7 @@
import { InjectedAccounts, InjectedAccount, Unsubcall } from '@polkadot/extension-inject/types'; import { InjectedAccounts, InjectedAccount, Unsubcall } from '@polkadot/extension-inject/types';
import { SendRequest } from './types'; import { SendRequest } from './types';
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest; let sendRequest: SendRequest;
export default class Accounts implements InjectedAccounts { export default class Accounts implements InjectedAccounts {

View File

@ -7,17 +7,21 @@ import { Injected } from '@polkadot/extension-inject/types';
import { SendRequest } from './types'; import { SendRequest } from './types';
import Accounts from './Accounts'; import Accounts from './Accounts';
import Metadata from './Metadata';
import Signer from './Signer'; import Signer from './Signer';
export default class implements Injected { export default class implements Injected {
public readonly accounts: Accounts; public readonly accounts: Accounts;
public readonly metadata: Metadata;
public readonly provider: PostMessageProvider; public readonly provider: PostMessageProvider;
public readonly signer: Signer; public readonly signer: Signer;
constructor (sendRequest: SendRequest) { constructor (sendRequest: SendRequest) {
this.accounts = new Accounts(sendRequest); this.accounts = new Accounts(sendRequest);
this.metadata = new Metadata(sendRequest);
this.provider = new PostMessageProvider(sendRequest); this.provider = new PostMessageProvider(sendRequest);
this.signer = new Signer(sendRequest); this.signer = new Signer(sendRequest);
} }

View File

@ -0,0 +1,23 @@
// Copyright 2019-2020 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { InjectedMetadata, InjectedMetadataKnown, MetadataDef } from '@polkadot/extension-inject/types';
import { SendRequest } from './types';
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest;
export default class Metadata implements InjectedMetadata {
constructor (_sendRequest: SendRequest) {
sendRequest = _sendRequest;
}
public get (): Promise<InjectedMetadataKnown[]> {
return sendRequest('pub(metadata.list)');
}
public provide (definition: MetadataDef): Promise<boolean> {
return sendRequest('pub(metadata.provide)', definition);
}
}

View File

@ -21,6 +21,9 @@ interface SubscriptionHandler {
type: string; type: string;
} }
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest;
/** /**
* @name PostMessageProvider * @name PostMessageProvider
* *
@ -32,8 +35,6 @@ export default class PostMessageProvider implements InjectedProvider {
// Whether or not the actual extension background provider is connected // Whether or not the actual extension background provider is connected
#isConnected = false #isConnected = false
readonly #sendRequest: SendRequest;
// Subscription IDs are (historically) not guaranteed to be globally unique; // Subscription IDs are (historically) not guaranteed to be globally unique;
// only unique for a given subscription method; which is why we identify // only unique for a given subscription method; which is why we identify
// the subscriptions based on subscription id + type // the subscriptions based on subscription id + type
@ -43,16 +44,17 @@ export default class PostMessageProvider implements InjectedProvider {
* @param {function} sendRequest The function to be called to send requests to the node * @param {function} sendRequest The function to be called to send requests to the node
* @param {function} subscriptionNotificationHandler Channel for receiving subscription messages * @param {function} subscriptionNotificationHandler Channel for receiving subscription messages
*/ */
public constructor (sendRequest: SendRequest) { public constructor (_sendRequest: SendRequest) {
this.#eventemitter = new EventEmitter(); this.#eventemitter = new EventEmitter();
this.#sendRequest = sendRequest;
sendRequest = _sendRequest;
} }
/** /**
* @description Returns a clone of the object * @description Returns a clone of the object
*/ */
public clone (): PostMessageProvider { public clone (): PostMessageProvider {
return new PostMessageProvider(this.#sendRequest); return new PostMessageProvider(sendRequest);
} }
/** /**
@ -80,7 +82,7 @@ export default class PostMessageProvider implements InjectedProvider {
} }
public listProviders (): Promise<ProviderList> { public listProviders (): Promise<ProviderList> {
return this.#sendRequest('pub(rpc.listProviders)', undefined); return sendRequest('pub(rpc.listProviders)', undefined);
} }
/** /**
@ -102,7 +104,7 @@ export default class PostMessageProvider implements InjectedProvider {
if (subscription) { if (subscription) {
const { callback, type } = subscription; const { callback, type } = subscription;
return this.#sendRequest( return sendRequest(
'pub(rpc.subscribe)', 'pub(rpc.subscribe)',
{ type, method, params }, { type, method, params },
(res) => { (res) => {
@ -114,7 +116,7 @@ export default class PostMessageProvider implements InjectedProvider {
return id; return id;
}); });
} else { } else {
return this.#sendRequest('pub(rpc.send)', { method, params }); return sendRequest('pub(rpc.send)', { method, params });
} }
} }
@ -126,9 +128,9 @@ export default class PostMessageProvider implements InjectedProvider {
this.#isConnected = false; this.#isConnected = false;
this.#eventemitter.emit('disconnected'); this.#eventemitter.emit('disconnected');
const meta = await this.#sendRequest('pub(rpc.startProvider)', key); const meta = await sendRequest('pub(rpc.startProvider)', key);
this.#sendRequest('pub(rpc.subscribeConnected)', null, (connected) => { sendRequest('pub(rpc.subscribeConnected)', null, (connected) => {
this.#isConnected = connected; this.#isConnected = connected;
if (connected) { if (connected) {

View File

@ -6,14 +6,12 @@ import { Signer as SignerInterface, SignerResult } from '@polkadot/api/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { SendRequest } from './types'; import { SendRequest } from './types';
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest; let sendRequest: SendRequest;
let nextId = 0; let nextId = 0;
export default class Signer implements SignerInterface { export default class Signer implements SignerInterface {
constructor (_sendRequest: SendRequest) { constructor (_sendRequest: SendRequest) {
// NOTE We are storing the `sendRequest` as a global to this class, since we don't
// have private members in JS, we err on the side of caution and hide it away so it
// is only available internally to the class.
sendRequest = _sendRequest; sendRequest = _sendRequest;
} }

View File

@ -5,8 +5,7 @@
"author": "Jaco Greeff <jacogr@gmail.com>", "author": "Jaco Greeff <jacogr@gmail.com>",
"license": "Apache-2", "license": "Apache-2",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.9.2", "@babel/runtime": "^7.9.2"
"edgeware-node-types": "^1.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"@polkadot/api": "*" "@polkadot/api": "*"

File diff suppressed because one or more lines are too long

View File

@ -2,21 +2,21 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { Chain, ChainDef } from './types'; import { MetadataDef } from '@polkadot/extension-inject/types';
import { Chain } from './types';
import { Metadata, TypeRegistry } from '@polkadot/types'; import { Metadata, TypeRegistry } from '@polkadot/types';
// imports chain details, generally metadata. For the generation of these, // imports chain details, generally metadata. For the generation of these,
// inside the api, run `yarn chain:info --ws <url>` // inside the api, run `yarn chain:info --ws <url>`
import edgeware from './edgeware';
import kusama from './kusama'; import kusama from './kusama';
const chains: Map<string, Chain> = new Map( const definitions: Map<string, MetadataDef> = new Map(
// eslint-disable-next-line @typescript-eslint/no-use-before-define [kusama].map((def) => [def.genesisHash, def])
[edgeware, kusama].map((def) => defToChain(def))
); );
function defToChain ({ chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types }: ChainDef): [string, Chain] { function metadataExpand (definition: MetadataDef): Chain {
const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types } = definition;
const registry = new TypeRegistry(); const registry = new TypeRegistry();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -33,7 +33,8 @@ function defToChain ({ chain, genesisHash, icon, metaCalls, specVersion, ss58For
tokenSymbol tokenSymbol
})); }));
return [genesisHash, { return {
definition,
genesisHash: isUnknown genesisHash: isUnknown
? undefined ? undefined
: genesisHash, : genesisHash,
@ -46,26 +47,21 @@ function defToChain ({ chain, genesisHash, icon, metaCalls, specVersion, ss58For
ss58Format, ss58Format,
tokenDecimals, tokenDecimals,
tokenSymbol tokenSymbol
}]; };
} }
const [, UNKNOWN_CHAIN] = defToChain({ export function findChain (definitions: MetadataDef[], genesisHash?: string | null): Chain | null {
chain: 'Unknown chain', const def = definitions.find((def) => def.genesisHash === genesisHash);
genesisHash: '0x',
icon: 'polkadot',
specVersion: 0,
ss58Format: 42,
tokenDecimals: 0,
tokenSymbol: 'Unit',
types: {}
});
export function addChainDef (def: ChainDef): void { return def
const [genesisHash, chain] = defToChain(def); ? metadataExpand(def)
: null;
chains.set(genesisHash, chain);
} }
export default function findChain (genesisHash?: string | null): Chain { export function addMetadata (def: MetadataDef): void {
return chains.get(genesisHash || '') || UNKNOWN_CHAIN; definitions.set(def.genesisHash, def);
}
export function knownMetadata (): MetadataDef[] {
return [...definitions.values()];
} }

View File

@ -2,9 +2,11 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { MetadataDef } from '@polkadot/extension-inject/types';
import { Registry } from '@polkadot/types/types'; import { Registry } from '@polkadot/types/types';
export interface Chain { export interface Chain {
definition: MetadataDef;
genesisHash?: string; genesisHash?: string;
hasMetadata: boolean; hasMetadata: boolean;
icon: string; icon: string;
@ -16,15 +18,3 @@ export interface Chain {
tokenDecimals: number; tokenDecimals: number;
tokenSymbol: string; tokenSymbol: string;
} }
export interface ChainDef {
chain: string;
genesisHash: string;
icon: string;
specVersion: number;
ss58Format: number;
tokenDecimals: number;
tokenSymbol: string;
types: Record<string, object | string>;
metaCalls?: string;
}

View File

@ -30,6 +30,7 @@ export interface InjectedAccounts {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe: (cb: (accounts: InjectedAccount[]) => any) => Unsubcall; subscribe: (cb: (accounts: InjectedAccount[]) => any) => Unsubcall;
} }
export interface InjectedExtensionInfo { export interface InjectedExtensionInfo {
name: string; name: string;
version: string; version: string;
@ -47,6 +48,28 @@ export interface ProviderMeta {
transport: string; transport: string;
} }
export interface MetadataDef {
chain: string;
genesisHash: string;
icon: string;
specVersion: number;
ss58Format: number;
tokenDecimals: number;
tokenSymbol: string;
types: Record<string, object | string>;
metaCalls?: string;
}
export interface InjectedMetadataKnown {
genesisHash: string;
specVersion: number;
}
export interface InjectedMetadata {
get: () => Promise<InjectedMetadataKnown[]>;
provide: (definition: MetadataDef) => Promise<boolean>;
}
export type ProviderList = Record<string, ProviderMeta> export type ProviderList = Record<string, ProviderMeta>
export interface InjectedProvider extends ProviderInterface { export interface InjectedProvider extends ProviderInterface {
@ -62,8 +85,9 @@ export interface InjectedProviderWithMeta {
export interface Injected { export interface Injected {
accounts: InjectedAccounts; accounts: InjectedAccounts;
signer: InjectedSigner; metadata?: InjectedMetadata;
provider?: InjectedProvider; provider?: InjectedProvider;
signer: InjectedSigner;
} }
export interface InjectedWindowProvider { export interface InjectedWindowProvider {

View File

@ -6,7 +6,7 @@ import React from 'react';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
import { AuthorizeContext, themes, Header, Icon } from '@polkadot/extension-ui/components'; import { AuthorizeReqContext, themes, Header, Icon } from '@polkadot/extension-ui/components';
import Authorize from '.'; import Authorize from '.';
import { configure, mount, ReactWrapper } from 'enzyme'; import { configure, mount, ReactWrapper } from 'enzyme';
import { AuthorizeRequest } from '@polkadot/extension-base/background/types'; import { AuthorizeRequest } from '@polkadot/extension-base/background/types';
@ -16,11 +16,11 @@ configure({ adapter: new Adapter() });
describe('Authorize', () => { describe('Authorize', () => {
const mountAuthorize = (authorizeRequests: AuthorizeRequest[] = []): ReactWrapper => mount( const mountAuthorize = (authorizeRequests: AuthorizeRequest[] = []): ReactWrapper => mount(
<AuthorizeContext.Provider value={authorizeRequests}> <AuthorizeReqContext.Provider value={authorizeRequests}>
<ThemeProvider theme={themes.dark}> <ThemeProvider theme={themes.dark}>
<Authorize /> <Authorize />
</ThemeProvider> </ThemeProvider>
</AuthorizeContext.Provider>); </AuthorizeReqContext.Provider>);
it('render component', () => { it('render component', () => {
const wrapper = mountAuthorize(); const wrapper = mountAuthorize();

View File

@ -38,8 +38,8 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
An application, self-identifying as <span className='tab-name'>{origin}</span> is requesting access from{' '} An application, self-identifying as <span className='tab-name'>{origin}</span> is requesting access from{' '}
<a <a
href={url} href={url}
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
> >
<span className='tab-url'>{url}</span> <span className='tab-url'>{url}</span>
</a>. </a>.

View File

@ -4,17 +4,17 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { AuthorizeContext, Header } from '../../components'; import { AuthorizeReqContext, Header } from '../../components';
import Request from './Request'; import Request from './Request';
import styled from 'styled-components'; import styled from 'styled-components';
export default function Authorize (): React.ReactElement<{}> { export default function Authorize (): React.ReactElement<{}> {
const requests = useContext(AuthorizeContext); const requests = useContext(AuthorizeReqContext);
return ( return (
<> <>
<Scroll isLastRequest={requests.length === 1}> <Scroll isLastRequest={requests.length === 1}>
<Header text='Authorize'/> <Header text='Authorize' />
{requests.map(({ id, request, url }, index): React.ReactNode => ( {requests.map(({ id, request, url }, index): React.ReactNode => (
<Request <Request
authId={id} authId={id}

View File

@ -0,0 +1,125 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MetadataDef } from '@polkadot/extension-inject/types';
import React, { useContext } from 'react';
import styled from 'styled-components';
import { ActionBar, ActionContext, Button, Link, Table, Warning } from '../../components';
import useMetadata from '../../hooks/useMetadata';
import { approveMetaRequest, rejectMetaRequest } from '../../messaging';
interface Props {
className?: string;
request: MetadataDef;
metaId: string;
url: string;
}
function Request ({ className, metaId, request, url }: Props): React.ReactElement<Props> {
const chain = useMetadata(request.genesisHash);
const onAction = useContext(ActionContext);
const _onApprove = (): Promise<void> =>
approveMetaRequest(metaId)
.then((): void => onAction())
.catch((error: Error) => console.error(error));
const _onReject = (): Promise<void> =>
rejectMetaRequest(metaId)
.then((): void => onAction())
.catch((error: Error) => console.error(error));
return (
<div className={className}>
<Table>
<tr>
<td className='label'>from</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>chain</td>
<td className='data'>{request.chain}</td>
</tr>
<tr>
<td className='label'>icon</td>
<td className='data'>{request.icon}</td>
</tr>
<tr>
<td className='label'>decimals</td>
<td className='data'>{request.tokenDecimals}</td>
</tr>
<tr>
<td className='label'>symbol</td>
<td className='data'>{request.tokenSymbol}</td>
</tr>
<tr>
<td className='label'>upgrade</td>
<td className='data'>{chain ? chain.specVersion : 'unknown'} -&gt; {request.specVersion}</td>
</tr>
</Table>
<RequestInfo>
<RequestWarning>This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.</RequestWarning>
<AcceptButton onClick={_onApprove}>Yes, do this metadata update</AcceptButton>
<RejectButton>
<Link isDanger onClick={_onReject}>Reject</Link>
</RejectButton>
</RequestInfo>
</div>
);
}
const RequestInfo = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
background: ${({ theme }): string => theme.highlightedAreaBackground};
`;
const AcceptButton = styled(Button)`
width: 90%;
margin: 25px auto 0;
`;
const RequestWarning = styled(Warning)`
margin: 24px 24px 0 1.45rem;
`;
AcceptButton.displayName = 'AcceptButton';
const RejectButton = styled(ActionBar)`
margin: 8px 0 15px 0;
text-decoration: underline;
`;
export default styled(Request)`
.icon {
background: ${({ theme }): string => theme.buttonBackgroundDanger};
color: white;
min-width: 18px;
width: 14px;
height: 18px;
font-size: 10px;
line-height: 20px;
margin: 16px 15px 0 1.35rem;
font-weight: 800;
padding-left: 0.5px;
}
.tab-info {
overflow: hidden;
margin: 0.75rem 20px 0 0;
}
.tab-name,
.tab-url {
color: ${({ theme }): string => theme.textColor};
display: inline-block;
max-width: 20rem;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
cursor: pointer;
text-decoration: underline;
}
`;

View File

@ -0,0 +1,29 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext } from 'react';
import { Header, Loading, MetadataReqContext } from '../../components';
import Request from './Request';
export default function Metadata (): React.ReactElement<{}> {
const requests = useContext(MetadataReqContext);
return (
<>
<Header text='Metadata' />
{requests[0]
? (
<Request
key={requests[0].id}
metaId={requests[0].id}
request={requests[0].request}
url={requests[0].url}
/>
)
: <Loading />
}
</>
);
}

View File

@ -8,10 +8,11 @@ import { AnyJson, SignerPayloadJSON } from '@polkadot/types/types';
import BN from 'bn.js'; import BN from 'bn.js';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import findChain from '@polkadot/extension-chains';
import { formatNumber, bnToBn } from '@polkadot/util'; import { formatNumber, bnToBn } from '@polkadot/util';
import { Table } from '../../components';
import useMetadata from '../../hooks/useMetadata';
interface Decoded { interface Decoded {
args: AnyJson | null; args: AnyJson | null;
method: Call | null; method: Call | null;
@ -25,12 +26,12 @@ interface Props {
url: string; url: string;
} }
function decodeMethod (data: string, isDecoded: boolean, chain: Chain, specVersion: BN): Decoded { function decodeMethod (data: string, isDecoded: boolean, chain: Chain | null, specVersion: BN): Decoded {
let args: AnyJson | null = null; let args: AnyJson | null = null;
let method: Call | null = null; let method: Call | null = null;
try { try {
if (isDecoded && chain.hasMetadata && specVersion.eqn(chain.specVersion)) { if (isDecoded && chain && chain.hasMetadata && specVersion.eqn(chain.specVersion)) {
method = chain.registry.createType('Call', data); method = chain.registry.createType('Call', data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
args = (method.toHuman() as any).args; args = (method.toHuman() as any).args;
@ -94,7 +95,7 @@ function mortalityAsString (era: ExtrinsicEra, hexBlockNumber: string): string {
} }
function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> { function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> {
const chain = useRef(findChain(genesisHash)).current; const chain = useMetadata(genesisHash);
const specVersion = useRef(bnToBn(hexSpec)).current; const specVersion = useRef(bnToBn(hexSpec)).current;
const [decoded, setDecoded] = useState<Decoded>({ args: null, method: null }); const [decoded, setDecoded] = useState<Decoded>({ args: null, method: null });
@ -103,84 +104,36 @@ function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, reques
}, [method, isDecoded, chain, specVersion]); }, [method, isDecoded, chain, specVersion]);
return ( return (
<table className={className}> <Table className={className} isFull>
<tbody> <tr>
<td className='label'>from</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>{chain ? 'chain' : 'genesis'}</td>
<td className='data'>{chain ? chain.name : genesisHash}</td>
</tr>
<tr>
<td className='label'>version</td>
<td className='data'>{specVersion.toNumber()}</td>
</tr>
<tr>
<td className='label'>nonce</td>
<td className='data'>{formatNumber(nonce)}</td>
</tr>
{!tip.isEmpty && (
<tr> <tr>
<td className='label'>from</td> <td className='label'>tip</td>
<td className='data'>{url}</td> <td className='data'>{formatNumber(tip)}</td>
</tr> </tr>
<tr> )}
<td className='label'>{chain.isUnknown ? 'genesis' : 'chain'}</td> {renderMethod(method, decoded)}
<td className='data'>{chain.isUnknown ? genesisHash : chain.name}</td> <tr>
</tr> <td className='label'>lifetime</td>
<tr> <td className='data'>{mortalityAsString(era, blockNumber)}</td>
<td className='label'>version</td> </tr>
<td className='data'>{specVersion.toNumber()}</td> </Table>
</tr>
<tr>
<td className='label'>nonce</td>
<td className='data'>{formatNumber(nonce)}</td>
</tr>
{!tip.isEmpty && (
<tr>
<td className='label'>tip</td>
<td className='data'>{formatNumber(tip)}</td>
</tr>
)}
{renderMethod(method, decoded)}
<tr>
<td className='label'>lifetime</td>
<td className='data'>{mortalityAsString(era, blockNumber)}</td>
</tr>
</tbody>
</table>
); );
} }
export default styled(Extrinsic)` export default React.memo(Extrinsic);
height: 100%;
overflow: scroll;
display: block;
border: 0;
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
td.data {
max-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
width: 100%;
pre {
font-family: inherit;
font-size: 0.75rem;
margin: 0;
}
}
td.label {
opacity: 0.5;
padding: 0 0.5rem;
text-align: right;
vertical-align: top;
white-space: nowrap;
}
details {
cursor: pointer;
max-width: 24rem;
&[open] summary {
white-space: normal;
}
summary {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
outline: 0;
}
}
`;

View File

@ -36,6 +36,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
useEffect((): void => { useEffect((): void => {
const inner = request.inner; const inner = request.inner;
if ((inner as SignerPayloadRaw).data) { if ((inner as SignerPayloadRaw).data) {
setHexBytes((inner as SignerPayloadRaw).data); setHexBytes((inner as SignerPayloadRaw).data);
setExtrinsic(null); setExtrinsic(null);
@ -45,10 +46,10 @@ export default function Request ({ account: { isExternal }, request, signId, url
} }
}, [request]); }, [request]);
const _onCancel = (): Promise<void> => { const _onCancel = (): Promise<void> =>
return cancelSignRequest(signId).then((): void => onAction()) cancelSignRequest(signId)
.then((): void => onAction())
.catch((error: Error) => console.error(error)); .catch((error: Error) => console.error(error));
};
const _onSign = (password: string): Promise<void> => const _onSign = (password: string): Promise<void> =>
approveSignPassword(signId, password) approveSignPassword(signId, password)
.then((): void => onAction()) .then((): void => onAction())
@ -60,6 +61,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
if (extrinsic !== null) { if (extrinsic !== null) {
const payload = request.inner as SignerPayloadJSON; const payload = request.inner as SignerPayloadJSON;
return ( return (
<> <>
<Address <Address
@ -92,6 +94,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
); );
} else if (hexBytes !== null) { } else if (hexBytes !== null) {
const payload = request.inner as SignerPayloadRaw; const payload = request.inner as SignerPayloadRaw;
return ( return (
<> <>
<Address address={payload.address} /> <Address address={payload.address} />
@ -105,9 +108,9 @@ export default function Request ({ account: { isExternal }, request, signId, url
</SignArea> </SignArea>
</> </>
); );
} else {
return null;
} }
return null;
} }
const SignArea = styled(ButtonArea)` const SignArea = styled(ButtonArea)`

View File

@ -10,15 +10,7 @@ import { act } from 'react-dom/test-utils';
import * as messaging from '@polkadot/extension-ui/messaging'; import * as messaging from '@polkadot/extension-ui/messaging';
import { AccountJson, SigningRequest } from '@polkadot/extension-base/background/types'; import { AccountJson, SigningRequest } from '@polkadot/extension-base/background/types';
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers'; import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
import { import { AccountContext, ActionContext, Address, Button, Input, SigningReqContext, themes } from '@polkadot/extension-ui/components';
AccountContext,
ActionContext,
themes,
SigningContext,
Address,
Input,
Button
} from '@polkadot/extension-ui/components';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
import Signing from '.'; import Signing from '.';
import TransactionIndex from './TransactionIndex'; import TransactionIndex from './TransactionIndex';
@ -52,9 +44,9 @@ describe('Signing requests', () => {
emitter.on('request', setRequests); emitter.on('request', setRequests);
return ( return (
<SigningContext.Provider value={requests}> <SigningReqContext.Provider value={requests}>
<Signing /> <Signing />
</SigningContext.Provider> </SigningReqContext.Provider>
); );
} }
const mountComponent = async (): Promise<void> => { const mountComponent = async (): Promise<void> => {

View File

@ -2,16 +2,24 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Header, Loading, SigningContext } from '../../components'; import { Header, Loading, SigningReqContext } from '../../components';
import Request from './Request'; import Request from './Request';
import TransactionIndex from './TransactionIndex'; import TransactionIndex from './TransactionIndex';
import { SigningRequest } from '@polkadot/extension-base/background/types'; import { SigningRequest } from '@polkadot/extension-base/background/types';
function isInnerTransaction (signingRequest: SigningRequest): boolean {
return 'blockNumber' in signingRequest.request.inner;
}
export default function Signing (): React.ReactElement<{}> { export default function Signing (): React.ReactElement<{}> {
const requests = useContext(SigningContext); const requests = useContext(SigningReqContext);
const [requestIndex, setRequestIndex] = useState(0); const [requestIndex, setRequestIndex] = useState(0);
const isTransaction = useMemo(
() => isInnerTransaction(requests[requestIndex]),
[requests, requestIndex]
);
useEffect(() => { useEffect(() => {
if (requestIndex >= requests.length) { if (requestIndex >= requests.length) {
@ -19,13 +27,9 @@ export default function Signing (): React.ReactElement<{}> {
} }
}, [requests]); }, [requests]);
function isTransaction (signingRequest: SigningRequest): boolean {
return 'blockNumber' in signingRequest.request.inner;
}
return requests[requestIndex] ? ( return requests[requestIndex] ? (
<> <>
<Header text={isTransaction(requests[requestIndex]) ? 'Transaction' : 'Sign message'}> <Header text={isTransaction ? 'Transaction' : 'Sign message'}>
{requests.length > 1 && ( {requests.length > 1 && (
<TransactionIndex <TransactionIndex
index={requestIndex} index={requestIndex}
@ -41,7 +45,7 @@ export default function Signing (): React.ReactElement<{}> {
request={requests[requestIndex].request} request={requests[requestIndex].request}
signId={requests[requestIndex].id} signId={requests[requestIndex].id}
url={requests[requestIndex].url} url={requests[requestIndex].url}
buttonText={isTransaction(requests[requestIndex]) ? 'Sign the transaction' : 'Sign the message'} buttonText={isTransaction ? 'Sign the transaction' : 'Sign the message'}
/> />
</> </>
) : <Loading />; ) : <Loading />;

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AccountJson, AuthorizeRequest, SigningRequest } from '@polkadot/extension-base/background/types'; import { AccountJson, AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router'; import { Route, Switch } from 'react-router';
@ -10,8 +10,8 @@ import settings from '@polkadot/ui-settings';
import { setSS58Format } from '@polkadot/util-crypto'; import { setSS58Format } from '@polkadot/util-crypto';
import { Loading } from '../components'; import { Loading } from '../components';
import { AccountContext, ActionContext, AuthorizeContext, MediaContext, SigningContext } from '../components/contexts'; import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SigningReqContext } from '../components/contexts';
import { subscribeAccounts, subscribeAuthorize, subscribeSigning } from '../messaging'; import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
import Accounts from './Accounts'; import Accounts from './Accounts';
import Authorize from './Authorize'; import Authorize from './Authorize';
import CreateAccount from './CreateAccount'; import CreateAccount from './CreateAccount';
@ -19,6 +19,7 @@ import Export from './Export';
import Forget from './Forget'; import Forget from './Forget';
import ImportQr from './ImportQr'; import ImportQr from './ImportQr';
import ImportSeed from './ImportSeed'; import ImportSeed from './ImportSeed';
import Metadata from './Metadata';
import Signing from './Signing'; import Signing from './Signing';
import Welcome from './Welcome'; import Welcome from './Welcome';
@ -51,6 +52,7 @@ export default function Popup (): React.ReactElement<{}> {
const [authRequests, setAuthRequests] = useState<null | AuthorizeRequest[]>(null); const [authRequests, setAuthRequests] = useState<null | AuthorizeRequest[]>(null);
const [cameraOn, setCameraOn] = useState(settings.get().camera === 'on'); const [cameraOn, setCameraOn] = useState(settings.get().camera === 'on');
const [mediaAllowed, setMediaAllowed] = useState(false); const [mediaAllowed, setMediaAllowed] = useState(false);
const [metaRequests, setMetaRequests] = useState<null | MetadataRequest[]>(null);
const [signRequests, setSignRequests] = useState<null | SigningRequest[]>(null); const [signRequests, setSignRequests] = useState<null | SigningRequest[]>(null);
const [isWelcomeDone, setWelcomeDone] = useState(false); const [isWelcomeDone, setWelcomeDone] = useState(false);
@ -65,8 +67,9 @@ export default function Popup (): React.ReactElement<{}> {
useEffect((): void => { useEffect((): void => {
Promise.all([ Promise.all([
subscribeAccounts(setAccounts), subscribeAccounts(setAccounts),
subscribeAuthorize(setAuthRequests), subscribeAuthorizeRequests(setAuthRequests),
subscribeSigning(setSignRequests) subscribeMetadataRequests(setMetaRequests),
subscribeSigningRequests(setSignRequests)
]).catch((error: Error) => console.error(error)); ]).catch((error: Error) => console.error(error));
settings.on('change', ({ camera }): void => setCameraOn(camera === 'on')); settings.on('change', ({ camera }): void => setCameraOn(camera === 'on'));
@ -81,29 +84,33 @@ export default function Popup (): React.ReactElement<{}> {
const Root = isWelcomeDone const Root = isWelcomeDone
? authRequests && authRequests.length ? authRequests && authRequests.length
? Authorize ? Authorize
: signRequests && signRequests.length : metaRequests && metaRequests.length
? Signing ? Metadata
: Accounts : signRequests && signRequests.length
? Signing
: Accounts
: Welcome; : Welcome;
return ( return (
<Loading>{accounts && authRequests && signRequests && ( <Loading>{accounts && authRequests && metaRequests && signRequests && (
<ActionContext.Provider value={_onAction}> <ActionContext.Provider value={_onAction}>
<AccountContext.Provider value={accounts}> <AccountContext.Provider value={accounts}>
<AuthorizeContext.Provider value={authRequests}> <AuthorizeReqContext.Provider value={authRequests}>
<MediaContext.Provider value={cameraOn && mediaAllowed}> <MediaContext.Provider value={cameraOn && mediaAllowed}>
<SigningContext.Provider value={signRequests}> <MetadataReqContext.Provider value={metaRequests}>
<Switch> <SigningReqContext.Provider value={signRequests}>
<Route path='/account/create' component={CreateAccount} /> <Switch>
<Route path='/account/forget/:address' component={Forget} /> <Route path='/account/create' component={CreateAccount} />
<Route path='/account/export/:address' component={Export} /> <Route path='/account/forget/:address' component={Forget} />
<Route path='/account/import-qr' component={ImportQr} /> <Route path='/account/export/:address' component={Export} />
<Route path='/account/import-seed' component={ImportSeed} /> <Route path='/account/import-qr' component={ImportQr} />
<Route exact path='/' component={Root} /> <Route path='/account/import-seed' component={ImportSeed} />
</Switch> <Route exact path='/' component={Root} />
</SigningContext.Provider> </Switch>
</SigningReqContext.Provider>
</MetadataReqContext.Provider>
</MediaContext.Provider> </MediaContext.Provider>
</AuthorizeContext.Provider> </AuthorizeReqContext.Provider>
</AccountContext.Provider> </AccountContext.Provider>
</ActionContext.Provider> </ActionContext.Provider>
)}</Loading> )}</Loading>

View File

@ -7,16 +7,17 @@ import { Chain } from '@polkadot/extension-chains/types';
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import findChain from '@polkadot/extension-chains';
import settings from '@polkadot/ui-settings'; import settings from '@polkadot/ui-settings';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'; import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import { AccountContext } from './contexts'; import { AccountContext } from './contexts';
import Identicon from '@polkadot/extension-ui/components/Identicon'; import Identicon from './Identicon';
import Svg from '@polkadot/extension-ui/components/Svg'; import Svg from './Svg';
import Menu from '@polkadot/extension-ui/components/Menu'; import Menu from './Menu';
import DetailsImg from '../assets/details.svg'; import DetailsImg from '../assets/details.svg';
import { useOutsideClick } from '@polkadot/extension-ui/hooks'; import useOutsideClick from '../hooks/useOutsideClick';
import useMetadata from '../hooks/useMetadata';
interface Props { interface Props {
address?: string | null; address?: string | null;
@ -29,7 +30,6 @@ interface Props {
interface Recoded { interface Recoded {
account: AccountJson | null; account: AccountJson | null;
chain: Chain | null;
formatted: string | null; formatted: string | null;
prefix: number; prefix: number;
} }
@ -44,19 +44,17 @@ function findAccount (accounts: AccountJson[], publicKey: Uint8Array): AccountJs
} }
// recodes an supplied address using the prefix/genesisHash, include the actual saved account & chain // recodes an supplied address using the prefix/genesisHash, include the actual saved account & chain
function recodeAddress (address: string, accounts: AccountJson[], genesisHash?: string | null): Recoded { function recodeAddress (address: string, accounts: AccountJson[], chain: Chain | null): Recoded {
// decode and create a shortcut for the encoded address // decode and create a shortcut for the encoded address
const publicKey = decodeAddress(address); const publicKey = decodeAddress(address);
// find our account using the actual publicKey, and then find the associated chain // find our account using the actual publicKey, and then find the associated chain
const account = findAccount(accounts, publicKey); const account = findAccount(accounts, publicKey);
const chain = findChain(account?.genesisHash || genesisHash); const prefix = settings.prefix === -1 ? (chain?.ss58Format || 42) : settings.prefix;
const prefix = settings.prefix === -1 ? chain.ss58Format : settings.prefix;
// always allow the actual settings to override the display // always allow the actual settings to override the display
return { return {
account, account,
chain,
formatted: encodeAddress(publicKey, prefix), formatted: encodeAddress(publicKey, prefix),
prefix prefix
}; };
@ -66,7 +64,8 @@ const ACCOUNTS_SCREEN_HEIGHT = 500;
function Address ({ address, className, children, genesisHash, name, actions }: Props): React.ReactElement<Props> { function Address ({ address, className, children, genesisHash, name, actions }: Props): React.ReactElement<Props> {
const accounts = useContext(AccountContext); const accounts = useContext(AccountContext);
const [{ account, chain, formatted, prefix }, setRecoded] = useState<Recoded>({ account: null, chain: null, formatted: null, prefix: 42 }); const chain = useMetadata(genesisHash);
const [{ account, formatted, prefix }, setRecoded] = useState<Recoded>({ account: null, formatted: null, prefix: 42 });
const [showActionsMenu, setShowActionsMenu] = useState(false); const [showActionsMenu, setShowActionsMenu] = useState(false);
const [moveMenuUp, setIsMovedMenu] = useState(false); const [moveMenuUp, setIsMovedMenu] = useState(false);
const actionsRef = useRef<HTMLDivElement>(null); const actionsRef = useRef<HTMLDivElement>(null);
@ -75,9 +74,9 @@ function Address ({ address, className, children, genesisHash, name, actions }:
useEffect((): void => { useEffect((): void => {
address && setRecoded( address && setRecoded(
recodeAddress(address, accounts, genesisHash) recodeAddress(address, accounts, chain)
); );
}, [accounts, address, genesisHash]); }, [accounts, address, chain]);
useEffect(() => { useEffect(() => {
if (!showActionsMenu) { if (!showActionsMenu) {
@ -183,7 +182,7 @@ const Settings = styled.div`
} }
&:before { &:before {
content: ""; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
top: 25%; top: 25%;

View File

@ -8,7 +8,7 @@ import styled from 'styled-components';
import ArrowLeftImage from '../assets/arrowLeft.svg'; import ArrowLeftImage from '../assets/arrowLeft.svg';
import logo from '../assets/pjs.svg'; import logo from '../assets/pjs.svg';
import gear from '../assets/gear.svg'; import gear from '../assets/gear.svg';
import { useOutsideClick } from '../hooks'; import useOutsideClick from '../hooks/useOutsideClick';
import Link from './Link'; import Link from './Link';
import Settings from './Settings'; import Settings from './Settings';
import Svg from './Svg'; import Svg from './Svg';

View File

@ -39,7 +39,7 @@ const Slider = styled.span`
&:before { &:before {
position: absolute; position: absolute;
content: ""; content: '';
height: 16px; height: 16px;
width: 16px; width: 16px;
left: 4px; left: 4px;
@ -56,7 +56,7 @@ const Checkbox = styled.input.attrs(() => ({
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
&:checked + ${Slider}:before { &:checked + ${Slider}:before {
transform: translateX(24px); transform: translateX(24px);
} }
@ -70,7 +70,7 @@ export default styled(Switch)`
height: 24px; height: 24px;
margin: 8px; margin: 8px;
} }
span { span {
font-weight: 600; font-weight: 600;
} }

View File

@ -0,0 +1,74 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
interface Props {
children: React.ReactNode;
className?: string;
isFull?: boolean;
}
function Table ({ children, className, isFull }: Props): React.ReactElement<Props> {
return (
<table className={`${className} ${isFull && 'isFull'}`}>
<tbody>
{children}
</tbody>
</table>
);
}
export default React.memo(styled(Table)`
&.isFull {
height: 100%;
overflow: scroll;
}
border: 0;
display: block;
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
margin-bottom: 1rem;
td.data {
max-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
width: 100%;
pre {
font-family: inherit;
font-size: 0.75rem;
margin: 0;
}
}
td.label {
opacity: 0.5;
padding: 0 0.5rem;
text-align: right;
vertical-align: top;
white-space: nowrap;
}
details {
cursor: pointer;
max-width: 24rem;
&[open] summary {
white-space: normal;
}
summary {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
outline: 0;
}
}
`);

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AccountJson, AuthorizeRequest, SigningRequest } from '@polkadot/extension-base/background/types'; import { AccountJson, AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types';
import React from 'react'; import React from 'react';
import { AvailableThemes } from '.'; import { AvailableThemes } from '.';
@ -12,16 +12,18 @@ const noop = (): void => undefined;
const AccountContext = React.createContext<AccountJson[]>([]); const AccountContext = React.createContext<AccountJson[]>([]);
const ActionContext = React.createContext<(to?: string) => void>(noop); const ActionContext = React.createContext<(to?: string) => void>(noop);
const AuthorizeContext = React.createContext<AuthorizeRequest[]>([]); const AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
const MediaContext = React.createContext<boolean>(false); const MediaContext = React.createContext<boolean>(false);
const SigningContext = React.createContext<SigningRequest[]>([]); const MetadataReqContext = React.createContext<MetadataRequest[]>([]);
const SigningReqContext = React.createContext<SigningRequest[]>([]);
const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop); const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop);
export { export {
AccountContext, AccountContext,
ActionContext, ActionContext,
AuthorizeContext, AuthorizeReqContext,
MediaContext, MediaContext,
SigningContext, MetadataReqContext,
SigningReqContext,
ThemeSwitchContext ThemeSwitchContext
}; };

View File

@ -24,6 +24,7 @@ export { default as Menu } from './Menu';
export { default as MnemonicSeed } from './MnemonicSeed'; export { default as MnemonicSeed } from './MnemonicSeed';
export { default as Settings } from './Settings'; export { default as Settings } from './Settings';
export { default as Svg } from './Svg'; export { default as Svg } from './Svg';
export { default as Table } from './Table';
export { default as TextAreaWithLabel } from './TextAreaWithLabel'; export { default as TextAreaWithLabel } from './TextAreaWithLabel';
export { default as Title } from './Title'; export { default as Title } from './Title';
export { default as VerticalSpace } from './VerticalSpace'; export { default as VerticalSpace } from './VerticalSpace';

View File

@ -0,0 +1,19 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Chain } from '@polkadot/extension-chains/types';
import { useEffect, useState } from 'react';
import { getMetadata } from '../messaging';
export default function useMetadata (genesisHash?: string | null): Chain | null {
const [chain, setChain] = useState<Chain | null>(null);
useEffect((): void => {
getMetadata(genesisHash).then(setChain);
}, [genesisHash]);
return chain;
}

View File

@ -4,16 +4,18 @@
import { useEffect, RefObject } from 'react'; import { useEffect, RefObject } from 'react';
export const useOutsideClick = (ref: RefObject<HTMLDivElement>, callback: () => void): void => { export default function useOutsideClick (ref: RefObject<HTMLDivElement>, callback: () => void): void {
const handleClick = (e: MouseEvent): void => { const handleClick = (e: MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) { if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) {
callback(); callback();
} }
}; };
useEffect(() => { useEffect(() => {
document.addEventListener('click', handleClick); document.addEventListener('click', handleClick);
return (): void => { return (): void => {
document.removeEventListener('click', handleClick); document.removeEventListener('click', handleClick);
}; };
}); });
}; }

View File

@ -2,11 +2,13 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AccountJson, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '@polkadot/extension-base/background/types'; import { AccountJson, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MetadataRequest, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '@polkadot/extension-base/background/types';
import { Chain } from '@polkadot/extension-chains/types';
import { KeypairType } from '@polkadot/util-crypto/types'; import { KeypairType } from '@polkadot/util-crypto/types';
import extension from 'extensionizer'; import extension from 'extensionizer';
import { PORT_EXTENSION } from '@polkadot/extension-base/defaults'; import { PORT_EXTENSION } from '@polkadot/extension-base/defaults';
import { findChain } from '@polkadot/extension-chains';
interface Handler { interface Handler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -27,7 +29,7 @@ port.onMessage.addListener((data): void => {
const handler = handlers[data.id]; const handler = handlers[data.id];
if (!handler) { if (!handler) {
console.error(`Uknown response: ${JSON.stringify(data)}`); console.error(`Unknown response: ${JSON.stringify(data)}`);
return; return;
} }
@ -70,14 +72,14 @@ export async function forgetAccount (address: string): Promise<boolean> {
return sendMessage('pri(accounts.forget)', { address }); return sendMessage('pri(accounts.forget)', { address });
} }
export async function rejectAuthRequest (id: string): Promise<boolean> {
return sendMessage('pri(authorize.reject)', { id });
}
export async function approveAuthRequest (id: string): Promise<boolean> { export async function approveAuthRequest (id: string): Promise<boolean> {
return sendMessage('pri(authorize.approve)', { id }); return sendMessage('pri(authorize.approve)', { id });
} }
export async function approveMetaRequest (id: string): Promise<boolean> {
return sendMessage('pri(metadata.approve)', { id });
}
export async function cancelSignRequest (id: string): Promise<boolean> { export async function cancelSignRequest (id: string): Promise<boolean> {
return sendMessage('pri(signing.cancel)', { id }); return sendMessage('pri(signing.cancel)', { id });
} }
@ -102,16 +104,34 @@ export async function createSeed (length?: SeedLengths, type?: KeypairType): Pro
return sendMessage('pri(seed.create)', { length, type }); return sendMessage('pri(seed.create)', { length, type });
} }
export async function getMetadata (genesisHash?: string | null): Promise<Chain | null> {
const definitions = await sendMessage('pri(metadata.list)');
return findChain(definitions, genesisHash);
}
export async function rejectAuthRequest (id: string): Promise<boolean> {
return sendMessage('pri(authorize.reject)', { id });
}
export async function rejectMetaRequest (id: string): Promise<boolean> {
return sendMessage('pri(metadata.reject)', { id });
}
export async function subscribeAccounts (cb: (accounts: AccountJson[]) => void): Promise<boolean> { export async function subscribeAccounts (cb: (accounts: AccountJson[]) => void): Promise<boolean> {
return sendMessage('pri(accounts.subscribe)', null, cb); return sendMessage('pri(accounts.subscribe)', null, cb);
} }
export async function subscribeAuthorize (cb: (accounts: AuthorizeRequest[]) => void): Promise<boolean> { export async function subscribeAuthorizeRequests (cb: (accounts: AuthorizeRequest[]) => void): Promise<boolean> {
return sendMessage('pri(authorize.subscribe)', null, cb); return sendMessage('pri(authorize.requests)', null, cb);
} }
export async function subscribeSigning (cb: (accounts: SigningRequest[]) => void): Promise<boolean> { export async function subscribeMetadataRequests (cb: (accounts: MetadataRequest[]) => void): Promise<boolean> {
return sendMessage('pri(signing.subscribe)', null, cb); return sendMessage('pri(metadata.requests)', null, cb);
}
export async function subscribeSigningRequests (cb: (accounts: SigningRequest[]) => void): Promise<boolean> {
return sendMessage('pri(signing.requests)', null, cb);
} }
export async function validateSeed (suri: string, type?: KeypairType): Promise<{ address: string; suri: string }> { export async function validateSeed (suri: string, type?: KeypairType): Promise<{ address: string; suri: string }> {

View File

@ -2888,7 +2888,6 @@ __metadata:
resolution: "@polkadot/extension-chains@workspace:packages/extension-chains" resolution: "@polkadot/extension-chains@workspace:packages/extension-chains"
dependencies: dependencies:
"@babel/runtime": ^7.9.2 "@babel/runtime": ^7.9.2
edgeware-node-types: ^1.2.0
peerDependencies: peerDependencies:
"@polkadot/api": "*" "@polkadot/api": "*"
languageName: unknown languageName: unknown
@ -8271,13 +8270,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"edgeware-node-types@npm:^1.2.0":
version: 1.2.0
resolution: "edgeware-node-types@npm:1.2.0"
checksum: 2/2d04eb58ba2cef6bbe7d39726e3665e16579c42122a2e2da378977eafe07e2c615a86e95bc33293fcd25b008b64700d16ed7055faf50d3ddbaaa2a95bcbe17d9
languageName: node
linkType: hard
"ee-first@npm:1.1.1": "ee-first@npm:1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "ee-first@npm:1.1.1" resolution: "ee-first@npm:1.1.1"