From f04263887e700d1c29e19445fb6b44e028face9d Mon Sep 17 00:00:00 2001 From: zzcwoshizz Date: Mon, 27 Mar 2023 15:25:13 +0800 Subject: [PATCH] add PostMessageProvider, it work on iframe of zkid wallet pwa app. (#20) --- .changeset/pre.json | 12 ++ .changeset/short-flowers-clean.md | 7 ++ packages/providers/src/ExtensionProvider.ts | 80 +++++++++++++ packages/providers/src/PostMessageProvider.ts | 108 ++++++++++++++++++ packages/providers/src/ZkidWalletProvider.ts | 60 ---------- packages/providers/src/index.ts | 4 +- packages/providers/src/types.ts | 7 ++ packages/providers/src/zkidWallet.ts | 14 +++ packages/rpc-defines/src/defineZk.ts | 6 + packages/rpc/package.json | 5 +- packages/rpc/src/errors.ts | 52 ++++++++- packages/rpc/src/index.ts | 1 + packages/rpc/src/request.ts | 6 +- packages/rpc/src/rpcs.ts | 5 + packages/rpc/src/transport.ts | 46 ++++++++ yarn.lock | 2 + 16 files changed, 351 insertions(+), 64 deletions(-) create mode 100644 .changeset/pre.json create mode 100644 .changeset/short-flowers-clean.md create mode 100644 packages/providers/src/ExtensionProvider.ts create mode 100644 packages/providers/src/PostMessageProvider.ts delete mode 100644 packages/providers/src/ZkidWalletProvider.ts create mode 100644 packages/providers/src/zkidWallet.ts create mode 100644 packages/rpc/src/transport.ts diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..561b1cd --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,12 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@zcloak/login-did": "1.1.1", + "@zcloak/login-providers": "1.1.1", + "@zcloak/login-rpc": "1.0.1", + "@zcloak/login-rpc-defines": "1.1.1", + "@zcloak/login-verify": "1.1.3" + }, + "changesets": [] +} diff --git a/.changeset/short-flowers-clean.md b/.changeset/short-flowers-clean.md new file mode 100644 index 0000000..339ac0d --- /dev/null +++ b/.changeset/short-flowers-clean.md @@ -0,0 +1,7 @@ +--- +"@zcloak/login-rpc-defines": minor +"@zcloak/login-providers": minor +"@zcloak/login-rpc": minor +--- + +add PostMessageProvider, it work on iframe of zkid wallet pwa app. diff --git a/packages/providers/src/ExtensionProvider.ts b/packages/providers/src/ExtensionProvider.ts new file mode 100644 index 0000000..2a6e7eb --- /dev/null +++ b/packages/providers/src/ExtensionProvider.ts @@ -0,0 +1,80 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Request, RpcMethods, RpcRequest } from '@zcloak/login-rpc'; +import type { ZkidWalletProvider } from './types'; + +import { BaseProvider } from './base/Provider'; + +declare module '@zcloak/login-rpc/rpcs' { + interface Rpcs { + credential_import: [{ credential: HexString }, undefined]; + } +} + +declare global { + interface Window { + zkid?: { request: Request; events: any }; + } +} + +type HexString = `0x${string}`; + +const waitReady: Promise = new Promise((resolve) => { + if (document.readyState === 'complete') { + resolve(); + } else { + self.addEventListener('load', () => resolve()); + } +}); + +const request: Request = async (method: RpcMethods, params: RpcRequest) => { + await waitReady; + if (!self.zkid) throw new Error('Please install zkID Wallet'); + + return self.zkid.request(method, params); +}; + +export class ExtensionProvider extends BaseProvider implements ZkidWalletProvider { + public static async isInstalled(): Promise { + await ExtensionProvider.isReady(); + + return !!self.zkid; + } + + public static isReady(): Promise { + return waitReady; + } + + constructor() { + super(request); + + waitReady.then(() => { + if (!self.zkid) { + console.warn( + 'Not zkID Wallet context, please install zkID Wallet https://chrome.google.com/webstore/detail/zkid-wallet/ahkpfejaeoepmfopmbhjgjekibmfcfgo' + ); + } + }); + + self.zkid?.events?.on?.('zkID_Wallet_didLoggedChanged', this.#didChanged); + self.zkid?.events?.on?.('zkID_Wallet_lock', this.#lock); + self.zkid?.events?.on?.('zkID_Wallet_unlock', this.#unlock); + } + + #didChanged = (...args: any[]) => { + this.emit('did_changed', ...args); + }; + + #lock = (...args: any[]) => { + this.emit('lock', ...args); + }; + + #unlock = (...args: any[]) => { + this.emit('unlock', ...args); + }; + + public importCredential(data: HexString) { + return this.request('credential_import', { credential: data }); + } +} diff --git a/packages/providers/src/PostMessageProvider.ts b/packages/providers/src/PostMessageProvider.ts new file mode 100644 index 0000000..985a6de --- /dev/null +++ b/packages/providers/src/PostMessageProvider.ts @@ -0,0 +1,108 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ZkidWalletProvider } from './types'; + +import { + isTransportEventMessage, + isTransportResponseMessage, + Request, + RpcMethods, + RpcRequest, + RpcResponse +} from '@zcloak/login-rpc'; + +import { BaseProvider } from './base/Provider'; + +type Handlers = Record< + string, + { + resolve: (data: RpcResponse) => void; + reject: (error: Error) => void; + } +>; +type HexString = `0x${string}`; + +let increaseId = 0; + +const handlers: Handlers = {}; + +const request: Request = (method: RpcMethods, params: RpcRequest) => { + if (self === parent) throw new Error('Please open on zkID Wallet'); + + return new Promise((resolve, reject) => { + const id = `${Date.now()}.${increaseId++}`; + + handlers[id] = { reject, resolve }; + + const message = { + jsonrpc: '2.0', + id, + method, + params + }; + + parent.postMessage(message, '*'); + }); +}; + +export class PostMessageProvider extends BaseProvider implements ZkidWalletProvider { + private readonly allowedOrigins: RegExp[] | null = null; + + constructor(allowedOrigins: RegExp[] | null = null) { + super(request); + + this.allowedOrigins = allowedOrigins; + self.addEventListener('message', this.#handleMessage); + } + + private isValidMessage = ({ data, origin, source }: MessageEvent): boolean => { + const emptyOrMalformed = !data; + const sentFromParentEl = source === parent; + let validOrigin = true; + + if (Array.isArray(this.allowedOrigins)) { + validOrigin = this.allowedOrigins.find((regExp) => regExp.test(origin)) !== undefined; + } + + return !emptyOrMalformed && sentFromParentEl && validOrigin; + }; + + #handleMessage = (message: MessageEvent) => { + if (this.isValidMessage(message)) { + if (isTransportEventMessage(message.data)) { + // event transport message + + if (message.data.event === 'zkID_Wallet_didLoggedChanged') { + this.emit('did_changed', message.data.data); + } else if (message.data.event === 'zkID_Wallet_lock') { + this.emit('lock', message.data.data); + } else if (message.data.event === 'zkID_Wallet_unlock') { + this.emit('unlock', message.data.data); + } + } else if (isTransportResponseMessage(message.data)) { + // response transport message + + const handler = handlers[message.data.id]; + + if (!handler) { + console.error(`Unknown response: ${JSON.stringify(message.data)}`); + + return; + } + + delete handlers[message.data.id]; + + if (message.data.error) { + handler.reject(new Error(message.data.error.message)); + } else { + handler.resolve(message.data.result); + } + } + } + }; + + public importCredential(data: HexString) { + return this.request('credential_import', { credential: data }); + } +} diff --git a/packages/providers/src/ZkidWalletProvider.ts b/packages/providers/src/ZkidWalletProvider.ts deleted file mode 100644 index 3b54aff..0000000 --- a/packages/providers/src/ZkidWalletProvider.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2021-2023 zcloak authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import type { Request } from '@zcloak/login-rpc'; - -import { BaseProvider } from './base/Provider'; - -declare module '@zcloak/login-rpc/rpcs' { - interface Rpcs { - credential_import: [{ credential: HexString }, undefined]; - } -} - -const injectWindow: { zkid?: { request: Request; events: any } } = window as any; - -type HexString = `0x${string}`; - -export class ZkidWalletProvider extends BaseProvider { - public static async isInstalled(): Promise { - await ZkidWalletProvider.isReady(); - - return !!injectWindow.zkid; - } - - public static isReady(): Promise { - return new Promise((resolve) => { - if (document.readyState === 'complete') { - resolve(); - } else { - window.addEventListener('load', () => resolve()); - } - }); - } - - constructor() { - if (!injectWindow.zkid) throw new Error('Zkid Wallet not install'); - - super(injectWindow.zkid.request); - - injectWindow.zkid.events.on?.('zkID_Wallet_didLoggedChanged', this.#didChanged); - injectWindow.zkid.events.on?.('zkID_Wallet_lock', this.#lock); - injectWindow.zkid.events.on?.('zkID_Wallet_unlock', this.#unlock); - } - - #didChanged = (...args: any[]) => { - this.emit('did_changed', ...args); - }; - - #lock = () => { - this.emit('lock'); - }; - - #unlock = () => { - this.emit('unlock'); - }; - - importCredential(data: HexString) { - return this.request('credential_import', { credential: data }); - } -} diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 0ebe05f..2c48a47 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -1,4 +1,6 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -export * from './ZkidWalletProvider'; +export * from './ExtensionProvider'; +export * from './PostMessageProvider'; +export * from './zkidWallet'; diff --git a/packages/providers/src/types.ts b/packages/providers/src/types.ts index dc4c404..71d56cb 100644 --- a/packages/providers/src/types.ts +++ b/packages/providers/src/types.ts @@ -1,4 +1,11 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { BaseProvider } from './base/Provider'; + export type ProviderEvents = 'did_changed' | 'lock' | 'unlock'; +type HexString = `0x${string}`; + +export interface ZkidWalletProvider extends BaseProvider { + importCredential(data: HexString): Promise; +} diff --git a/packages/providers/src/zkidWallet.ts b/packages/providers/src/zkidWallet.ts new file mode 100644 index 0000000..03c4c1c --- /dev/null +++ b/packages/providers/src/zkidWallet.ts @@ -0,0 +1,14 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ExtensionProvider } from './ExtensionProvider'; +import { PostMessageProvider } from './PostMessageProvider'; +import { ZkidWalletProvider } from './types'; + +export function adaptZkidWallet(allowedOrigins: RegExp[] | null = null): ZkidWalletProvider { + if (self !== parent) { + return new PostMessageProvider(allowedOrigins); + } else { + return new ExtensionProvider(); + } +} diff --git a/packages/rpc-defines/src/defineZk.ts b/packages/rpc-defines/src/defineZk.ts index c2ecb06..6421940 100644 --- a/packages/rpc-defines/src/defineZk.ts +++ b/packages/rpc-defines/src/defineZk.ts @@ -92,4 +92,10 @@ declare module '@zcloak/login-rpc/rpcs' { did_decrypt: [DidDecryptParams, HexString]; proof_generate: [ZkpGenRequest, ZkpGenResponse]; } + + interface RpcEvents { + zkID_Wallet_lock: any; + zkID_Wallet_unlock: any; + zkID_Wallet_didLoggedChanged: DidInfo; + } } diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 9007b5f..ab8d0dd 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -18,5 +18,8 @@ "sideEffects": false, "type": "module", "version": "1.0.1", - "main": "index.js" + "main": "index.js", + "dependencies": { + "@polkadot/util": "^10.3.1" + } } diff --git a/packages/rpc/src/errors.ts b/packages/rpc/src/errors.ts index 32d3cc1..1e3228a 100644 --- a/packages/rpc/src/errors.ts +++ b/packages/rpc/src/errors.ts @@ -1,8 +1,58 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -export interface RpcError { +export interface RpcErrorInterface { code: number; message: string; meaning: string; } + +export class RpcError extends Error { + public code: number; + public meaning: string; + + constructor(code: number, message: string, meaning: string) { + super(message); + this.code = code; + this.meaning = meaning; + } + + public toJson(): RpcErrorInterface { + return { + code: this.code, + message: this.message, + meaning: this.meaning + }; + } + + public static fromCode(code: number): RpcErrorInterface { + switch (code) { + case -32700: + return new RpcError( + code, + 'Parse error', + 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.' + ).toJson(); + case -32600: + return new RpcError(code, 'Invalid Request', 'The JSON sent is not a valid Request object.').toJson(); + case -32601: + return new RpcError(code, 'Method not found', 'The method does not exist / is not available.').toJson(); + case -32602: + return new RpcError(code, 'Invalid params', 'Invalid method parameter(s).').toJson(); + case -32603: + return new RpcError(code, 'Internal error', 'Internal JSON-Rpc error.').toJson(); + case -32001: + return new RpcError(code, 'User Reject', 'User reject operation').toJson(); + default: + return new RpcError(code, 'Unknown Error', 'Unknown Error').toJson(); + } + } + + public static fromError(error: any): RpcErrorInterface { + return new RpcError( + error?.code ?? -1, + error?.message ?? 'Unknown Error', + error?.reason ?? 'Unknown Error' + ).toJson(); + } +} diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts index 0c0f7dd..4c9391d 100644 --- a/packages/rpc/src/index.ts +++ b/packages/rpc/src/index.ts @@ -4,3 +4,4 @@ export * from './errors'; export * from './request'; export * from './rpcs'; +export * from './transport'; diff --git a/packages/rpc/src/request.ts b/packages/rpc/src/request.ts index 79ff90a..740269c 100644 --- a/packages/rpc/src/request.ts +++ b/packages/rpc/src/request.ts @@ -1,7 +1,7 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { RpcMethods, RpcRequest, RpcResponse } from './rpcs'; +import type { RpcEvents, RpcEventTypes, RpcMethods, RpcRequest, RpcResponse } from './rpcs'; export type Unsub = () => void; @@ -11,3 +11,7 @@ export type Unsub = () => void; export interface Request { (method: Method, params: RpcRequest): Promise>; } + +export interface SendEvent { + (method: EventType, params: RpcEvents[EventType]): Promise; +} diff --git a/packages/rpc/src/rpcs.ts b/packages/rpc/src/rpcs.ts index 0b512f9..ccd2985 100644 --- a/packages/rpc/src/rpcs.ts +++ b/packages/rpc/src/rpcs.ts @@ -9,3 +9,8 @@ export type RpcMethods = keyof Rpcs; export type RpcRequest = Rpcs[Method][0]; export type RpcResponse = Rpcs[Method][1]; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RpcEvents {} + +export type RpcEventTypes = keyof RpcEvents; diff --git a/packages/rpc/src/transport.ts b/packages/rpc/src/transport.ts new file mode 100644 index 0000000..fdee677 --- /dev/null +++ b/packages/rpc/src/transport.ts @@ -0,0 +1,46 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { RpcErrorInterface } from './errors'; +import type { RpcEvents, RpcEventTypes, RpcMethods, RpcRequest, RpcResponse } from './rpcs'; + +import { isObject } from '@polkadot/util'; + +export interface TransportRequestMessage { + jsonrpc: '2.0'; + method: Method; + params: RpcRequest; + id: string; +} + +export interface TransportResponseMessage { + jsonrpc: '2.0'; + result: RpcResponse; + id: string; + error?: RpcErrorInterface; +} + +export interface TransportEventMessage { + event: Type; + data: RpcEvents[Type]; +} + +export function isTransportRequestMessage( + value: unknown +): value is TransportRequestMessage { + return ( + isObject(value) && Object.hasOwn(value, 'jsonrpc') && Object.hasOwn(value, 'id') && Object.hasOwn(value, 'method') + ); +} + +export function isTransportResponseMessage( + value: unknown +): value is TransportResponseMessage { + return isObject(value) && Object.hasOwn(value, 'jsonrpc') && Object.hasOwn(value, 'id'); +} + +export function isTransportEventMessage( + value: unknown +): value is TransportEventMessage { + return isObject(value) && Object.hasOwn(value, 'event') && Object.hasOwn(value, 'data'); +} diff --git a/yarn.lock b/yarn.lock index a8ff76f..5ba5db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4002,6 +4002,8 @@ __metadata: "@zcloak/login-rpc@workspace:^, @zcloak/login-rpc@workspace:packages/rpc": version: 0.0.0-use.local resolution: "@zcloak/login-rpc@workspace:packages/rpc" + dependencies: + "@polkadot/util": "npm:^10.3.1" languageName: unknown linkType: soft