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
}
yarn polkadot-dev-build-ts
yarn build
copy_folder "extension-dapp"
copy_folder "extension-inject"

View File

@ -24,7 +24,7 @@
"postinstall": "polkadot-dev-yarn-only",
"start": "yarn watch",
"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": {
"@babel/core": "^7.9.0",

View File

@ -2,8 +2,9 @@
// 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 { 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 keyring from '@polkadot/ui-keyring';
@ -108,7 +109,7 @@ export default class Extension {
// 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.subscribe)'>(id, port);
const cb = createSubscription<'pri(authorize.requests)'>(id, port);
const subscription = this.#state.authSubject.subscribe((requests: AuthorizeRequest[]): void =>
cb(requests)
);
@ -121,6 +122,50 @@ export default class Extension {
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 {
const seed = mnemonicGenerate(length);
@ -199,7 +244,7 @@ export default class Extension {
// 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.subscribe)'>(id, port);
const cb = createSubscription<'pri(signing.requests)'>(id, port);
const subscription = this.#state.signSubject.subscribe((requests: SigningRequest[]): void =>
cb(requests)
);
@ -228,9 +273,9 @@ export default class Extension {
return this.authorizeApprove(request as RequestAuthorizeApprove);
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);
case 'pri(accounts.create.external)':
@ -251,6 +296,18 @@ export default class Extension {
case 'pri(accounts.subscribe)':
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)':
return this.seedCreate(request as RequestSeedCreate);
@ -266,7 +323,7 @@ export default class Extension {
case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel);
case 'pri(signing.subscribe)':
case 'pri(signing.requests)':
return this.signingSubscribe(id, port);
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
// 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 { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest, RequestRpcUnsubscribe } from '../types';
import { AccountJson, AuthorizeRequest, MetadataRequest, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning, SigningRequest, RequestRpcUnsubscribe } from '../types';
import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs';
import { assert } from '@polkadot/util';
import MetadataStore from './MetadataStore';
interface AuthRequest {
id: string;
idStr: string;
@ -28,6 +30,14 @@ type AuthUrls = Record<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
// exposed by the extension.
type Providers = Record<string, {
@ -68,9 +78,13 @@ export default class State {
readonly #authRequests: Record<string, AuthRequest> = {};
readonly #metaStore = new MetadataStore();
// Map of providers currently injected in tabs
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
readonly #providers: Providers;
@ -78,26 +92,34 @@ export default class State {
#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 = {}) {
this.#providers = providers;
this.#metaStore.all((key: string, def: MetadataDef): void => {
if (key.startsWith('metadata:')) {
addMetadata(def);
}
});
}
public get hasAuthRequests (): boolean {
return this.numAuthRequests === 0;
}
public get hasSignRequests (): boolean {
return this.numSignRequests === 0;
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;
}
@ -108,6 +130,12 @@ export default class State {
.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)
@ -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 => {
return (result: ResponseSigning | Error): void => {
delete this.#signRequests[id];
@ -168,11 +205,14 @@ export default class State {
private updateIcon (shouldClose?: boolean): void {
const authCount = this.numAuthRequests;
const metaCount = this.numMetaRequests;
const signCount = this.numSignRequests;
const text = (
authCount
? 'Auth'
: (signCount ? `${signCount}` : '')
: metaCount
? 'Meta'
: (signCount ? `${signCount}` : '')
);
extension.browserAction.setBadgeText({ text });
@ -187,6 +227,11 @@ export default class State {
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);
@ -198,7 +243,7 @@ export default class State {
if (this.#authUrls[idStr]) {
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 => {
@ -227,10 +272,31 @@ export default class State {
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 {
return this.#authRequests[id];
}
public getMetaRequest (id: string): MetaRequest {
return this.#metaRequests[id];
}
public getSignRequest (id: string): SignRequest {
return this.#signRequests[id];
}
@ -303,6 +369,12 @@ export default class State {
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> {
const id = getId();

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// 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 { JsonRpcResponse } from '@polkadot/rpc-provider/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 accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert } from '@polkadot/util';
import RequestBytesSign from '../RequestBytesSign';
import RequestExtrinsicSign from '../RequestExtrinsicSign';
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
@ -57,22 +57,38 @@ export default class Tabs {
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);
}
// 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> {
return this.#state.rpcListProviders();
}
@ -135,6 +151,12 @@ export default class Tabs {
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(rpc.listProviders)':
return this.rpcListProviders();

View File

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

View File

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

View File

@ -7,17 +7,21 @@ import { Injected } from '@polkadot/extension-inject/types';
import { SendRequest } from './types';
import Accounts from './Accounts';
import Metadata from './Metadata';
import Signer from './Signer';
export default class implements Injected {
public readonly accounts: Accounts;
public readonly metadata: Metadata;
public readonly provider: PostMessageProvider;
public readonly signer: Signer;
constructor (sendRequest: SendRequest) {
this.accounts = new Accounts(sendRequest);
this.metadata = new Metadata(sendRequest);
this.provider = new PostMessageProvider(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;
}
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest;
/**
* @name PostMessageProvider
*
@ -32,8 +35,6 @@ export default class PostMessageProvider implements InjectedProvider {
// Whether or not the actual extension background provider is connected
#isConnected = false
readonly #sendRequest: SendRequest;
// Subscription IDs are (historically) not guaranteed to be globally unique;
// only unique for a given subscription method; which is why we identify
// 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} subscriptionNotificationHandler Channel for receiving subscription messages
*/
public constructor (sendRequest: SendRequest) {
public constructor (_sendRequest: SendRequest) {
this.#eventemitter = new EventEmitter();
this.#sendRequest = sendRequest;
sendRequest = _sendRequest;
}
/**
* @description Returns a clone of the object
*/
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> {
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) {
const { callback, type } = subscription;
return this.#sendRequest(
return sendRequest(
'pub(rpc.subscribe)',
{ type, method, params },
(res) => {
@ -114,7 +116,7 @@ export default class PostMessageProvider implements InjectedProvider {
return id;
});
} 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.#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;
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 { SendRequest } from './types';
// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest;
let nextId = 0;
export default class Signer implements SignerInterface {
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;
}

View File

@ -5,8 +5,7 @@
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "Apache-2",
"dependencies": {
"@babel/runtime": "^7.9.2",
"edgeware-node-types": "^1.2.0"
"@babel/runtime": "^7.9.2"
},
"peerDependencies": {
"@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
// 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';
// imports chain details, generally metadata. For the generation of these,
// inside the api, run `yarn chain:info --ws <url>`
import edgeware from './edgeware';
import kusama from './kusama';
const chains: Map<string, Chain> = new Map(
// eslint-disable-next-line @typescript-eslint/no-use-before-define
[edgeware, kusama].map((def) => defToChain(def))
const definitions: Map<string, MetadataDef> = new Map(
[kusama].map((def) => [def.genesisHash, 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();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -33,7 +33,8 @@ function defToChain ({ chain, genesisHash, icon, metaCalls, specVersion, ss58For
tokenSymbol
}));
return [genesisHash, {
return {
definition,
genesisHash: isUnknown
? undefined
: genesisHash,
@ -46,26 +47,21 @@ function defToChain ({ chain, genesisHash, icon, metaCalls, specVersion, ss58For
ss58Format,
tokenDecimals,
tokenSymbol
}];
};
}
const [, UNKNOWN_CHAIN] = defToChain({
chain: 'Unknown chain',
genesisHash: '0x',
icon: 'polkadot',
specVersion: 0,
ss58Format: 42,
tokenDecimals: 0,
tokenSymbol: 'Unit',
types: {}
});
export function findChain (definitions: MetadataDef[], genesisHash?: string | null): Chain | null {
const def = definitions.find((def) => def.genesisHash === genesisHash);
export function addChainDef (def: ChainDef): void {
const [genesisHash, chain] = defToChain(def);
chains.set(genesisHash, chain);
return def
? metadataExpand(def)
: null;
}
export default function findChain (genesisHash?: string | null): Chain {
return chains.get(genesisHash || '') || UNKNOWN_CHAIN;
export function addMetadata (def: MetadataDef): void {
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
// 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';
export interface Chain {
definition: MetadataDef;
genesisHash?: string;
hasMetadata: boolean;
icon: string;
@ -16,15 +18,3 @@ export interface Chain {
tokenDecimals: number;
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
subscribe: (cb: (accounts: InjectedAccount[]) => any) => Unsubcall;
}
export interface InjectedExtensionInfo {
name: string;
version: string;
@ -47,6 +48,28 @@ export interface ProviderMeta {
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 interface InjectedProvider extends ProviderInterface {
@ -62,8 +85,9 @@ export interface InjectedProviderWithMeta {
export interface Injected {
accounts: InjectedAccounts;
signer: InjectedSigner;
metadata?: InjectedMetadata;
provider?: InjectedProvider;
signer: InjectedSigner;
}
export interface InjectedWindowProvider {

View File

@ -6,7 +6,7 @@ import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
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 { configure, mount, ReactWrapper } from 'enzyme';
import { AuthorizeRequest } from '@polkadot/extension-base/background/types';
@ -16,11 +16,11 @@ configure({ adapter: new Adapter() });
describe('Authorize', () => {
const mountAuthorize = (authorizeRequests: AuthorizeRequest[] = []): ReactWrapper => mount(
<AuthorizeContext.Provider value={authorizeRequests}>
<AuthorizeReqContext.Provider value={authorizeRequests}>
<ThemeProvider theme={themes.dark}>
<Authorize />
</ThemeProvider>
</AuthorizeContext.Provider>);
</AuthorizeReqContext.Provider>);
it('render component', () => {
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{' '}
<a
href={url}
target="_blank"
rel="noopener noreferrer"
target='_blank'
rel='noopener noreferrer'
>
<span className='tab-url'>{url}</span>
</a>.

View File

@ -4,17 +4,17 @@
import React, { useContext } from 'react';
import { AuthorizeContext, Header } from '../../components';
import { AuthorizeReqContext, Header } from '../../components';
import Request from './Request';
import styled from 'styled-components';
export default function Authorize (): React.ReactElement<{}> {
const requests = useContext(AuthorizeContext);
const requests = useContext(AuthorizeReqContext);
return (
<>
<Scroll isLastRequest={requests.length === 1}>
<Header text='Authorize'/>
<Header text='Authorize' />
{requests.map(({ id, request, url }, index): React.ReactNode => (
<Request
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 React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import findChain from '@polkadot/extension-chains';
import { formatNumber, bnToBn } from '@polkadot/util';
import { Table } from '../../components';
import useMetadata from '../../hooks/useMetadata';
interface Decoded {
args: AnyJson | null;
method: Call | null;
@ -25,12 +26,12 @@ interface Props {
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 method: Call | null = null;
try {
if (isDecoded && chain.hasMetadata && specVersion.eqn(chain.specVersion)) {
if (isDecoded && chain && chain.hasMetadata && specVersion.eqn(chain.specVersion)) {
method = chain.registry.createType('Call', data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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> {
const chain = useRef(findChain(genesisHash)).current;
const chain = useMetadata(genesisHash);
const specVersion = useRef(bnToBn(hexSpec)).current;
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]);
return (
<table className={className}>
<tbody>
<Table className={className} isFull>
<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>
<td className='label'>from</td>
<td className='data'>{url}</td>
<td className='label'>tip</td>
<td className='data'>{formatNumber(tip)}</td>
</tr>
<tr>
<td className='label'>{chain.isUnknown ? 'genesis' : 'chain'}</td>
<td className='data'>{chain.isUnknown ? genesisHash : chain.name}</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>
<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>
)}
{renderMethod(method, decoded)}
<tr>
<td className='label'>lifetime</td>
<td className='data'>{mortalityAsString(era, blockNumber)}</td>
</tr>
</Table>
);
}
export default styled(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;
}
}
`;
export default React.memo(Extrinsic);

View File

@ -36,6 +36,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
useEffect((): void => {
const inner = request.inner;
if ((inner as SignerPayloadRaw).data) {
setHexBytes((inner as SignerPayloadRaw).data);
setExtrinsic(null);
@ -45,10 +46,10 @@ export default function Request ({ account: { isExternal }, request, signId, url
}
}, [request]);
const _onCancel = (): Promise<void> => {
return cancelSignRequest(signId).then((): void => onAction())
const _onCancel = (): Promise<void> =>
cancelSignRequest(signId)
.then((): void => onAction())
.catch((error: Error) => console.error(error));
};
const _onSign = (password: string): Promise<void> =>
approveSignPassword(signId, password)
.then((): void => onAction())
@ -60,6 +61,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
if (extrinsic !== null) {
const payload = request.inner as SignerPayloadJSON;
return (
<>
<Address
@ -92,6 +94,7 @@ export default function Request ({ account: { isExternal }, request, signId, url
);
} else if (hexBytes !== null) {
const payload = request.inner as SignerPayloadRaw;
return (
<>
<Address address={payload.address} />
@ -105,9 +108,9 @@ export default function Request ({ account: { isExternal }, request, signId, url
</SignArea>
</>
);
} else {
return null;
}
return null;
}
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 { AccountJson, SigningRequest } from '@polkadot/extension-base/background/types';
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
import {
AccountContext,
ActionContext,
themes,
SigningContext,
Address,
Input,
Button
} from '@polkadot/extension-ui/components';
import { AccountContext, ActionContext, Address, Button, Input, SigningReqContext, themes } from '@polkadot/extension-ui/components';
import { ThemeProvider } from 'styled-components';
import Signing from '.';
import TransactionIndex from './TransactionIndex';
@ -52,9 +44,9 @@ describe('Signing requests', () => {
emitter.on('request', setRequests);
return (
<SigningContext.Provider value={requests}>
<SigningReqContext.Provider value={requests}>
<Signing />
</SigningContext.Provider>
</SigningReqContext.Provider>
);
}
const mountComponent = async (): Promise<void> => {

View File

@ -2,16 +2,24 @@
// 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, 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 TransactionIndex from './TransactionIndex';
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<{}> {
const requests = useContext(SigningContext);
const requests = useContext(SigningReqContext);
const [requestIndex, setRequestIndex] = useState(0);
const isTransaction = useMemo(
() => isInnerTransaction(requests[requestIndex]),
[requests, requestIndex]
);
useEffect(() => {
if (requestIndex >= requests.length) {
@ -19,13 +27,9 @@ export default function Signing (): React.ReactElement<{}> {
}
}, [requests]);
function isTransaction (signingRequest: SigningRequest): boolean {
return 'blockNumber' in signingRequest.request.inner;
}
return requests[requestIndex] ? (
<>
<Header text={isTransaction(requests[requestIndex]) ? 'Transaction' : 'Sign message'}>
<Header text={isTransaction ? 'Transaction' : 'Sign message'}>
{requests.length > 1 && (
<TransactionIndex
index={requestIndex}
@ -41,7 +45,7 @@ export default function Signing (): React.ReactElement<{}> {
request={requests[requestIndex].request}
signId={requests[requestIndex].id}
url={requests[requestIndex].url}
buttonText={isTransaction(requests[requestIndex]) ? 'Sign the transaction' : 'Sign the message'}
buttonText={isTransaction ? 'Sign the transaction' : 'Sign the message'}
/>
</>
) : <Loading />;

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ const Slider = styled.span`
&:before {
position: absolute;
content: "";
content: '';
height: 16px;
width: 16px;
left: 4px;

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
// 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 { AvailableThemes } from '.';
@ -12,16 +12,18 @@ const noop = (): void => undefined;
const AccountContext = React.createContext<AccountJson[]>([]);
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 SigningContext = React.createContext<SigningRequest[]>([]);
const MetadataReqContext = React.createContext<MetadataRequest[]>([]);
const SigningReqContext = React.createContext<SigningRequest[]>([]);
const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop);
export {
AccountContext,
ActionContext,
AuthorizeContext,
AuthorizeReqContext,
MediaContext,
SigningContext,
MetadataReqContext,
SigningReqContext,
ThemeSwitchContext
};

View File

@ -24,6 +24,7 @@ export { default as Menu } from './Menu';
export { default as MnemonicSeed } from './MnemonicSeed';
export { default as Settings } from './Settings';
export { default as Svg } from './Svg';
export { default as Table } from './Table';
export { default as TextAreaWithLabel } from './TextAreaWithLabel';
export { default as Title } from './Title';
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';
export const useOutsideClick = (ref: RefObject<HTMLDivElement>, callback: () => void): void => {
export default function useOutsideClick (ref: RefObject<HTMLDivElement>, callback: () => void): void {
const handleClick = (e: MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) {
callback();
}
};
useEffect(() => {
document.addEventListener('click', handleClick);
return (): void => {
document.removeEventListener('click', handleClick);
};
});
};
}

View File

@ -2,11 +2,13 @@
// This software may be modified and distributed under the terms
// 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 extension from 'extensionizer';
import { PORT_EXTENSION } from '@polkadot/extension-base/defaults';
import { findChain } from '@polkadot/extension-chains';
interface Handler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -27,7 +29,7 @@ port.onMessage.addListener((data): void => {
const handler = handlers[data.id];
if (!handler) {
console.error(`Uknown response: ${JSON.stringify(data)}`);
console.error(`Unknown response: ${JSON.stringify(data)}`);
return;
}
@ -70,14 +72,14 @@ export async function forgetAccount (address: string): Promise<boolean> {
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> {
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> {
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 });
}
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> {
return sendMessage('pri(accounts.subscribe)', null, cb);
}
export async function subscribeAuthorize (cb: (accounts: AuthorizeRequest[]) => void): Promise<boolean> {
return sendMessage('pri(authorize.subscribe)', null, cb);
export async function subscribeAuthorizeRequests (cb: (accounts: AuthorizeRequest[]) => void): Promise<boolean> {
return sendMessage('pri(authorize.requests)', null, cb);
}
export async function subscribeSigning (cb: (accounts: SigningRequest[]) => void): Promise<boolean> {
return sendMessage('pri(signing.subscribe)', null, cb);
export async function subscribeMetadataRequests (cb: (accounts: MetadataRequest[]) => void): Promise<boolean> {
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 }> {

View File

@ -2888,7 +2888,6 @@ __metadata:
resolution: "@polkadot/extension-chains@workspace:packages/extension-chains"
dependencies:
"@babel/runtime": ^7.9.2
edgeware-node-types: ^1.2.0
peerDependencies:
"@polkadot/api": "*"
languageName: unknown
@ -8271,13 +8270,6 @@ __metadata:
languageName: node
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":
version: 1.1.1
resolution: "ee-first@npm:1.1.1"