From c9f4c2e4a621f16ed0f2f1cc969d94b256ced356 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 26 Mar 2020 11:01:33 +0100 Subject: [PATCH] Provide metadata from external source (#288) * Provide metadata from external source * Listing via metadata interface * Updates * UI update * Storage & end-to-end --- copy-to-apps.sh | 2 +- package.json | 2 +- .../src/background/handlers/Extension.ts | 69 +++++++++- .../src/background/handlers/MetadataStore.ts | 55 ++++++++ .../src/background/handlers/State.ts | 98 ++++++++++++-- .../src/background/handlers/Tabs.ts | 26 +++- .../extension-base/src/background/types.ts | 28 +++- packages/extension-base/src/page/Accounts.ts | 1 + packages/extension-base/src/page/Injected.ts | 4 + packages/extension-base/src/page/Metadata.ts | 23 ++++ .../src/page/PostMessageProvider.ts | 22 +-- packages/extension-base/src/page/Signer.ts | 4 +- packages/extension-chains/package.json | 3 +- packages/extension-chains/src/edgeware.ts | 25 ---- packages/extension-chains/src/index.ts | 44 +++--- packages/extension-chains/src/types.ts | 14 +- packages/extension-inject/src/types.ts | 26 +++- .../src/Popup/Authorize/Authorize.test.tsx | 6 +- .../src/Popup/Authorize/Request.tsx | 4 +- .../src/Popup/Authorize/index.tsx | 6 +- .../src/Popup/Metadata/Request.tsx | 125 ++++++++++++++++++ .../extension-ui/src/Popup/Metadata/index.tsx | 29 ++++ .../src/Popup/Signing/Extrinsic.tsx | 115 +++++----------- .../src/Popup/Signing/Request.tsx | 13 +- .../src/Popup/Signing/Signing.test.tsx | 14 +- .../extension-ui/src/Popup/Signing/index.tsx | 22 +-- packages/extension-ui/src/Popup/index.tsx | 49 ++++--- .../extension-ui/src/components/Address.tsx | 27 ++-- .../extension-ui/src/components/Header.tsx | 2 +- .../extension-ui/src/components/Switch.tsx | 6 +- .../extension-ui/src/components/Table.tsx | 74 +++++++++++ .../extension-ui/src/components/contexts.tsx | 12 +- packages/extension-ui/src/components/index.ts | 1 + .../extension-ui/src/hooks/useMetadata.ts | 19 +++ .../{hooks.ts => hooks/useOutsideClick.ts} | 6 +- packages/extension-ui/src/messaging.ts | 40 ++++-- yarn.lock | 8 -- 37 files changed, 743 insertions(+), 281 deletions(-) create mode 100644 packages/extension-base/src/background/handlers/MetadataStore.ts create mode 100644 packages/extension-base/src/page/Metadata.ts delete mode 100644 packages/extension-chains/src/edgeware.ts create mode 100644 packages/extension-ui/src/Popup/Metadata/Request.tsx create mode 100644 packages/extension-ui/src/Popup/Metadata/index.tsx create mode 100644 packages/extension-ui/src/components/Table.tsx create mode 100644 packages/extension-ui/src/hooks/useMetadata.ts rename packages/extension-ui/src/{hooks.ts => hooks/useOutsideClick.ts} (84%) diff --git a/copy-to-apps.sh b/copy-to-apps.sh index 5c91a6ed..e13e7dc1 100755 --- a/copy-to-apps.sh +++ b/copy-to-apps.sh @@ -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" diff --git a/package.json b/package.json index 76e6bcea..cad25a57 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 6bb4c37e..0c40ff18 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -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)': diff --git a/packages/extension-base/src/background/handlers/MetadataStore.ts b/packages/extension-base/src/background/handlers/MetadataStore.ts new file mode 100644 index 00000000..408c4a6b --- /dev/null +++ b/packages/extension-base/src/background/handlers/MetadataStore.ts @@ -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; + +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(); + }); + } +} diff --git a/packages/extension-base/src/background/handlers/State.ts b/packages/extension-base/src/background/handlers/State.ts index 3fc5dc38..2c263b9e 100644 --- a/packages/extension-base/src/background/handlers/State.ts +++ b/packages/extension-base/src/background/handlers/State.ts @@ -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; +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 = {}; + readonly #metaStore = new MetadataStore(); + // Map of providers currently injected in tabs readonly #injectedProviders: Map = new Map(); + readonly #metaRequests: Record = {}; + // 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 = new BehaviorSubject([] as AuthorizeRequest[]); + public readonly authSubject: BehaviorSubject = new BehaviorSubject([]); - public readonly signSubject: BehaviorSubject = new BehaviorSubject([] as SigningRequest[]); + public readonly metaSubject: BehaviorSubject = new BehaviorSubject([]); + + public readonly signSubject: BehaviorSubject = new BehaviorSubject([]); 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 { + 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 { const id = getId(); diff --git a/packages/extension-base/src/background/handlers/Tabs.ts b/packages/extension-base/src/background/handlers/Tabs.ts index e298464b..a77547be 100644 --- a/packages/extension-base/src/background/handlers/Tabs.ts +++ b/packages/extension-base/src/background/handlers/Tabs.ts @@ -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 { 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 { 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 { + 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 { 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(); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 77e56f40..980afa37 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -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; diff --git a/packages/extension-base/src/page/Accounts.ts b/packages/extension-base/src/page/Accounts.ts index 6d357fb3..072fcf66 100644 --- a/packages/extension-base/src/page/Accounts.ts +++ b/packages/extension-base/src/page/Accounts.ts @@ -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 { diff --git a/packages/extension-base/src/page/Injected.ts b/packages/extension-base/src/page/Injected.ts index 26c62eb2..7bb9f7aa 100644 --- a/packages/extension-base/src/page/Injected.ts +++ b/packages/extension-base/src/page/Injected.ts @@ -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); } diff --git a/packages/extension-base/src/page/Metadata.ts b/packages/extension-base/src/page/Metadata.ts new file mode 100644 index 00000000..b8ac91d2 --- /dev/null +++ b/packages/extension-base/src/page/Metadata.ts @@ -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 { + return sendRequest('pub(metadata.list)'); + } + + public provide (definition: MetadataDef): Promise { + return sendRequest('pub(metadata.provide)', definition); + } +} diff --git a/packages/extension-base/src/page/PostMessageProvider.ts b/packages/extension-base/src/page/PostMessageProvider.ts index 7dffbf9a..49d6155a 100644 --- a/packages/extension-base/src/page/PostMessageProvider.ts +++ b/packages/extension-base/src/page/PostMessageProvider.ts @@ -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 { - 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) { diff --git a/packages/extension-base/src/page/Signer.ts b/packages/extension-base/src/page/Signer.ts index a2ef27a2..3fae99da 100644 --- a/packages/extension-base/src/page/Signer.ts +++ b/packages/extension-base/src/page/Signer.ts @@ -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; } diff --git a/packages/extension-chains/package.json b/packages/extension-chains/package.json index f9bf29bf..a52af557 100644 --- a/packages/extension-chains/package.json +++ b/packages/extension-chains/package.json @@ -5,8 +5,7 @@ "author": "Jaco Greeff ", "license": "Apache-2", "dependencies": { - "@babel/runtime": "^7.9.2", - "edgeware-node-types": "^1.2.0" + "@babel/runtime": "^7.9.2" }, "peerDependencies": { "@polkadot/api": "*" diff --git a/packages/extension-chains/src/edgeware.ts b/packages/extension-chains/src/edgeware.ts deleted file mode 100644 index 8e0017ab..00000000 --- a/packages/extension-chains/src/edgeware.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019-2020 @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. - -// Generated via (in api) `yarn chain:info --ws wss://mainnet1.edgewa.re` - -import { EdgewareTypes } from 'edgeware-node-types/dist'; - -// ...EdgewareTypes, -// Keys: 'SessionKeys4' - -export default { - chain: 'Edgeware', - genesisHash: '0xda60008770287b5eb4f9b5241317ac4c7e42c3c3d8e85fbbfa2a88705394d75e', - icon: 'substrate', - specVersion: 20, - ss58Format: 42, - tokenDecimals: 18, - tokenSymbol: 'EDG', - types: { - ...EdgewareTypes, - Keys: 'SessionKeys4' - }, - metaCalls: 'bWV0YQdcGFN5c3RlbQABGChmaWxsX2Jsb2NrAAQhASBBIGJpZyBkaXNwYXRjaCB0aGF0IHdpbGwgZGlzYWxsb3cgYW55IG90aGVyIHRyYW5zYWN0aW9uIHRvIGJlIGluY2x1ZGVkLhhyZW1hcmsEHF9yZW1hcmsUQnl0ZXMEbCBNYWtlIHNvbWUgb24tY2hhaW4gcmVtYXJrLjhzZXRfaGVhcF9wYWdlcwQUcGFnZXMMdTY0BPwgU2V0IHRoZSBudW1iZXIgb2YgcGFnZXMgaW4gdGhlIFdlYkFzc2VtYmx5IGVudmlyb25tZW50J3MgaGVhcC4gc2V0X2NvZGUEDG5ldxRCeXRlcwRIIFNldCB0aGUgbmV3IGNvZGUuLHNldF9zdG9yYWdlBBRpdGVtczRWZWM8S2V5VmFsdWU+BGwgU2V0IHNvbWUgaXRlbXMgb2Ygc3RvcmFnZS4wa2lsbF9zdG9yYWdlBBBrZXlzIFZlYzxLZXk+BHggS2lsbCBzb21lIGl0ZW1zIGZyb20gc3RvcmFnZS4AABBBdXJhAAEAAAAkVGltZXN0YW1wAAEEDHNldAQMbm93PENvbXBhY3Q8TW9tZW50PiRYIFNldCB0aGUgY3VycmVudCB0aW1lLgBZASBUaGlzIGNhbGwgc2hvdWxkIGJlIGludm9rZWQgZXhhY3RseSBvbmNlIHBlciBibG9jay4gSXQgd2lsbCBwYW5pYyBhdCB0aGUgZmluYWxpemF0aW9u2CBwaGFzZSwgaWYgdGhpcyBjYWxsIGhhc24ndCBiZWVuIGludm9rZWQgYnkgdGhhdCB0aW1lLgBFASBUaGUgdGltZXN0YW1wIHNob3VsZCBiZSBncmVhdGVyIHRoYW4gdGhlIHByZXZpb3VzIG9uZSBieSB0aGUgYW1vdW50IHNwZWNpZmllZCBieUQgYE1pbmltdW1QZXJpb2RgLgDYIFRoZSBkaXNwYXRjaCBvcmlnaW4gZm9yIHRoaXMgY2FsbCBtdXN0IGJlIGBJbmhlcmVudGAuAAAoQXV0aG9yc2hpcAABBChzZXRfdW5jbGVzBChuZXdfdW5jbGVzLFZlYzxIZWFkZXI+BGQgUHJvdmlkZSBhIHNldCBvZiB1bmNsZXMuAAAcSW5kaWNlcwABAAAAIEJhbGFuY2VzAAEMIHRyYW5zZmVyCBBkZXN0HEFkZHJlc3MUdmFsdWVAQ29tcGFjdDxCYWxhbmNlPlzYIFRyYW5zZmVyIHNvbWUgbGlxdWlkIGZyZWUgYmFsYW5jZSB0byBhbm90aGVyIGFjY291bnQuAAkBIGB0cmFuc2ZlcmAgd2lsbCBzZXQgdGhlIGBGcmVlQmFsYW5jZWAgb2YgdGhlIHNlbmRlciBhbmQgcmVjZWl2ZXIuIQEgSXQgd2lsbCBkZWNyZWFzZSB0aGUgdG90YWwgaXNzdWFuY2Ugb2YgdGhlIHN5c3RlbSBieSB0aGUgYFRyYW5zZmVyRmVlYC4VASBJZiB0aGUgc2VuZGVyJ3MgYWNjb3VudCBpcyBiZWxvdyB0aGUgZXhpc3RlbnRpYWwgZGVwb3NpdCBhcyBhIHJlc3VsdLQgb2YgdGhlIHRyYW5zZmVyLCB0aGUgYWNjb3VudCB3aWxsIGJlIHJlYXBlZC4AGQEgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgYFNpZ25lZGAgYnkgdGhlIHRyYW5zYWN0b3IuACwgIyA8d2VpZ2h0PjEBIC0gRGVwZW5kZW50IG9uIGFyZ3VtZW50cyBidXQgbm90IGNyaXRpY2FsLCBnaXZlbiBwcm9wZXIgaW1wbGVtZW50YXRpb25zIGZvcswgICBpbnB1dCBjb25maWcgdHlwZXMuIFNlZSByZWxhdGVkIGZ1bmN0aW9ucyBiZWxvdy5pASAtIEl0IGNvbnRhaW5zIGEgbGltaXRlZCBudW1iZXIgb2YgcmVhZHMgYW5kIHdyaXRlcyBpbnRlcm5hbGx5IGFuZCBubyBjb21wbGV4IGNvbXB1dGF0aW9uLgBMIFJlbGF0ZWQgZnVuY3Rpb25zOgBRASAgIC0gYGVuc3VyZV9jYW5fd2l0aGRyYXdgIGlzIGFsd2F5cyBjYWxsZWQgaW50ZXJuYWxseSBidXQgaGFzIGEgYm91bmRlZCBjb21wbGV4aXR5Li0BICAgLSBUcmFuc2ZlcnJpbmcgYmFsYW5jZXMgdG8gYWNjb3VudHMgdGhhdCBkaWQgbm90IGV4aXN0IGJlZm9yZSB3aWxsIGNhdXNl1CAgICAgIGBUOjpPbk5ld0FjY291bnQ6Om9uX25ld19hY2NvdW50YCB0byBiZSBjYWxsZWQu3CAgIC0gUmVtb3ZpbmcgZW5vdWdoIGZ1bmRzIGZyb20gYW4gYWNjb3VudCB3aWxsIHRyaWdnZXJZASAgICAgYFQ6OkR1c3RSZW1vdmFsOjpvbl91bmJhbGFuY2VkYCBhbmQgYFQ6Ok9uRnJlZUJhbGFuY2VaZXJvOjpvbl9mcmVlX2JhbGFuY2VfemVyb2AuADAgIyA8L3dlaWdodD4sc2V0X2JhbGFuY2UMDHdobxxBZGRyZXNzIG5ld19mcmVlQENvbXBhY3Q8QmFsYW5jZT4wbmV3X3Jlc2VydmVkQENvbXBhY3Q8QmFsYW5jZT40lCBTZXQgdGhlIGJhbGFuY2VzIG9mIGEgZ2l2ZW4gYWNjb3VudC4AIQEgVGhpcyB3aWxsIGFsdGVyIGBGcmVlQmFsYW5jZWAgYW5kIGBSZXNlcnZlZEJhbGFuY2VgIGluIHN0b3JhZ2UuIGl0IHdpbGwJASBhbHNvIGRlY3JlYXNlIHRoZSB0b3RhbCBpc3N1YW5jZSBvZiB0aGUgc3lzdGVtIChgVG90YWxJc3N1YW5jZWApLhkBIElmIHRoZSBuZXcgZnJlZSBvciByZXNlcnZlZCBiYWxhbmNlIGlzIGJlbG93IHRoZSBleGlzdGVudGlhbCBkZXBvc2l0LOggaXQgd2lsbCByZXNldCB0aGUgYWNjb3VudCBub25jZSAoYHN5c3RlbTo6QWNjb3VudE5vbmNlYCkuALQgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIGlzIGByb290YC4ALCAjIDx3ZWlnaHQ+gCAtIEluZGVwZW5kZW50IG9mIHRoZSBhcmd1bWVudHMuxCAtIENvbnRhaW5zIGEgbGltaXRlZCBudW1iZXIgb2YgcmVhZHMgYW5kIHdyaXRlcy4wICMgPC93ZWlnaHQ+OGZvcmNlX3RyYW5zZmVyDBhzb3VyY2UcQWRkcmVzcxBkZXN0HEFkZHJlc3MUdmFsdWVAQ29tcGFjdDxCYWxhbmNlPghRASBFeGFjdGx5IGFzIGB0cmFuc2ZlcmAsIGV4Y2VwdCB0aGUgb3JpZ2luIG11c3QgYmUgcm9vdCBhbmQgdGhlIHNvdXJjZSBhY2NvdW50IG1heSBiZSwgc3BlY2lmaWVkLgAAHFN0YWtpbmcAATQQYm9uZAwoY29udHJvbGxlchxBZGRyZXNzFHZhbHVlSENvbXBhY3Q8QmFsYW5jZU9mPhRwYXllZURSZXdhcmREZXN0aW5hdGlvbjxlASBUYWtlIHRoZSBvcmlnaW4gYWNjb3VudCBhcyBhIHN0YXNoIGFuZCBsb2NrIHVwIGB2YWx1ZWAgb2YgaXRzIGJhbGFuY2UuIGBjb250cm9sbGVyYCB3aWxshCBiZSB0aGUgYWNjb3VudCB0aGF0IGNvbnRyb2xzIGl0LgAxASBgdmFsdWVgIG11c3QgYmUgbW9yZSB0aGFuIHRoZSBgbWluaW11bV9iYWxhbmNlYCBzcGVjaWZpZWQgYnkgYFQ6OkN1cnJlbmN5YC4AJQEgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgX1NpZ25lZF8gYnkgdGhlIHN0YXNoIGFjY291bnQuACwgIyA8d2VpZ2h0PtQgLSBJbmRlcGVuZGVudCBvZiB0aGUgYXJndW1lbnRzLiBNb2RlcmF0ZSBjb21wbGV4aXR5LiAgLSBPKDEpLmggLSBUaHJlZSBleHRyYSBEQiBlbnRyaWVzLgBtASBOT1RFOiBUd28gb2YgdGhlIHN0b3JhZ2Ugd3JpdGVzIChgU2VsZjo6Ym9uZGVkYCwgYFNlbGY6OnBheWVlYCkgYXJlIF9uZXZlcl8gY2xlYW5lZCB1bmxlc3MlASB0aGUgYG9yaWdpbmAgZmFsbHMgYmVsb3cgX2V4aXN0ZW50aWFsIGRlcG9zaXRfIGFuZCBnZXRzIHJlbW92ZWQgYXMgZHVzdC4wICMgPC93ZWlnaHQ+KGJvbmRfZXh0cmEEOG1heF9hZGRpdGlvbmFsSENvbXBhY3Q8QmFsYW5jZU9mPjhlASBBZGQgc29tZSBleHRyYSBhbW91bnQgdGhhdCBoYXZlIGFwcGVhcmVkIGluIHRoZSBzdGFzaCBgZnJlZV9iYWxhbmNlYCBpbnRvIHRoZSBiYWxhbmNlIHVwNCBmb3Igc3Rha2luZy4AUQEgVXNlIHRoaXMgaWYgdGhlcmUgYXJlIGFkZGl0aW9uYWwgZnVuZHMgaW4geW91ciBzdGFzaCBhY2NvdW50IHRoYXQgeW91IHdpc2ggdG8gYm9uZC5lASBVbmxpa2UgW2Bib25kYF0gb3IgW2B1bmJvbmRgXSB0aGlzIGZ1bmN0aW9uIGRvZXMgbm90IGltcG9zZSBhbnkgbGltaXRhdGlvbiBvbiB0aGUgYW1vdW50TCB0aGF0IGNhbiBiZSBhZGRlZC4AVQEgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgX1NpZ25lZF8gYnkgdGhlIHN0YXNoLCBub3QgdGhlIGNvbnRyb2xsZXIuACwgIyA8d2VpZ2h0PuggLSBJbmRlcGVuZGVudCBvZiB0aGUgYXJndW1lbnRzLiBJbnNpZ25pZmljYW50IGNvbXBsZXhpdHkuICAtIE8oMSkuQCAtIE9uZSBEQiBlbnRyeS4wICMgPC93ZWlnaHQ+GHVuYm9uZAQUdmFsdWVIQ29tcGFjdDxCYWxhbmNlT2Y+XFUBIFNjaGVkdWxlIGEgcG9ydGlvbiBvZiB0aGUgc3Rhc2ggdG8gYmUgdW5sb2NrZWQgcmVhZHkgZm9yIHRyYW5zZmVyIG91dCBhZnRlciB0aGUgYm9uZAEBIHBlcmlvZCBlbmRzLiBJZiB0aGlzIGxlYXZlcyBhbiBhbW91bnQgYWN0aXZlbHkgYm9uZGVkIGxlc3MgdGhhbiUBIFQ6OkN1cnJlbmN5OjptaW5pbXVtX2JhbGFuY2UoKSwgdGhlbiBpdCBpcyBpbmNyZWFzZWQgdG8gdGhlIGZ1bGwgYW1vdW50LgBJASBPbmNlIHRoZSB1bmxvY2sgcGVyaW9kIGlzIGRvbmUsIHlvdSBjYW4gY2FsbCBgd2l0aGRyYXdfdW5ib25kZWRgIHRvIGFjdHVhbGx5IG1vdmXAIHRoZSBmdW5kcyBvdXQgb2YgbWFuYWdlbWVudCByZWFkeSBmb3IgdHJhbnNmZXIuAD0BIE5vIG1vcmUgdGhhbiBhIGxpbWl0ZWQgbnVtYmVyIG9mIHVubG9ja2luZyBjaHVua3MgKHNlZSBgTUFYX1VOTE9DS0lOR19DSFVOS1NgKT0BIGNhbiBjby1leGlzdHMgYXQgdGhlIHNhbWUgdGltZS4gSW4gdGhhdCBjYXNlLCBbYENhbGw6OndpdGhkcmF3X3VuYm9uZGVkYF0gbmVlZPwgdG8gYmUgY2FsbGVkIGZpcnN0IHRvIHJlbW92ZSBzb21lIG9mIHRoZSBjaHVua3MgKGlmIHBvc3NpYmxlKS4AVQEgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgX1NpZ25lZF8gYnkgdGhlIGNvbnRyb2xsZXIsIG5vdCB0aGUgc3Rhc2guAJggU2VlIGFsc28gW2BDYWxsOjp3aXRoZHJhd191bmJvbmRlZGBdLgAsICMgPHdlaWdodD5BASAtIEluZGVwZW5kZW50IG9mIHRoZSBhcmd1bWVudHMuIExpbWl0ZWQgYnV0IHBvdGVudGlhbGx5IGV4cGxvaXRhYmxlIGNvbXBsZXhpdHkumCAtIENvbnRhaW5zIGEgbGltaXRlZCBudW1iZXIgb2YgcmVhZHMuZQEgLSBFYWNoIGNhbGwgKHJlcXVpcmVzIHRoZSByZW1haW5kZXIgb2YgdGhlIGJvbmRlZCBiYWxhbmNlIHRvIGJlIGFib3ZlIGBtaW5pbXVtX2JhbGFuY2VgKXEBICAgd2lsbCBjYXVzZSBhIG5ldyBlbnRyeSB0byBiZSBpbnNlcnRlZCBpbnRvIGEgdmVjdG9yIChgTGVkZ2VyLnVubG9ja2luZ2ApIGtlcHQgaW4gc3RvcmFnZS6lASAgIFRoZSBvbmx5IHdheSB0byBjbGVhbiB0aGUgYWZvcmVtZW50aW9uZWQgc3RvcmFnZSBpdGVtIGlzIGFsc28gdXNlci1jb250cm9sbGVkIHZpYSBgd2l0aGRyYXdfdW5ib25kZWRgLkAgLSBPbmUgREIgZW50cnkuKCA8L3dlaWdodD5Ed2l0aGRyYXdfdW5ib25kZWQAQC0BIFJlbW92ZSBhbnkgdW5sb2NrZWQgY2h1bmtzIGZyb20gdGhlIGB1bmxvY2tpbmdgIHF1ZXVlIGZyb20gb3VyIG1hbmFnZW1lbnQuADUBIFRoaXMgZXNzZW50aWFsbHkgZnJlZXMgdXAgdGhhdCBiYWxhbmNlIHRvIGJlIHVzZWQgYnkgdGhlIHN0YXNoIGFjY291bnQgdG8gZG9MIHdoYXRldmVyIGl0IHdhbnRzLgBVASBUaGUgZGlzcGF0Y2ggb3JpZ2luIGZvciB0aGlzIGNhbGwgbXVzdCBiZSBfU2lnbmVkXyBieSB0aGUgY29udHJvbGxlciwgbm90IHRoZSBzdGFzaC4AbCBTZWUgYWxzbyBbYENhbGw6OnVuYm9uZGBdLgAsICMgPHdlaWdodD5VASAtIENvdWxkIGJlIGRlcGVuZGVudCBvbiB0aGUgYG9yaWdpbmAgYXJndW1lbnQgYW5kIGhvdyBtdWNoIGB1bmxvY2tpbmdgIGNodW5rcyBleGlzdC5FASAgSXQgaW1wbGllcyBgY29uc29saWRhdGVfdW5sb2NrZWRgIHdoaWNoIGxvb3BzIG92ZXIgYExlZGdlci51bmxvY2tpbmdgLCB3aGljaCBpc/QgIGluZGlyZWN0bHkgdXNlci1jb250cm9sbGVkLiBTZWUgW2B1bmJvbmRgXSBmb3IgbW9yZSBkZXRhaWwueQEgLSBDb250YWlucyBhIGxpbWl0ZWQgbnVtYmVyIG9mIHJlYWRzLCB5ZXQgdGhlIHNpemUgb2Ygd2hpY2ggY291bGQgYmUgbGFyZ2UgYmFzZWQgb24gYGxlZGdlcmAuyCAtIFdyaXRlcyBhcmUgbGltaXRlZCB0byB0aGUgYG9yaWdpbmAgYWNjb3VudCBrZXkuMCAjIDwvd2VpZ2h0PiB2YWxpZGF0ZQQUcHJlZnM4VmFsaWRhdG9yUHJlZnMs6CBEZWNsYXJlIHRoZSBkZXNpcmUgdG8gdmFsaWRhdGUgZm9yIHRoZSBvcmlnaW4gY29udHJvbGxlci4A3CBFZmZlY3RzIHdpbGwgYmUgZmVsdCBhdCB0aGUgYmVnaW5uaW5nIG9mIHRoZSBuZXh0IGVyYS4AVQEgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgX1NpZ25lZF8gYnkgdGhlIGNvbnRyb2xsZXIsIG5vdCB0aGUgc3Rhc2guACwgIyA8d2VpZ2h0PuggLSBJbmRlcGVuZGVudCBvZiB0aGUgYXJndW1lbnRzLiBJbnNpZ25pZmljYW50IGNvbXBsZXhpdHkumCAtIENvbnRhaW5zIGEgbGltaXRlZCBudW1iZXIgb2YgcmVhZHMuyCAtIFdyaXRlcyBhcmUgbGltaXRlZCB0byB0aGUgYG9yaWdpbmAgYWNjb3VudCBrZXkuMCAjIDwvd2VpZ2h0PiBub21pbmF0ZQQcdGFyZ2V0czBWZWM8QWRkcmVzcz4sEQEgRGVjbGFyZSB0aGUgZGVzaXJlIHRvIG5vbWluYXRlIGB0YXJnZXRzYCBmb3IgdGhlIG9yaWdpbiBjb250cm9sbGVyLgDcIEVmZmVjdHMgd2lsbCBiZSBmZWx0IGF0IHRoZSBiZWdpbm5pbmcgb2YgdGhlIG5leHQgZXJhLgBVASBUaGUgZGlzcGF0Y2ggb3JpZ2luIGZvciB0aGlzIGNhbGwgbXVzdCBiZSBfU2lnbmVkXyBieSB0aGUgY29udHJvbGxlciwgbm90IHRoZSBzdGFzaC4ALCAjIDx3ZWlnaHQ+JQEgLSBUaGUgdHJhbnNhY3Rpb24ncyBjb21wbGV4aXR5IGlzIHByb3BvcnRpb25hbCB0byB0aGUgc2l6ZSBvZiBgdGFyZ2V0c2AsmCB3aGljaCBpcyBjYXBwZWQgYXQgYE1BWF9OT01JTkFUSU9OU2Au2CAtIEJvdGggdGhlIHJlYWRzIGFuZCB3cml0ZXMgZm9sbG93IGEgc2ltaWxhciBwYXR0ZXJuLjAgIyA8L3dlaWdodD4UY2hpbGwALMggRGVjbGFyZSBubyBkZXNpcmUgdG8gZWl0aGVyIHZhbGlkYXRlIG9yIG5vbWluYXRlLgDcIEVmZmVjdHMgd2lsbCBiZSBmZWx0IGF0IHRoZSBiZWdpbm5pbmcgb2YgdGhlIG5leHQgZXJhLgBVASBUaGUgZGlzcGF0Y2ggb3JpZ2luIGZvciB0aGlzIGNhbGwgbXVzdCBiZSBfU2lnbmVkXyBieSB0aGUgY29udHJvbGxlciwgbm90IHRoZSBzdGFzaC4ALCAjIDx3ZWlnaHQ+6CAtIEluZGVwZW5kZW50IG9mIHRoZSBhcmd1bWVudHMuIEluc2lnbmlmaWNhbnQgY29tcGxleGl0eS5UIC0gQ29udGFpbnMgb25lIHJlYWQuyCAtIFdyaXRlcyBhcmUgbGltaXRlZCB0byB0aGUgYG9yaWdpbmAgYWNjb3VudCBrZXkuMCAjIDwvd2VpZ2h0PiRzZXRfcGF5ZWUEFHBheWVlRFJld2FyZERlc3RpbmF0aW9uLLggKFJlLSlzZXQgdGhlIHBheW1lbnQgdGFyZ2V0IGZvciBhIGNvbnRyb2xsZXIuANwgRWZmZWN0cyB3aWxsIGJlIGZlbHQgYXQgdGhlIGJlZ2lubmluZyBvZiB0aGUgbmV4dCBlcmEuAFUBIFRoZSBkaXNwYXRjaCBvcmlnaW4gZm9yIHRoaXMgY2FsbCBtdXN0IGJlIF9TaWduZWRfIGJ5IHRoZSBjb250cm9sbGVyLCBub3QgdGhlIHN0YXNoLgAsICMgPHdlaWdodD7oIC0gSW5kZXBlbmRlbnQgb2YgdGhlIGFyZ3VtZW50cy4gSW5zaWduaWZpY2FudCBjb21wbGV4aXR5LpggLSBDb250YWlucyBhIGxpbWl0ZWQgbnVtYmVyIG9mIHJlYWRzLsggLSBXcml0ZXMgYXJlIGxpbWl0ZWQgdG8gdGhlIGBvcmlnaW5gIGFjY291bnQga2V5LjAgIyA8L3dlaWdodD44c2V0X2NvbnRyb2xsZXIEKGNvbnRyb2xsZXIcQWRkcmVzcyyQIChSZS0pc2V0IHRoZSBjb250cm9sbGVyIG9mIGEgc3Rhc2guANwgRWZmZWN0cyB3aWxsIGJlIGZlbHQgYXQgdGhlIGJlZ2lubmluZyBvZiB0aGUgbmV4dCBlcmEuAFUBIFRoZSBkaXNwYXRjaCBvcmlnaW4gZm9yIHRoaXMgY2FsbCBtdXN0IGJlIF9TaWduZWRfIGJ5IHRoZSBzdGFzaCwgbm90IHRoZSBjb250cm9sbGVyLgAsICMgPHdlaWdodD7oIC0gSW5kZXBlbmRlbnQgb2YgdGhlIGFyZ3VtZW50cy4gSW5zaWduaWZpY2FudCBjb21wbGV4aXR5LpggLSBDb250YWlucyBhIGxpbWl0ZWQgbnVtYmVyIG9mIHJlYWRzLsggLSBXcml0ZXMgYXJlIGxpbWl0ZWQgdG8gdGhlIGBvcmlnaW5gIGFjY291bnQga2V5LjAgIyA8L3dlaWdodD5Mc2V0X3ZhbGlkYXRvcl9jb3VudAQMbmV3MENvbXBhY3Q8dTMyPgSAIFRoZSBpZGVhbCBudW1iZXIgb2YgdmFsaWRhdG9ycy40Zm9yY2Vfbm9fZXJhcwAUsCBGb3JjZSB0aGVyZSB0byBiZSBubyBuZXcgZXJhcyBpbmRlZmluaXRlbHkuACwgIyA8d2VpZ2h0PkAgLSBObyBhcmd1bWVudHMuMCAjIDwvd2VpZ2h0PjRmb3JjZV9uZXdfZXJhABhNASBGb3JjZSB0aGVyZSB0byBiZSBhIG5ldyBlcmEgYXQgdGhlIGVuZCBvZiB0aGUgbmV4dCBzZXNzaW9uLiBBZnRlciB0aGlzLCBpdCB3aWxsIGJloCByZXNldCB0byBub3JtYWwgKG5vbi1mb3JjZWQpIGJlaGF2aW91ci4ALCAjIDx3ZWlnaHQ+QCAtIE5vIGFyZ3VtZW50cy4wICMgPC93ZWlnaHQ+RHNldF9pbnZ1bG5lcmFibGVzBCh2YWxpZGF0b3JzOFZlYzxBY2NvdW50SWQ+BMwgU2V0IHRoZSB2YWxpZGF0b3JzIHdobyBjYW5ub3QgYmUgc2xhc2hlZCAoaWYgYW55KS4AABxTZXNzaW9uAAEEIHNldF9rZXlzCBBrZXlzEEtleXMUcHJvb2YUQnl0ZXMo5CBTZXRzIHRoZSBzZXNzaW9uIGtleShzKSBvZiB0aGUgZnVuY3Rpb24gY2FsbGVyIHRvIGBrZXlgLiEBIEFsbG93cyBhbiBhY2NvdW50IHRvIHNldCBpdHMgc2Vzc2lvbiBrZXkgcHJpb3IgdG8gYmVjb21pbmcgYSB2YWxpZGF0b3IuxCBUaGlzIGRvZXNuJ3QgdGFrZSBlZmZlY3QgdW50aWwgdGhlIG5leHQgc2Vzc2lvbi4A1CBUaGUgZGlzcGF0Y2ggb3JpZ2luIG9mIHRoaXMgZnVuY3Rpb24gbXVzdCBiZSBzaWduZWQuACwgIyA8d2VpZ2h0PoggLSBPKGxvZyBuKSBpbiBudW1iZXIgb2YgYWNjb3VudHMuWCAtIE9uZSBleHRyYSBEQiBlbnRyeS4wICMgPC93ZWlnaHQ+AAAkRGVtb2NyYWN5AAFEHHByb3Bvc2UIIHByb3Bvc2FsIFByb3Bvc2FsFHZhbHVlSENvbXBhY3Q8QmFsYW5jZU9mPhigIFByb3Bvc2UgYSBzZW5zaXRpdmUgYWN0aW9uIHRvIGJlIHRha2VuLgAsICMgPHdlaWdodD4gIC0gTygxKS6AIC0gVHdvIERCIGNoYW5nZXMsIG9uZSBEQiBlbnRyeS4wICMgPC93ZWlnaHQ+GHNlY29uZAQgcHJvcG9zYWxIQ29tcGFjdDxQcm9wSW5kZXg+GKAgUHJvcG9zZSBhIHNlbnNpdGl2ZSBhY3Rpb24gdG8gYmUgdGFrZW4uACwgIyA8d2VpZ2h0PiAgLSBPKDEpLkAgLSBPbmUgREIgZW50cnkuMCAjIDwvd2VpZ2h0PhB2b3RlCCRyZWZfaW5kZXhgQ29tcGFjdDxSZWZlcmVuZHVtSW5kZXg+EHZvdGUQVm90ZRw1ASBWb3RlIGluIGEgcmVmZXJlbmR1bS4gSWYgYHZvdGUuaXNfYXllKClgLCB0aGUgdm90ZSBpcyB0byBlbmFjdCB0aGUgcHJvcG9zYWw7vCBvdGhlcndpc2UgaXQgaXMgYSB2b3RlIHRvIGtlZXAgdGhlIHN0YXR1cyBxdW8uACwgIyA8d2VpZ2h0PiAgLSBPKDEpLnwgLSBPbmUgREIgY2hhbmdlLCBvbmUgREIgZW50cnkuMCAjIDwvd2VpZ2h0Pihwcm94eV92b3RlCCRyZWZfaW5kZXhgQ29tcGFjdDxSZWZlcmVuZHVtSW5kZXg+EHZvdGUQVm90ZRxRASBWb3RlIGluIGEgcmVmZXJlbmR1bSBvbiBiZWhhbGYgb2YgYSBzdGFzaC4gSWYgYHZvdGUuaXNfYXllKClgLCB0aGUgdm90ZSBpcyB0byBlbmFjdPggdGhlIHByb3Bvc2FsOyAgb3RoZXJ3aXNlIGl0IGlzIGEgdm90ZSB0byBrZWVwIHRoZSBzdGF0dXMgcXVvLgAsICMgPHdlaWdodD4gIC0gTygxKS58IC0gT25lIERCIGNoYW5nZSwgb25lIERCIGVudHJ5LjAgIyA8L3dlaWdodD5AZW1lcmdlbmN5X2NhbmNlbAQkcmVmX2luZGV4PFJlZmVyZW5kdW1JbmRleAhRASBTY2hlZHVsZSBhbiBlbWVyZ2VuY3kgY2FuY2VsbGF0aW9uIG9mIGEgcmVmZXJlbmR1bS4gQ2Fubm90IGhhcHBlbiB0d2ljZSB0byB0aGUgc2FtZTAgcmVmZXJlbmR1bS5AZXh0ZXJuYWxfcHJvcG9zZQQgcHJvcG9zYWwgUHJvcG9zYWwIMQEgU2NoZWR1bGUgYSByZWZlcmVuZHVtIHRvIGJlIHRhYmxlZCBvbmNlIGl0IGlzIGxlZ2FsIHRvIHNjaGVkdWxlIGFuIGV4dGVybmFsMCByZWZlcmVuZHVtLmRleHRlcm5hbF9wcm9wb3NlX21ham9yaXR5BCBwcm9wb3NhbCBQcm9wb3NhbBRZASBTY2hlZHVsZSBhIG1ham9yaXR5LWNhcnJpZXMgcmVmZXJlbmR1bSB0byBiZSB0YWJsZWQgbmV4dCBvbmNlIGl0IGlzIGxlZ2FsIHRvIHNjaGVkdWxlYCBhbiBleHRlcm5hbCByZWZlcmVuZHVtLgBNASBVbmxpa2UgYGV4dGVybmFsX3Byb3Bvc2VgLCBibGFja2xpc3RpbmcgaGFzIG5vIGVmZmVjdCBvbiB0aGlzIGFuZCBpdCBtYXkgcmVwbGFjZSBhnCBwcmUtc2NoZWR1bGVkIGBleHRlcm5hbF9wcm9wb3NlYCBjYWxsLmBleHRlcm5hbF9wcm9wb3NlX2RlZmF1bHQEIHByb3Bvc2FsIFByb3Bvc2FsFEkBIFNjaGVkdWxlIGEgbmVnYXRpdmUtdHVybm91dC1iaWFzIHJlZmVyZW5kdW0gdG8gYmUgdGFibGVkIG5leHQgb25jZSBpdCBpcyBsZWdhbCB0b4Qgc2NoZWR1bGUgYW4gZXh0ZXJuYWwgcmVmZXJlbmR1bS4ATQEgVW5saWtlIGBleHRlcm5hbF9wcm9wb3NlYCwgYmxhY2tsaXN0aW5nIGhhcyBubyBlZmZlY3Qgb24gdGhpcyBhbmQgaXQgbWF5IHJlcGxhY2UgYZwgcHJlLXNjaGVkdWxlZCBgZXh0ZXJuYWxfcHJvcG9zZWAgY2FsbC4oZmFzdF90cmFjaww0cHJvcG9zYWxfaGFzaBBIYXNoNHZvdGluZ19wZXJpb2QsQmxvY2tOdW1iZXIUZGVsYXksQmxvY2tOdW1iZXIgUQEgU2NoZWR1bGUgdGhlIGN1cnJlbnRseSBleHRlcm5hbGx5LXByb3Bvc2VkIG1ham9yaXR5LWNhcnJpZXMgcmVmZXJlbmR1bSB0byBiZSB0YWJsZWRlASBpbW1lZGlhdGVseS4gSWYgdGhlcmUgaXMgbm8gZXh0ZXJuYWxseS1wcm9wb3NlZCByZWZlcmVuZHVtIGN1cnJlbnRseSwgb3IgaWYgdGhlcmUgaXMgb25l7CBidXQgaXQgaXMgbm90IGEgbWFqb3JpdHktY2FycmllcyByZWZlcmVuZHVtIHRoZW4gaXQgZmFpbHMuAPggLSBgcHJvcG9zYWxfaGFzaGA6IFRoZSBoYXNoIG9mIHRoZSBjdXJyZW50IGV4dGVybmFsIHByb3Bvc2FsLi0BIC0gYHZvdGluZ19wZXJpb2RgOiBUaGUgcGVyaW9kIHRoYXQgaXMgYWxsb3dlZCBmb3Igdm90aW5nIG9uIHRoaXMgcHJvcG9zYWwuVQEgLSBgZGVsYXlgOiBUaGUgbnVtYmVyIG9mIGJsb2NrIGFmdGVyIHZvdGluZyBoYXMgZW5kZWQgaW4gYXBwcm92YWwgYW5kIHRoaXMgc2hvdWxkIGJl8CAgIGVuYWN0ZWQuIEluY3JlYXNlZCB0byBgRW1lcmdlbmN5Vm90aW5nUGVyaW9kYCBpZiB0b28gbG93LjR2ZXRvX2V4dGVybmFsBDRwcm9wb3NhbF9oYXNoEEhhc2gEvCBWZXRvIGFuZCBibGFja2xpc3QgdGhlIGV4dGVybmFsIHByb3Bvc2FsIGhhc2guRGNhbmNlbF9yZWZlcmVuZHVtBCRyZWZfaW5kZXhgQ29tcGFjdDxSZWZlcmVuZHVtSW5kZXg+BFQgUmVtb3ZlIGEgcmVmZXJlbmR1bS40Y2FuY2VsX3F1ZXVlZAwQd2hlblBDb21wYWN0PEJsb2NrTnVtYmVyPhR3aGljaDBDb21wYWN0PHUzMj4Qd2hhdGBDb21wYWN0PFJlZmVyZW5kdW1JbmRleD4EoCBDYW5jZWwgYSBwcm9wb3NhbCBxdWV1ZWQgZm9yIGVuYWN0bWVudC4kc2V0X3Byb3h5BBRwcm94eSRBY2NvdW50SWQUmCBTcGVjaWZ5IGEgcHJveHkuIENhbGxlZCBieSB0aGUgc3Rhc2guACwgIyA8d2VpZ2h0PlggLSBPbmUgZXh0cmEgREIgZW50cnkuMCAjIDwvd2VpZ2h0PjByZXNpZ25fcHJveHkAFJggQ2xlYXIgdGhlIHByb3h5LiBDYWxsZWQgYnkgdGhlIHByb3h5LgAsICMgPHdlaWdodD5AIC0gT25lIERCIGNsZWFyLjAgIyA8L3dlaWdodD4wcmVtb3ZlX3Byb3h5BBRwcm94eSRBY2NvdW50SWQUmCBDbGVhciB0aGUgcHJveHkuIENhbGxlZCBieSB0aGUgc3Rhc2guACwgIyA8d2VpZ2h0PkAgLSBPbmUgREIgY2xlYXIuMCAjIDwvd2VpZ2h0PiBkZWxlZ2F0ZQgIdG8kQWNjb3VudElkKGNvbnZpY3Rpb24oQ29udmljdGlvbhQ8IERlbGVnYXRlIHZvdGUuACwgIyA8d2VpZ2h0PlggLSBPbmUgZXh0cmEgREIgZW50cnkuMCAjIDwvd2VpZ2h0Pih1bmRlbGVnYXRlABREIFVuZGVsZWdhdGUgdm90ZS4ALCAjIDx3ZWlnaHQ+ICAtIE8oMSkuMCAjIDwvd2VpZ2h0PgAAHENvdW5jaWwAARAsc2V0X21lbWJlcnMELG5ld19tZW1iZXJzOFZlYzxBY2NvdW50SWQ+EFEBIFNldCB0aGUgY29sbGVjdGl2ZSdzIG1lbWJlcnNoaXAgbWFudWFsbHkgdG8gYG5ld19tZW1iZXJzYC4gQmUgbmljZSB0byB0aGUgY2hhaW4gYW5kXCBwcm92aWRlIGl0IHByZS1zb3J0ZWQuAFggUmVxdWlyZXMgcm9vdCBvcmlnaW4uHGV4ZWN1dGUEIHByb3Bvc2FsIFByb3Bvc2FsDPQgRGlzcGF0Y2ggYSBwcm9wb3NhbCBmcm9tIGEgbWVtYmVyIHVzaW5nIHRoZSBgTWVtYmVyYCBvcmlnaW4uAKwgT3JpZ2luIG11c3QgYmUgYSBtZW1iZXIgb2YgdGhlIGNvbGxlY3RpdmUuHHByb3Bvc2UIJHRocmVzaG9sZFBDb21wYWN0PE1lbWJlckNvdW50PiBwcm9wb3NhbCBQcm9wb3NhbBAsICMgPHdlaWdodD6QIC0gQm91bmRlZCBzdG9yYWdlIHJlYWRzIGFuZCB3cml0ZXMuuCAtIEFyZ3VtZW50IGB0aHJlc2hvbGRgIGhhcyBiZWFyaW5nIG9uIHdlaWdodC4wICMgPC93ZWlnaHQ+EHZvdGUMIHByb3Bvc2FsEEhhc2gUaW5kZXhYQ29tcGFjdDxQcm9wb3NhbEluZGV4PhxhcHByb3ZlEGJvb2wQLCAjIDx3ZWlnaHQ+jCAtIEJvdW5kZWQgc3RvcmFnZSByZWFkIGFuZCB3cml0ZXMuVQEgLSBXaWxsIGJlIHNsaWdodGx5IGhlYXZpZXIgaWYgdGhlIHByb3Bvc2FsIGlzIGFwcHJvdmVkIC8gZGlzYXBwcm92ZWQgYWZ0ZXIgdGhlIHZvdGUuMCAjIDwvd2VpZ2h0PgAAJEVsZWN0aW9ucwABKDRzZXRfYXBwcm92YWxzDBR2b3RlcyRWZWM8Ym9vbD4UaW5kZXhIQ29tcGFjdDxWb3RlSW5kZXg+EGhpbnQgU2V0SW5kZXhgYQEgU2V0IGNhbmRpZGF0ZSBhcHByb3ZhbHMuIEFwcHJvdmFsIHNsb3RzIHN0YXkgdmFsaWQgYXMgbG9uZyBhcyBjYW5kaWRhdGVzIGluIHRob3NlIHNsb3RzQCBhcmUgcmVnaXN0ZXJlZC4AwCBMb2NrcyB0aGUgdG90YWwgYmFsYW5jZSBvZiBjYWxsZXIgaW5kZWZpbml0ZWx5LikBIE9ubHkgW2ByZXRyYWN0X3ZvdGVyYF0gb3IgW2ByZWFwX2luYWN0aXZlX3ZvdGVyYF0gY2FuIHVubG9jayB0aGUgYmFsYW5jZS4A1CBgaGludGAgYXJndW1lbnQgaXMgaW50ZXJwcmV0ZWQgZGlmZmVyZW50bHkgYmFzZWQgb246RQEgLSBpZiBgb3JpZ2luYCBpcyBzZXR0aW5nIGFwcHJvdmFscyBmb3IgdGhlIGZpcnN0IHRpbWU6IFRoZSBpbmRleCB3aWxsIGJlIGNoZWNrZWSwIGZvciBiZWluZyBhIHZhbGlkIF9ob2xlXyBpbiB0aGUgdm90ZXIgbGlzdC5NASAgIC0gaWYgdGhlIGhpbnQgaXMgY29ycmVjdGx5IHBvaW50aW5nIHRvIGEgaG9sZSwgbm8gZmVlIGlzIGRlZHVjdGVkIGZyb20gYG9yaWdpbmAuiQEgICAtIE90aGVyd2lzZSwgdGhlIGNhbGwgd2lsbCBzdWNjZWVkIGJ1dCB0aGUgaW5kZXggaXMgaWdub3JlZCBhbmQgc2ltcGx5IGEgcHVzaCB0byB0aGUgbGFzdCBjaHVua4EBICAgd2l0aCBmcmVlIHNwYWNlIGhhcHBlbnMuIElmIHRoZSBuZXcgcHVzaCBjYXVzZXMgYSBuZXcgY2h1bmsgdG8gYmUgY3JlYXRlZCwgYSBmZWUgaW5kaWNhdGVkIGJ5dCAgIFtgVm90aW5nRmVlYF0gaXMgZGVkdWN0ZWQuXQEgLSBpZiBgb3JpZ2luYCBpcyBhbHJlYWR5IGEgdm90ZXI6IHRoZSBpbmRleCBfX211c3RfXyBiZSB2YWxpZCBhbmQgcG9pbnQgdG8gdGhlIGNvcnJlY3TUIHBvc2l0aW9uIG9mIHRoZSBgb3JpZ2luYCBpbiB0aGUgY3VycmVudCB2b3RlcnMgbGlzdC4ArQEgTm90ZSB0aGF0IGFueSB0cmFpbGluZyBgZmFsc2VgIHZvdGVzIGluIGB2b3Rlc2AgaXMgaWdub3JlZDsgSW4gYXBwcm92YWwgdm90aW5nLCBub3Qgdm90aW5nIGZvciBhIGNhbmRpZGF0ZXQgYW5kIHZvdGluZyBmYWxzZSwgYXJlIGVxdWFsLgAsICMgPHdlaWdodD4gIC0gTygxKS6cIC0gVHdvIGV4dHJhIERCIGVudHJpZXMsIG9uZSBEQiBjaGFuZ2UuBQEgLSBBcmd1bWVudCBgdm90ZXNgIGlzIGxpbWl0ZWQgaW4gbGVuZ3RoIHRvIG51bWJlciBvZiBjYW5kaWRhdGVzLjAgIyA8L3dlaWdodD5McHJveHlfc2V0X2FwcHJvdmFscwwUdm90ZXMkVmVjPGJvb2w+FGluZGV4SENvbXBhY3Q8Vm90ZUluZGV4PhBoaW50IFNldEluZGV4GJUBIFNldCBjYW5kaWRhdGUgYXBwcm92YWxzIGZyb20gYSBwcm94eS4gQXBwcm92YWwgc2xvdHMgc3RheSB2YWxpZCBhcyBsb25nIGFzIGNhbmRpZGF0ZXMgaW4gdGhvc2Ugc2xvdHNAIGFyZSByZWdpc3RlcmVkLgAsICMgPHdlaWdodD7wIC0gU2FtZSBhcyBgc2V0X2FwcHJvdmFsc2Agd2l0aCBvbmUgYWRkaXRpb25hbCBzdG9yYWdlIHJlYWQuMCAjIDwvd2VpZ2h0PkxyZWFwX2luYWN0aXZlX3ZvdGVyEDhyZXBvcnRlcl9pbmRleDBDb21wYWN0PHUzMj4Md2hvHEFkZHJlc3Mkd2hvX2luZGV4MENvbXBhY3Q8dTMyPkhhc3N1bWVkX3ZvdGVfaW5kZXhIQ29tcGFjdDxWb3RlSW5kZXg+MGEBIFJlbW92ZSBhIHZvdGVyLiBGb3IgaXQgbm90IHRvIGJlIGEgYm9uZC1jb25zdW1pbmcgbm8tb3AsIGFsbCBhcHByb3ZlZCBjYW5kaWRhdGUgaW5kaWNlc3EBIG11c3Qgbm93IGJlIGVpdGhlciB1bnJlZ2lzdGVyZWQgb3IgcmVnaXN0ZXJlZCB0byBhIGNhbmRpZGF0ZSB0aGF0IHJlZ2lzdGVyZWQgdGhlIHNsb3QgYWZ0ZXKgIHRoZSB2b3RlciBnYXZlIHRoZWlyIGxhc3QgYXBwcm92YWwgc2V0LgAVASBCb3RoIGluZGljZXMgbXVzdCBiZSBwcm92aWRlZCBhcyBleHBsYWluZWQgaW4gW2B2b3Rlcl9hdGBdIGZ1bmN0aW9uLgABASBNYXkgYmUgY2FsbGVkIGJ5IGFueW9uZS4gUmV0dXJucyB0aGUgdm90ZXIgZGVwb3NpdCB0byBgc2lnbmVkYC4ALCAjIDx3ZWlnaHQ+ICAtIE8oMSkunCAtIFR3byBmZXdlciBEQiBlbnRyaWVzLCBvbmUgREIgY2hhbmdlLjAgIyA8L3dlaWdodD40cmV0cmFjdF92b3RlcgQUaW5kZXgwQ29tcGFjdDx1MzI+KC0BIFJlbW92ZSBhIHZvdGVyLiBBbGwgdm90ZXMgYXJlIGNhbmNlbGxlZCBhbmQgdGhlIHZvdGVyIGRlcG9zaXQgaXMgcmV0dXJuZWQuAAkBIFRoZSBpbmRleCBtdXN0IGJlIHByb3ZpZGVkIGFzIGV4cGxhaW5lZCBpbiBbYHZvdGVyX2F0YF0gZnVuY3Rpb24uAD0BIEFsc28gcmVtb3ZlcyB0aGUgbG9jayBvbiB0aGUgYmFsYW5jZSBvZiB0aGUgdm90ZXIuIFNlZSBbYGRvX3NldF9hcHByb3ZhbHMoKWBdLgAsICMgPHdlaWdodD4gIC0gTygxKS6cIC0gVHdvIGZld2VyIERCIGVudHJpZXMsIG9uZSBEQiBjaGFuZ2UuMCAjIDwvd2VpZ2h0PkBzdWJtaXRfY2FuZGlkYWN5BBBzbG90MENvbXBhY3Q8dTMyPjR4IFN1Ym1pdCBvbmVzZWxmIGZvciBjYW5kaWRhY3kuABEBIEFjY291bnQgbXVzdCBoYXZlIGVub3VnaCB0cmFuc2ZlcnJhYmxlIGZ1bmRzIGluIGl0IHRvIHBheSB0aGUgYm9uZC4AIQEgTk9URTogaWYgYG9yaWdpbmAgaGFzIGFscmVhZHkgYXNzaWduZWQgYXBwcm92YWxzIHZpYSBbYHNldF9hcHByb3ZhbHNgXSxFASBpdCB3aWxsIE5PVCBoYXZlIGFueSB1c2FibGUgZnVuZHMgdG8gcGFzcyBjYW5kaWRhY3kgYm9uZCBhbmQgbXVzdCBmaXJzdCByZXRyYWN0LjEBIE5vdGUgdGhhdCBzZXR0aW5nIGFwcHJvdmFscyB3aWxsIGxvY2sgdGhlIGVudGlyZSBiYWxhbmNlIG9mIHRoZSB2b3RlciB1bnRpbHggcmV0cmFjdGlvbiBvciBiZWluZyByZXBvcnRlZC4ALCAjIDx3ZWlnaHQ+YCAtIEluZGVwZW5kZW50IG9mIGlucHV0LlAgLSBUaHJlZSBEQiBjaGFuZ2VzLjAgIyA8L3dlaWdodD44cHJlc2VudF93aW5uZXIMJGNhbmRpZGF0ZRxBZGRyZXNzFHRvdGFsSENvbXBhY3Q8QmFsYW5jZU9mPhRpbmRleEhDb21wYWN0PFZvdGVJbmRleD4gZQEgQ2xhaW0gdGhhdCBgc2lnbmVkYCBpcyBvbmUgb2YgdGhlIHRvcCBTZWxmOjpjYXJyeV9jb3VudCgpICsgY3VycmVudF92b3RlKCkuMSBjYW5kaWRhdGVzLqEBIE9ubHkgd29ya3MgaWYgdGhlIGBibG9ja19udW1iZXIgPj0gY3VycmVudF92b3RlKCkuMGAgYW5kIGA8IGN1cnJlbnRfdm90ZSgpLjAgKyBwcmVzZW50YXRpb25fZHVyYXRpb24oKWB4IGBzaWduZWRgIHNob3VsZCBoYXZlIGF0IGxlYXN0ACwgIyA8d2VpZ2h0PlQgLSBPKHZvdGVycykgY29tcHV0ZS5EIC0gT25lIERCIGNoYW5nZS4wICMgPC93ZWlnaHQ+RHNldF9kZXNpcmVkX3NlYXRzBBRjb3VudDBDb21wYWN0PHUzMj4MZQEgU2V0IHRoZSBkZXNpcmVkIG1lbWJlciBjb3VudDsgaWYgbG93ZXIgdGhhbiB0aGUgY3VycmVudCBjb3VudCwgdGhlbiBzZWF0cyB3aWxsIG5vdCBiZSB1cEkBIGVsZWN0aW9uIHdoZW4gdGhleSBleHBpcmUuIElmIG1vcmUsIHRoZW4gYSBuZXcgdm90ZSB3aWxsIGJlIHN0YXJ0ZWQgaWYgb25lIGlzIG5vdFQgYWxyZWFkeSBpbiBwcm9ncmVzcy40cmVtb3ZlX21lbWJlcgQMd2hvHEFkZHJlc3MQIQEgUmVtb3ZlIGEgcGFydGljdWxhciBtZW1iZXIgZnJvbSB0aGUgc2V0LiBUaGlzIGlzIGVmZmVjdGl2ZSBpbW1lZGlhdGVseS4AIQEgTm90ZTogQSB0YWxseSBzaG91bGQgaGFwcGVuIGluc3RhbnRseSAoaWYgbm90IGFscmVhZHkgaW4gYSBwcmVzZW50YXRpb25BASBwZXJpb2QpIHRvIGZpbGwgdGhlIHNlYXQgaWYgcmVtb3ZhbCBtZWFucyB0aGF0IHRoZSBkZXNpcmVkIG1lbWJlcnMgYXJlIG5vdCBtZXQuZHNldF9wcmVzZW50YXRpb25fZHVyYXRpb24EFGNvdW50UENvbXBhY3Q8QmxvY2tOdW1iZXI+CFkBIFNldCB0aGUgcHJlc2VudGF0aW9uIGR1cmF0aW9uLiBJZiB0aGVyZSBpcyBjdXJyZW50bHkgYSB2b3RlIGJlaW5nIHByZXNlbnRlZCBmb3IsIHdpbGxgIGludm9rZSBgZmluYWxpemVfdm90ZWAuRHNldF90ZXJtX2R1cmF0aW9uBBRjb3VudFBDb21wYWN0PEJsb2NrTnVtYmVyPghRASBTZXQgdGhlIHByZXNlbnRhdGlvbiBkdXJhdGlvbi4gSWYgdGhlcmUgaXMgY3VycmVudCBhIHZvdGUgYmVpbmcgcHJlc2VudGVkIGZvciwgd2lsbGAgaW52b2tlIGBmaW5hbGl6ZV92b3RlYC4AADxGaW5hbGl0eVRyYWNrZXIAAQQoZmluYWxfaGludAQQaGludFBDb21wYWN0PEJsb2NrTnVtYmVyPgj0IEhpbnQgdGhhdCB0aGUgYXV0aG9yIG9mIHRoaXMgYmxvY2sgdGhpbmtzIHRoZSBiZXN0IGZpbmFsaXplZGwgYmxvY2sgaXMgdGhlIGdpdmVuIG51bWJlci4AABxHcmFuZHBhAAEESHJlcG9ydF9taXNiZWhhdmlvcgQcX3JlcG9ydBRCeXRlcwRkIFJlcG9ydCBzb21lIG1pc2JlaGF2aW9yLgAAIFRyZWFzdXJ5AAEMNHByb3Bvc2Vfc3BlbmQIFHZhbHVlSENvbXBhY3Q8QmFsYW5jZU9mPixiZW5lZmljaWFyeRxBZGRyZXNzJC0BIFB1dCBmb3J3YXJkIGEgc3VnZ2VzdGlvbiBmb3Igc3BlbmRpbmcuIEEgZGVwb3NpdCBwcm9wb3J0aW9uYWwgdG8gdGhlIHZhbHVlNQEgaXMgcmVzZXJ2ZWQgYW5kIHNsYXNoZWQgaWYgdGhlIHByb3Bvc2FsIGlzIHJlamVjdGVkLiBJdCBpcyByZXR1cm5lZCBvbmNlIHRoZVQgcHJvcG9zYWwgaXMgYXdhcmRlZC4ALCAjIDx3ZWlnaHQ+ICAtIE8oMSkuZCAtIExpbWl0ZWQgc3RvcmFnZSByZWFkcy6UIC0gT25lIERCIGNoYW5nZSwgb25lIGV4dHJhIERCIGVudHJ5LjAgIyA8L3dlaWdodD48cmVqZWN0X3Byb3Bvc2FsBCxwcm9wb3NhbF9pZFhDb21wYWN0PFByb3Bvc2FsSW5kZXg+HPwgUmVqZWN0IGEgcHJvcG9zZWQgc3BlbmQuIFRoZSBvcmlnaW5hbCBkZXBvc2l0IHdpbGwgYmUgc2xhc2hlZC4ALCAjIDx3ZWlnaHQ+ICAtIE8oMSkuZCAtIExpbWl0ZWQgc3RvcmFnZSByZWFkcy5AIC0gT25lIERCIGNsZWFyLjAgIyA8L3dlaWdodD5AYXBwcm92ZV9wcm9wb3NhbAQscHJvcG9zYWxfaWRYQ29tcGFjdDxQcm9wb3NhbEluZGV4PiBdASBBcHByb3ZlIGEgcHJvcG9zYWwuIEF0IGEgbGF0ZXIgdGltZSwgdGhlIHByb3Bvc2FsIHdpbGwgYmUgYWxsb2NhdGVkIHRvIHRoZSBiZW5lZmljaWFyeawgYW5kIHRoZSBvcmlnaW5hbCBkZXBvc2l0IHdpbGwgYmUgcmV0dXJuZWQuACwgIyA8d2VpZ2h0PiAgLSBPKDEpLmQgLSBMaW1pdGVkIHN0b3JhZ2UgcmVhZHMuRCAtIE9uZSBEQiBjaGFuZ2UuMCAjIDwvd2VpZ2h0PgAAJENvbnRyYWN0cwABFDx1cGRhdGVfc2NoZWR1bGUEIHNjaGVkdWxlIFNjaGVkdWxlDLQgVXBkYXRlcyB0aGUgc2NoZWR1bGUgZm9yIG1ldGVyaW5nIGNvbnRyYWN0cy4ADQEgVGhlIHNjaGVkdWxlIG11c3QgaGF2ZSBhIGdyZWF0ZXIgdmVyc2lvbiB0aGFuIHRoZSBzdG9yZWQgc2NoZWR1bGUuIHB1dF9jb2RlCCRnYXNfbGltaXQwQ29tcGFjdDxHYXM+EGNvZGUUQnl0ZXMIXQEgU3RvcmVzIHRoZSBnaXZlbiBiaW5hcnkgV2FzbSBjb2RlIGludG8gdGhlIGNoYWluJ3Mgc3RvcmFnZSBhbmQgcmV0dXJucyBpdHMgYGNvZGVoYXNoYC7UIFlvdSBjYW4gaW5zdGFudGlhdGUgY29udHJhY3RzIG9ubHkgd2l0aCBzdG9yZWQgY29kZS4QY2FsbBAQZGVzdBxBZGRyZXNzFHZhbHVlSENvbXBhY3Q8QmFsYW5jZU9mPiRnYXNfbGltaXQwQ29tcGFjdDxHYXM+EGRhdGEUQnl0ZXMcCQEgTWFrZXMgYSBjYWxsIHRvIGFuIGFjY291bnQsIG9wdGlvbmFsbHkgdHJhbnNmZXJyaW5nIHNvbWUgYmFsYW5jZS4AKQEgKiBJZiB0aGUgYWNjb3VudCBpcyBhIHNtYXJ0LWNvbnRyYWN0IGFjY291bnQsIHRoZSBhc3NvY2lhdGVkIGNvZGUgd2lsbCBiZbAgZXhlY3V0ZWQgYW5kIGFueSB2YWx1ZSB3aWxsIGJlIHRyYW5zZmVycmVkLhkBICogSWYgdGhlIGFjY291bnQgaXMgYSByZWd1bGFyIGFjY291bnQsIGFueSB2YWx1ZSB3aWxsIGJlIHRyYW5zZmVycmVkLkkBICogSWYgbm8gYWNjb3VudCBleGlzdHMgYW5kIHRoZSBjYWxsIHZhbHVlIGlzIG5vdCBsZXNzIHRoYW4gYGV4aXN0ZW50aWFsX2RlcG9zaXRgLBUBIGEgcmVndWxhciBhY2NvdW50IHdpbGwgYmUgY3JlYXRlZCBhbmQgYW55IHZhbHVlIHdpbGwgYmUgdHJhbnNmZXJyZWQuGGNyZWF0ZRAkZW5kb3dtZW50SENvbXBhY3Q8QmFsYW5jZU9mPiRnYXNfbGltaXQwQ29tcGFjdDxHYXM+JGNvZGVfaGFzaCBDb2RlSGFzaBBkYXRhFEJ5dGVzKKkBIENyZWF0ZXMgYSBuZXcgY29udHJhY3QgZnJvbSB0aGUgYGNvZGVoYXNoYCBnZW5lcmF0ZWQgYnkgYHB1dF9jb2RlYCwgb3B0aW9uYWxseSB0cmFuc2ZlcnJpbmcgc29tZSBiYWxhbmNlLgCEIENyZWF0aW9uIGlzIGV4ZWN1dGVkIGFzIGZvbGxvd3M6AEEBIC0gVGhlIGRlc3RpbmF0aW9uIGFkZHJlc3MgaXMgY29tcHV0ZWQgYmFzZWQgb24gdGhlIHNlbmRlciBhbmQgaGFzaCBvZiB0aGUgY29kZS4FASAtIFRoZSBzbWFydC1jb250cmFjdCBhY2NvdW50IGlzIGNyZWF0ZWQgYXQgdGhlIGNvbXB1dGVkIGFkZHJlc3MubQEgLSBUaGUgYGN0b3JfY29kZWAgaXMgZXhlY3V0ZWQgaW4gdGhlIGNvbnRleHQgb2YgdGhlIG5ld2x5LWNyZWF0ZWQgYWNjb3VudC4gQnVmZmVyIHJldHVybmVkXQEgICBhZnRlciB0aGUgZXhlY3V0aW9uIGlzIHNhdmVkIGFzIHRoZSBgY29kZWAgb2YgdGhlIGFjY291bnQuIFRoYXQgY29kZSB3aWxsIGJlIGludm9rZWSoICAgdXBvbiBhbnkgY2FsbCByZWNlaXZlZCBieSB0aGlzIGFjY291bnQufCAtIFRoZSBjb250cmFjdCBpcyBpbml0aWFsaXplZC48Y2xhaW1fc3VyY2hhcmdlCBBkZXN0JEFjY291bnRJZChhdXhfc2VuZGVyRE9wdGlvbjxBY2NvdW50SWQ+FHEBIEFsbG93cyBibG9jayBwcm9kdWNlcnMgdG8gY2xhaW0gYSBzbWFsbCByZXdhcmQgZm9yIGV2aWN0aW5nIGEgY29udHJhY3QuIElmIGEgYmxvY2sgcHJvZHVjZXIVASBmYWlscyB0byBkbyBzbywgYSByZWd1bGFyIHVzZXJzIHdpbGwgYmUgYWxsb3dlZCB0byBjbGFpbSB0aGUgcmV3YXJkLgA5ASBJZiBjb250cmFjdCBpcyBub3QgZXZpY3RlZCBhcyBhIHJlc3VsdCBvZiB0aGlzIGNhbGwsIG5vIGFjdGlvbnMgYXJlIHRha2VuIGFuZKwgdGhlIHNlbmRlciBpcyBub3QgZWxpZ2libGUgZm9yIHRoZSByZXdhcmQuAAAQU3VkbwABDBBzdWRvBCBwcm9wb3NhbCBQcm9wb3NhbCg5ASBBdXRoZW50aWNhdGVzIHRoZSBzdWRvIGtleSBhbmQgZGlzcGF0Y2hlcyBhIGZ1bmN0aW9uIGNhbGwgd2l0aCBgUm9vdGAgb3JpZ2luLgDQIFRoZSBkaXNwYXRjaCBvcmlnaW4gZm9yIHRoaXMgY2FsbCBtdXN0IGJlIF9TaWduZWRfLgAsICMgPHdlaWdodD4gIC0gTygxKS5kIC0gTGltaXRlZCBzdG9yYWdlIHJlYWRzLmAgLSBPbmUgREIgd3JpdGUgKGV2ZW50KS7UIC0gVW5rbm93biB3ZWlnaHQgb2YgZGVyaXZhdGl2ZSBgcHJvcG9zYWxgIGV4ZWN1dGlvbi4wICMgPC93ZWlnaHQ+HHNldF9rZXkEDG5ldxxBZGRyZXNzJHUBIEF1dGhlbnRpY2F0ZXMgdGhlIGN1cnJlbnQgc3VkbyBrZXkgYW5kIHNldHMgdGhlIGdpdmVuIEFjY291bnRJZCAoYG5ld2ApIGFzIHRoZSBuZXcgc3VkbyBrZXkuANAgVGhlIGRpc3BhdGNoIG9yaWdpbiBmb3IgdGhpcyBjYWxsIG11c3QgYmUgX1NpZ25lZF8uACwgIyA8d2VpZ2h0PiAgLSBPKDEpLmQgLSBMaW1pdGVkIHN0b3JhZ2UgcmVhZHMuRCAtIE9uZSBEQiBjaGFuZ2UuMCAjIDwvd2VpZ2h0PhxzdWRvX2FzCAx3aG8cQWRkcmVzcyBwcm9wb3NhbCBQcm9wb3NhbCxRASBBdXRoZW50aWNhdGVzIHRoZSBzdWRvIGtleSBhbmQgZGlzcGF0Y2hlcyBhIGZ1bmN0aW9uIGNhbGwgd2l0aCBgU2lnbmVkYCBvcmlnaW4gZnJvbUQgYSBnaXZlbiBhY2NvdW50LgDQIFRoZSBkaXNwYXRjaCBvcmlnaW4gZm9yIHRoaXMgY2FsbCBtdXN0IGJlIF9TaWduZWRfLgAsICMgPHdlaWdodD4gIC0gTygxKS5kIC0gTGltaXRlZCBzdG9yYWdlIHJlYWRzLmAgLSBPbmUgREIgd3JpdGUgKGV2ZW50KS7UIC0gVW5rbm93biB3ZWlnaHQgb2YgZGVyaXZhdGl2ZSBgcHJvcG9zYWxgIGV4ZWN1dGlvbi4wICMgPC93ZWlnaHQ+AAAgSW1PbmxpbmUAAQQkaGVhcnRiZWF0CCRoZWFydGJlYXQkSGVhcnRiZWF0JHNpZ25hdHVyZSRTaWduYXR1cmUAAAAgT2ZmZW5jZXMAAQAAAEhBdXRob3JpdHlEaXNjb3ZlcnkAAQAAACBJZGVudGl0eQABJCByZWdpc3Rlcgg0aWRlbnRpdHlfdHlwZTBJZGVudGl0eVR5cGUgaWRlbnRpdHkgSWRlbnRpdHkQDQEgQSBmdW5jdGlvbiB0aGF0IHJlZ2lzdGVycyBhbiBpZGVudGl0eV90eXBlIGFuZCBpZGVudGl0eSBmb3IgYSB1c2VyABUBIENoZWNrcyB3aGV0aGVyIHRoZSAoaWRlbnRpdHlfdHlwZSwgaWRlbnRpdHkpIHBhaXIgZXhpc3RzIGFuZCBjcmVhdGVzCQEgdGhlIHJlY29yZCBpZiBub3cuIFRoZSByZWNvcmQgaXMgaW5kZXhlZCBieSB0aGUgaGFzaCBvZiB0aGUgcGFpci4YYXR0ZXN0CDRpZGVudGl0eV9oYXNoEEhhc2gsYXR0ZXN0YXRpb24sQXR0ZXN0YXRpb24UwCBBIGZ1bmN0aW9uIHRoYXQgY3JlYXRlcyBhbiBpZGVudGl0eSBhdHRlc3RhdGlvbgAZASBBdHRlc3RhdGlvbiBpcyBvbmx5IHZhbGlkIGlmIHRoZSBpZGVudGl0eSBpcyBpbiB0aGUgYXR0ZXN0YXRpb24gcGhhc2UJASBhbmQgaXMgdmVyaWZpZWQgb2ZmLWNoYWluIHVzaW5nIGFuIG9mZi1jaGFpbiB3b3JrZXIgbm9kZS4gQ3VycmVudPAgaW1wbGVtZW50YXRpb24gb3ZlcndyaXRlcyBhbGwgcHJvb2ZzIGlmIHNhZmV0eSBjaGVja3MgcGFzcy5McmVnaXN0ZXJfYW5kX2F0dGVzdAw0aWRlbnRpdHlfdHlwZTBJZGVudGl0eVR5cGUgaWRlbnRpdHkgSWRlbnRpdHksYXR0ZXN0YXRpb24sQXR0ZXN0YXRpb24QFQEgQSBmdW5jdGlvbiB0aGF0IHJlZ2lzdGVycyBhbmQgYXR0ZXN0cyB0byBhbiBpZGVudGl0eSBzaW11bHRhbmVvdXNseS4AHQEgQWxsb3dzIG1vcmUgZWZmaWNpZW50IHJlZ2lzdHJhdGlvbiBhbmQgYXR0ZXN0YXRpb24gcHJvY2Vzc2luZyBzaW5jZSBpdHQgcmVxdWlyZXMgb25seSAxIHRyYW5zYWN0aW9uLhh2ZXJpZnkINGlkZW50aXR5X2hhc2gQSGFzaDh2ZXJpZmllcl9pbmRleAx1MzIQyCBBIGZ1bmN0aW9uIHRoYXQgdmVyaWZpZXMgYW4gaWRlbnRpdHkgYXR0ZXN0YXRpb24uBCAFASBUaGUgdmVyaWZpY2F0aW9uIGlzIGhhbmRsZWQgYnkgYSBzZXQgb2Ygc2VlZGVkIHZlcmlmaWVycyB3aG8gcnVuyCB0aGUgb2ZmLWNoYWluIHdvcmtlciBub2RlIHRvIHZlcmlmeSBhdHRlc3RhdGlvbnMuEGRlbnkINGlkZW50aXR5X2hhc2gQSGFzaDh2ZXJpZmllcl9pbmRleAx1MzIQwCBBIGZ1bmN0aW9uIHRoYXQgZGVuaWVzIGFuIGlkZW50aXR5IGF0dGVzdGF0aW9uLgQgBQEgVGhlIHZlcmlmaWNhdGlvbiBpcyBoYW5kbGVkIGJ5IGEgc2V0IG9mIHNlZWRlZCB2ZXJpZmllcnMgd2hvIHJ1bsggdGhlIG9mZi1jaGFpbiB3b3JrZXIgbm9kZSB0byB2ZXJpZnkgYXR0ZXN0YXRpb25zLix2ZXJpZnlfbWFueQg8aWRlbnRpdHlfaGFzaGVzJFZlYzxIYXNoPjh2ZXJpZmllcl9pbmRleAx1MzIEiCBWZXJpZnkgbWFueSB2ZXJpZmljYXRpb24gcmVxdWVzdHMkZGVueV9tYW55CDxpZGVudGl0eV9oYXNoZXMkVmVjPEhhc2g+OHZlcmlmaWVyX2luZGV4DHUzMgSAIERlbnkgbWFueSB2ZXJpZmljYXRpb24gcmVxdWVzdHMwYWRkX21ldGFkYXRhEDRpZGVudGl0eV9oYXNoEEhhc2gYYXZhdGFyFEJ5dGVzMGRpc3BsYXlfbmFtZRRCeXRlcxx0YWdsaW5lFEJ5dGVzBIggQWRkIG1ldGFkYXRhIHRvIHNlbmRlcidzIGFjY291bnQuGHJldm9rZQQ0aWRlbnRpdHlfaGFzaBBIYXNoBPwgUmV2b2tlIGFuIGlkZW50aXR5IGZyb20gdGhlIGNyZWF0b3Ivc2VuZGVyIG9mIHN1Y2ggYW4gaWRlbnRpdHkAABhWb3RpbmcAAQgYY29tbWl0CBx2b3RlX2lkDHU2NBhjb21taXQsVm90ZU91dGNvbWUUJQEgQSBmdW5jdGlvbiBmb3IgY29tbWl0LXJldmVhbCB2b3Rpbmcgc2NoZW1lcyB0aGF0IGFkZHMgYSB2b3RlIGNvbW1pdG1lbnQuABUBIEEgdm90ZSBjb21taXRtZW50IGlzIGZvcm1hdHRlZCB1c2luZyB0aGUgbmF0aXZlIGhhc2ggZnVuY3Rpb24uIFRoZXJlGQEgYXJlIGN1cnJlbnRseSBubyBjcnlwdG9lY29ub21pYyBwdW5pc2htZW50cyBhZ2FpbnN0IG5vdCByZXZlYWxpbmcgdGhlMCBjb21taXRtZW50LhhyZXZlYWwMHHZvdGVfaWQMdTY0EHZvdGVAVmVjPFZvdGVPdXRjb21lPhhzZWNyZXRMT3B0aW9uPFZvdGVPdXRjb21lPgxJASBBIGZ1bmN0aW9uIHRoYXQgcmV2ZWFscyBhIHZvdGUgY29tbWl0bWVudCBvciBzZXJ2ZXMgYXMgdGhlIGdlbmVyYWwgdm90ZSBmdW5jdGlvbi4APQEgVGhlcmUgYXJlIGN1cnJlbnRseSBubyBjcnlwdG9lY29ub21pYyBpbmNlbnRpdmVzIGZvciByZXZlYWxpbmcgY29tbWl0ZWQgdm90ZXMuAAAkU2lnbmFsaW5nAAEIPGNyZWF0ZV9wcm9wb3NhbBQUdGl0bGU0UHJvcG9zYWxUaXRsZSBjb250ZW50c0BQcm9wb3NhbENvbnRlbnRzIG91dGNvbWVzQFZlYzxWb3RlT3V0Y29tZT4kdm90ZV90eXBlQHZvdGluZzo6Vm90ZVR5cGUodGFsbHlfdHlwZUR2b3Rpbmc6OlRhbGx5VHlwZQSIIENyZWF0ZXMgYSBuZXcgc2lnbmFsaW5nIHByb3Bvc2FsLkBhZHZhbmNlX3Byb3Bvc2FsBDRwcm9wb3NhbF9oYXNoEEhhc2gICQEgQWR2YW5jZSBhIHNpZ25hbGluZyBwcm9wb3NhbCBpbnRvIHRoZSAidm90aW5nIiBvciAiY29tbWl0IiBzdGFnZS74IENhbiBvbmx5IGJlIHBlcmZvcm1lZCBieSB0aGUgb3JpZ2luYWwgYXV0aG9yIG9mIHRoZSBwcm9wb3NhbC4AADhUcmVhc3VyeVJld2FyZAABAAAA' -}; diff --git a/packages/extension-chains/src/index.ts b/packages/extension-chains/src/index.ts index 68dae029..b68c30f0 100644 --- a/packages/extension-chains/src/index.ts +++ b/packages/extension-chains/src/index.ts @@ -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 ` -import edgeware from './edgeware'; import kusama from './kusama'; -const chains: Map = new Map( - // eslint-disable-next-line @typescript-eslint/no-use-before-define - [edgeware, kusama].map((def) => defToChain(def)) +const definitions: Map = 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()]; } diff --git a/packages/extension-chains/src/types.ts b/packages/extension-chains/src/types.ts index 9c5cfa2a..85cecd5e 100644 --- a/packages/extension-chains/src/types.ts +++ b/packages/extension-chains/src/types.ts @@ -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; - metaCalls?: string; -} diff --git a/packages/extension-inject/src/types.ts b/packages/extension-inject/src/types.ts index 8495cbd9..4878cc41 100644 --- a/packages/extension-inject/src/types.ts +++ b/packages/extension-inject/src/types.ts @@ -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; + metaCalls?: string; +} + +export interface InjectedMetadataKnown { + genesisHash: string; + specVersion: number; +} + +export interface InjectedMetadata { + get: () => Promise; + provide: (definition: MetadataDef) => Promise; +} + export type ProviderList = Record 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 { diff --git a/packages/extension-ui/src/Popup/Authorize/Authorize.test.tsx b/packages/extension-ui/src/Popup/Authorize/Authorize.test.tsx index 2bb2054e..0825df9b 100644 --- a/packages/extension-ui/src/Popup/Authorize/Authorize.test.tsx +++ b/packages/extension-ui/src/Popup/Authorize/Authorize.test.tsx @@ -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( - + - ); + ); it('render component', () => { const wrapper = mountAuthorize(); diff --git a/packages/extension-ui/src/Popup/Authorize/Request.tsx b/packages/extension-ui/src/Popup/Authorize/Request.tsx index cf8518e1..3e48941d 100644 --- a/packages/extension-ui/src/Popup/Authorize/Request.tsx +++ b/packages/extension-ui/src/Popup/Authorize/Request.tsx @@ -38,8 +38,8 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop An application, self-identifying as {origin} is requesting access from{' '} {url} . diff --git a/packages/extension-ui/src/Popup/Authorize/index.tsx b/packages/extension-ui/src/Popup/Authorize/index.tsx index a7d4d9b4..42511857 100644 --- a/packages/extension-ui/src/Popup/Authorize/index.tsx +++ b/packages/extension-ui/src/Popup/Authorize/index.tsx @@ -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 ( <> -
+
{requests.map(({ id, request, url }, index): React.ReactNode => ( { + const chain = useMetadata(request.genesisHash); + const onAction = useContext(ActionContext); + const _onApprove = (): Promise => + approveMetaRequest(metaId) + .then((): void => onAction()) + .catch((error: Error) => console.error(error)); + const _onReject = (): Promise => + rejectMetaRequest(metaId) + .then((): void => onAction()) + .catch((error: Error) => console.error(error)); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
from{url}
chain{request.chain}
icon{request.icon}
decimals{request.tokenDecimals}
symbol{request.tokenSymbol}
upgrade{chain ? chain.specVersion : 'unknown'} -> {request.specVersion}
+ + This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata. + Yes, do this metadata update + + Reject + + +
+ ); +} + +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; + } +`; diff --git a/packages/extension-ui/src/Popup/Metadata/index.tsx b/packages/extension-ui/src/Popup/Metadata/index.tsx new file mode 100644 index 00000000..ed28885c --- /dev/null +++ b/packages/extension-ui/src/Popup/Metadata/index.tsx @@ -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 ( + <> +
+ {requests[0] + ? ( + + ) + : + } + + ); +} diff --git a/packages/extension-ui/src/Popup/Signing/Extrinsic.tsx b/packages/extension-ui/src/Popup/Signing/Extrinsic.tsx index 4ec00310..750065fd 100644 --- a/packages/extension-ui/src/Popup/Signing/Extrinsic.tsx +++ b/packages/extension-ui/src/Popup/Signing/Extrinsic.tsx @@ -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 { - const chain = useRef(findChain(genesisHash)).current; + const chain = useMetadata(genesisHash); const specVersion = useRef(bnToBn(hexSpec)).current; const [decoded, setDecoded] = useState({ args: null, method: null }); @@ -103,84 +104,36 @@ function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, reques }, [method, isDecoded, chain, specVersion]); return ( - - +
+ + + + + + + + + + + + + + + + + {!tip.isEmpty && ( - - + + - - - - - - - - - - - - - {!tip.isEmpty && ( - - - - - )} - {renderMethod(method, decoded)} - - - - - -
from{url}
{chain ? 'chain' : 'genesis'}{chain ? chain.name : genesisHash}
version{specVersion.toNumber()}
nonce{formatNumber(nonce)}
from{url}tip{formatNumber(tip)}
{chain.isUnknown ? 'genesis' : 'chain'}{chain.isUnknown ? genesisHash : chain.name}
version{specVersion.toNumber()}
nonce{formatNumber(nonce)}
tip{formatNumber(tip)}
lifetime{mortalityAsString(era, blockNumber)}
+ )} + {renderMethod(method, decoded)} + + lifetime + {mortalityAsString(era, blockNumber)} + + ); } -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); diff --git a/packages/extension-ui/src/Popup/Signing/Request.tsx b/packages/extension-ui/src/Popup/Signing/Request.tsx index 9a7788ef..b75b2ec4 100644 --- a/packages/extension-ui/src/Popup/Signing/Request.tsx +++ b/packages/extension-ui/src/Popup/Signing/Request.tsx @@ -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 => { - return cancelSignRequest(signId).then((): void => onAction()) + const _onCancel = (): Promise => + cancelSignRequest(signId) + .then((): void => onAction()) .catch((error: Error) => console.error(error)); - }; const _onSign = (password: string): Promise => 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 ( <>
@@ -105,9 +108,9 @@ export default function Request ({ account: { isExternal }, request, signId, url ); - } else { - return null; } + + return null; } const SignArea = styled(ButtonArea)` diff --git a/packages/extension-ui/src/Popup/Signing/Signing.test.tsx b/packages/extension-ui/src/Popup/Signing/Signing.test.tsx index a436ed53..e836fff5 100644 --- a/packages/extension-ui/src/Popup/Signing/Signing.test.tsx +++ b/packages/extension-ui/src/Popup/Signing/Signing.test.tsx @@ -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 ( - + - + ); } const mountComponent = async (): Promise => { diff --git a/packages/extension-ui/src/Popup/Signing/index.tsx b/packages/extension-ui/src/Popup/Signing/index.tsx index 5c000756..42b596b9 100644 --- a/packages/extension-ui/src/Popup/Signing/index.tsx +++ b/packages/extension-ui/src/Popup/Signing/index.tsx @@ -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] ? ( <> -
+
{requests.length > 1 && ( { 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'} /> ) : ; diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index 8bd53651..438bf31a 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -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); const [cameraOn, setCameraOn] = useState(settings.get().camera === 'on'); const [mediaAllowed, setMediaAllowed] = useState(false); + const [metaRequests, setMetaRequests] = useState(null); const [signRequests, setSignRequests] = useState(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 ( - {accounts && authRequests && signRequests && ( + {accounts && authRequests && metaRequests && signRequests && ( - + - - - - - - - - - - + + + + + + + + + + + + - + )} diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index 5def55e3..69476aad 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -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 { const accounts = useContext(AccountContext); - const [{ account, chain, formatted, prefix }, setRecoded] = useState({ account: null, chain: null, formatted: null, prefix: 42 }); + const chain = useMetadata(genesisHash); + const [{ account, formatted, prefix }, setRecoded] = useState({ account: null, formatted: null, prefix: 42 }); const [showActionsMenu, setShowActionsMenu] = useState(false); const [moveMenuUp, setIsMovedMenu] = useState(false); const actionsRef = useRef(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%; diff --git a/packages/extension-ui/src/components/Header.tsx b/packages/extension-ui/src/components/Header.tsx index 5e865ee7..b91037df 100644 --- a/packages/extension-ui/src/components/Header.tsx +++ b/packages/extension-ui/src/components/Header.tsx @@ -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'; diff --git a/packages/extension-ui/src/components/Switch.tsx b/packages/extension-ui/src/components/Switch.tsx index db5cd971..e9e79423 100644 --- a/packages/extension-ui/src/components/Switch.tsx +++ b/packages/extension-ui/src/components/Switch.tsx @@ -39,7 +39,7 @@ const Slider = styled.span` &:before { position: absolute; - content: ""; + content: ''; height: 16px; width: 16px; left: 4px; @@ -56,7 +56,7 @@ const Checkbox = styled.input.attrs(() => ({ opacity: 0; width: 0; height: 0; - + &:checked + ${Slider}:before { transform: translateX(24px); } @@ -70,7 +70,7 @@ export default styled(Switch)` height: 24px; margin: 8px; } - + span { font-weight: 600; } diff --git a/packages/extension-ui/src/components/Table.tsx b/packages/extension-ui/src/components/Table.tsx new file mode 100644 index 00000000..2a50fe86 --- /dev/null +++ b/packages/extension-ui/src/components/Table.tsx @@ -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 { + return ( + + + {children} + +
+ ); +} + +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; + } + } +`); diff --git a/packages/extension-ui/src/components/contexts.tsx b/packages/extension-ui/src/components/contexts.tsx index ad248892..13a77653 100644 --- a/packages/extension-ui/src/components/contexts.tsx +++ b/packages/extension-ui/src/components/contexts.tsx @@ -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([]); const ActionContext = React.createContext<(to?: string) => void>(noop); -const AuthorizeContext = React.createContext([]); +const AuthorizeReqContext = React.createContext([]); const MediaContext = React.createContext(false); -const SigningContext = React.createContext([]); +const MetadataReqContext = React.createContext([]); +const SigningReqContext = React.createContext([]); const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop); export { AccountContext, ActionContext, - AuthorizeContext, + AuthorizeReqContext, MediaContext, - SigningContext, + MetadataReqContext, + SigningReqContext, ThemeSwitchContext }; diff --git a/packages/extension-ui/src/components/index.ts b/packages/extension-ui/src/components/index.ts index f19a4898..22ab0c17 100644 --- a/packages/extension-ui/src/components/index.ts +++ b/packages/extension-ui/src/components/index.ts @@ -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'; diff --git a/packages/extension-ui/src/hooks/useMetadata.ts b/packages/extension-ui/src/hooks/useMetadata.ts new file mode 100644 index 00000000..4c73975c --- /dev/null +++ b/packages/extension-ui/src/hooks/useMetadata.ts @@ -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(null); + + useEffect((): void => { + getMetadata(genesisHash).then(setChain); + }, [genesisHash]); + + return chain; +} diff --git a/packages/extension-ui/src/hooks.ts b/packages/extension-ui/src/hooks/useOutsideClick.ts similarity index 84% rename from packages/extension-ui/src/hooks.ts rename to packages/extension-ui/src/hooks/useOutsideClick.ts index 89be0508..f7f40435 100644 --- a/packages/extension-ui/src/hooks.ts +++ b/packages/extension-ui/src/hooks/useOutsideClick.ts @@ -4,16 +4,18 @@ import { useEffect, RefObject } from 'react'; -export const useOutsideClick = (ref: RefObject, callback: () => void): void => { +export default function useOutsideClick (ref: RefObject, 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); }; }); -}; +} diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index 0ce20515..65baa7aa 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -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 { return sendMessage('pri(accounts.forget)', { address }); } -export async function rejectAuthRequest (id: string): Promise { - return sendMessage('pri(authorize.reject)', { id }); -} - export async function approveAuthRequest (id: string): Promise { return sendMessage('pri(authorize.approve)', { id }); } +export async function approveMetaRequest (id: string): Promise { + return sendMessage('pri(metadata.approve)', { id }); +} + export async function cancelSignRequest (id: string): Promise { 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 { + const definitions = await sendMessage('pri(metadata.list)'); + + return findChain(definitions, genesisHash); +} + +export async function rejectAuthRequest (id: string): Promise { + return sendMessage('pri(authorize.reject)', { id }); +} + +export async function rejectMetaRequest (id: string): Promise { + return sendMessage('pri(metadata.reject)', { id }); +} + export async function subscribeAccounts (cb: (accounts: AccountJson[]) => void): Promise { return sendMessage('pri(accounts.subscribe)', null, cb); } -export async function subscribeAuthorize (cb: (accounts: AuthorizeRequest[]) => void): Promise { - return sendMessage('pri(authorize.subscribe)', null, cb); +export async function subscribeAuthorizeRequests (cb: (accounts: AuthorizeRequest[]) => void): Promise { + return sendMessage('pri(authorize.requests)', null, cb); } -export async function subscribeSigning (cb: (accounts: SigningRequest[]) => void): Promise { - return sendMessage('pri(signing.subscribe)', null, cb); +export async function subscribeMetadataRequests (cb: (accounts: MetadataRequest[]) => void): Promise { + return sendMessage('pri(metadata.requests)', null, cb); +} + +export async function subscribeSigningRequests (cb: (accounts: SigningRequest[]) => void): Promise { + return sendMessage('pri(signing.requests)', null, cb); } export async function validateSeed (suri: string, type?: KeypairType): Promise<{ address: string; suri: string }> { diff --git a/yarn.lock b/yarn.lock index 12274275..a1539e2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"