From d8d683ae37a32fa39382f612da932d2f0bd2cac0 Mon Sep 17 00:00:00 2001 From: Felicio Mununga Date: Fri, 10 Jan 2025 04:19:39 +0100 Subject: [PATCH] add `apps/connector` logger --- .changeset/sharp-cheetahs-breathe.md | 5 ++ apps/connector/README.md | 8 ++ apps/connector/eslint.config.mjs | 5 ++ apps/connector/global.d.ts | 1 + apps/connector/src/background.ts | 8 ++ apps/connector/src/contents/main.ts | 73 +++++++++++++++++++ apps/connector/src/contents/provider.ts | 21 +++++- apps/connector/src/contents/proxy.ts | 55 ++++++++++++-- .../connector/src/hooks/use-local-storage.tsx | 9 ++- apps/connector/src/lib/desktop-client.ts | 13 ++++ apps/connector/src/lib/logger.ts | 47 ++++++++++++ apps/connector/src/lib/request-arguments.ts | 1 + apps/connector/src/messages/main-message.ts | 12 +++ 13 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 .changeset/sharp-cheetahs-breathe.md create mode 100644 apps/connector/src/contents/main.ts create mode 100644 apps/connector/src/lib/logger.ts create mode 100644 apps/connector/src/messages/main-message.ts diff --git a/.changeset/sharp-cheetahs-breathe.md b/.changeset/sharp-cheetahs-breathe.md new file mode 100644 index 000000000..5b4ca1be0 --- /dev/null +++ b/.changeset/sharp-cheetahs-breathe.md @@ -0,0 +1,5 @@ +--- +'connector': patch +--- + +add logger diff --git a/apps/connector/README.md b/apps/connector/README.md index 7569b7173..daa994ebc 100644 --- a/apps/connector/README.md +++ b/apps/connector/README.md @@ -102,3 +102,11 @@ FLAG_CONNECTOR_ENABLED=1 '/Volumes/Status Desktop/Status.app/Contents/MacOS/nim_ ## Testing Download latest build from last merged PR or build from source. To use the extension see the load steps from [Development](#development) section. + +### Logging + +Browser > Console + +``` +await connector.enableLogging(true) +``` diff --git a/apps/connector/eslint.config.mjs b/apps/connector/eslint.config.mjs index 24a7e5f29..95b86f2db 100644 --- a/apps/connector/eslint.config.mjs +++ b/apps/connector/eslint.config.mjs @@ -4,6 +4,11 @@ import configs, { tailwindcssConfigs } from '@status-im/eslint-config' export default [ ...configs, ...tailwindcssConfigs, + { + rules: { + 'no-console': 'error', + }, + }, { files: ['**/*.ts', '**/*.tsx'], rules: { diff --git a/apps/connector/global.d.ts b/apps/connector/global.d.ts index ad9b02fae..d2437462c 100644 --- a/apps/connector/global.d.ts +++ b/apps/connector/global.d.ts @@ -3,5 +3,6 @@ import type { Provider } from './src/contents/provider' declare global { interface Window { ethereum: Provider + registration?: ServiceWorkerRegistration } } diff --git a/apps/connector/src/background.ts b/apps/connector/src/background.ts index f76c57f3b..b5e5466f9 100644 --- a/apps/connector/src/background.ts +++ b/apps/connector/src/background.ts @@ -46,3 +46,11 @@ chrome.action.onClicked.addListener(async tab => { type: 'status:icon:clicked', } satisfies ServiceWorkerMessage) }) + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.method === 'toggleLogging') { + storage.set('status:logging', message.params[0]) + + sendResponse(message.params[0]) + } +}) diff --git a/apps/connector/src/contents/main.ts b/apps/connector/src/contents/main.ts new file mode 100644 index 000000000..babc619af --- /dev/null +++ b/apps/connector/src/contents/main.ts @@ -0,0 +1,73 @@ +import { ProxyMessage } from '~messages/proxy-message' + +import type { RequestArguments } from '~lib/request-arguments' +import type { PlasmoCSConfig } from 'plasmo' + +export const config: PlasmoCSConfig = { + matches: ['https://*/*'], + world: 'MAIN', + run_at: 'document_start', + all_frames: false, +} + +Object.defineProperties(window, { + connector: { + value: { + enableLogging: (value = true) => { + // shared web storage with host page and content scripts (Web Storage API) + window.localStorage.setItem('status:logging', JSON.stringify(value)) + + // service worker extension storage (extension storage API) + request({ + method: 'toggleLogging', + params: [value], + }) + + return value + }, + }, + configurable: false, + writable: false, + }, +}) + +async function request(args: RequestArguments) { + const { method, params } = args + + const messageChannel = new MessageChannel() + + return new Promise((resolve, reject) => { + messageChannel.port1.onmessage = ({ data }) => { + try { + const message = ProxyMessage.parse(data) + + messageChannel.port1.close() + + switch (message.type) { + case 'status:proxy:success': { + resolve(message.data) + + return + } + case 'status:proxy:error': { + reject(new Error(message.error.message)) + + return + } + } + } catch { + return + } + } + + const mainMessage = { + type: 'status:main', + data: { + method, + params, + }, + } + + window.postMessage(mainMessage, window.origin, [messageChannel.port2]) + }) +} diff --git a/apps/connector/src/contents/provider.ts b/apps/connector/src/contents/provider.ts index 20ef00f31..cbdea6a41 100644 --- a/apps/connector/src/contents/provider.ts +++ b/apps/connector/src/contents/provider.ts @@ -1,3 +1,4 @@ +import { logger } from '~lib/logger' import { ProviderRpcError } from '~lib/provider-rpc-error' import { RequestArguments } from '~lib/request-arguments' import { ProxyMessage } from '~messages/proxy-message' @@ -96,11 +97,15 @@ export class Provider { this.#listeners.get('connect')?.({ chainId: '0x1' }) this.#listeners.get('connected')?.({ chainId: '0x1' }) this.connected = true + + logger.info('connected::') } if (method === 'wallet_switchEthereumChain') { this.#listeners.get('chainChanged')?.(message.data) this.#listeners.get('networkChanged')?.(message.data) + + logger.info('chainChanged::') } resolve(message.data) @@ -108,6 +113,8 @@ export class Provider { return } case 'status:proxy:error': { + logger.error(message.error) + // note: for those dApps that make call after having permissions revoked if ( message.error.message === 'dApp is not permitted by user' && @@ -148,15 +155,21 @@ export class Provider { } public on(event: Event, handler: (args: unknown) => void): void { + logger.info('on::', event, handler) + this.#listeners.set(event, handler) } /** @deprecated */ - public async close(): Promise { + public async close(...args: unknown[]): Promise { + logger.info('close::', args) + this.disconnect() } - public removeListener(event: Event): void { + public removeListener(event: Event, handler: (args: unknown) => void): void { + logger.info('removeListener::', event, handler) + // note: not all dapps remove these on disconnect if (event === 'close' || event === 'disconnect') { this.disconnect() @@ -166,6 +179,8 @@ export class Provider { } public async enable() { + logger.info('enable::') + return true } @@ -176,6 +191,8 @@ export class Provider { this.connected = false + logger.info('disconnect::') + await this.request({ method: 'wallet_revokePermissions', params: [ diff --git a/apps/connector/src/contents/proxy.ts b/apps/connector/src/contents/proxy.ts index 92c269986..7346ffdb0 100644 --- a/apps/connector/src/contents/proxy.ts +++ b/apps/connector/src/contents/proxy.ts @@ -1,4 +1,6 @@ import { getFaviconUrl } from '~lib/get-favicon-url' +import { logger } from '~lib/logger' +import { MainMessage } from '~messages/main-message' import { ProviderMessage } from '~messages/provider-message' import { DesktopClient } from '../lib/desktop-client' @@ -13,7 +15,7 @@ export const config: PlasmoCSConfig = { const desktopClient = new DesktopClient() -const handleMessage = async (event: MessageEvent) => { +const handleProviderMessage = async (event: MessageEvent) => { if (event.origin !== window.origin) { return } @@ -39,6 +41,8 @@ const handleMessage = async (event: MessageEvent) => { } try { + logger.info('request::', message.data) + const response = await desktopClient.send({ ...message.data, name: window.location.hostname, @@ -46,6 +50,8 @@ const handleMessage = async (event: MessageEvent) => { iconUrl: getFaviconUrl() ?? '', }) + logger.info('response::', response) + event.ports[0].postMessage({ type: 'status:proxy:success', data: response, @@ -71,12 +77,10 @@ const handleMessage = async (event: MessageEvent) => { } } - const proxyMessage: ProxyMessage = { + event.ports[0].postMessage({ type: 'status:proxy:error', error: proxyError, - } - - event.ports[0].postMessage(proxyMessage) + } satisfies ProxyMessage) } } @@ -108,4 +112,43 @@ function isRpcError( ) } -window.addEventListener('message', handleMessage) +window.addEventListener('message', handleProviderMessage) + +const handleMainMessage = async (event: MessageEvent) => { + if (event.origin !== window.origin) { + return + } + + let message: MainMessage + try { + message = MainMessage.parse(event.data) + } catch { + return + } + + if (message.type !== 'status:main') { + return + } + + try { + const response = await chrome.runtime.sendMessage( + chrome.runtime.id, + message.data, + ) + + event.ports[0].postMessage({ + type: 'status:proxy:success', + data: response, + } satisfies ProxyMessage) + } catch (error) { + event.ports[0].postMessage({ + type: 'status:proxy:error', + error: { + code: -32603, + message: isError(error) ? error.message : 'Internal error', + }, + } satisfies ProxyMessage) + } +} + +window.addEventListener('message', handleMainMessage) diff --git a/apps/connector/src/hooks/use-local-storage.tsx b/apps/connector/src/hooks/use-local-storage.tsx index b3cd0ca79..96ed89bae 100644 --- a/apps/connector/src/hooks/use-local-storage.tsx +++ b/apps/connector/src/hooks/use-local-storage.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react' +import { logger } from '~lib/logger' + export function useLocalStorage( key: string, initialValue: T, @@ -10,7 +12,8 @@ export function useLocalStorage( try { const item = window.localStorage.getItem(key) setStoredValue(item ? JSON.parse(item) : initialValue) - } catch { + } catch (error) { + logger.error(error) setStoredValue(initialValue) } }, []) @@ -39,7 +42,9 @@ export function useLocalStorage( setStoredValue(valueToStore) window.localStorage.setItem(key, JSON.stringify(valueToStore)) window.dispatchEvent(new Event('storage')) - } catch {} + } catch (error) { + logger.error(error) + } } return [storedValue, setValue] diff --git a/apps/connector/src/lib/desktop-client.ts b/apps/connector/src/lib/desktop-client.ts index 8a1b1862b..4cfc8bf5c 100644 --- a/apps/connector/src/lib/desktop-client.ts +++ b/apps/connector/src/lib/desktop-client.ts @@ -2,6 +2,8 @@ import { WebSocketProvider } from 'ethers' import { config } from '~config' +import { logger } from './logger' + import type { RequestArguments } from '~lib/request-arguments' import type { WebSocketLike } from 'ethers' @@ -19,6 +21,7 @@ export class DesktopClient { return } + logger.info('stop::') this.#rpcClient?.destroy() this.#rpcClient = null @@ -27,6 +30,7 @@ export class DesktopClient { public async send(args: DesktopRequestArguments) { if (!this.#rpcClient) { + logger.info('start::') this.#rpcClient = new WebSocketProvider( config.desktop.rpc.url, 'mainnet', @@ -41,6 +45,11 @@ export class DesktopClient { await waitUntilOpen(this.#rpcClient.websocket) + logger.info('client::', { + method: config.desktop.rpc.method, + params: [JSON.stringify(args)], + }) + return await this.#rpcClient.send(config.desktop.rpc.method, [ JSON.stringify(args), ]) @@ -71,6 +80,10 @@ async function waitUntilOpen(websocket: WebSocketLike) { reject(new Error('Timed out to connect to the RPC server')) }, 30 * 1000) + if (websocket.readyState === WebSocket.CONNECTING) { + logger.warn('Waiting for the RPC server to connect') + } + const onopen = websocket.onopen?.bind(websocket) websocket.onopen = event => { onopen?.(event) diff --git a/apps/connector/src/lib/logger.ts b/apps/connector/src/lib/logger.ts new file mode 100644 index 000000000..e864c0ee7 --- /dev/null +++ b/apps/connector/src/lib/logger.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-console */ + +export const logger = { + info(message: unknown, ...args: unknown[]) { + log('info', message, ...args) + }, + + warn(message: unknown, ...args: unknown[]) { + log('warn', message, ...args) + }, + + error(message: unknown, ...args: unknown[]) { + log('error', message, ...args) + }, +} + +function log( + level: 'info' | 'warn' | 'error', + message: unknown, + ...args: unknown[] +) { + // service worker context + if ( + typeof window === 'undefined' && // !window throws + typeof self !== 'undefined' && + self.registration + ) { + chrome.storage.sync.get('status:logging').then(result => { + if (!(result['status:logging'] === 'true')) { + return + } + + // async logging + console[level]('status:connector:', message, ...args) + }) + + return + } + + // other extension contexts + if (!(window?.localStorage.getItem('status:logging') === 'true')) { + return + } + + // synchronous logging + console[level]('status:connector:', message, ...args) +} diff --git a/apps/connector/src/lib/request-arguments.ts b/apps/connector/src/lib/request-arguments.ts index 3cebaf64b..06632b214 100644 --- a/apps/connector/src/lib/request-arguments.ts +++ b/apps/connector/src/lib/request-arguments.ts @@ -2,6 +2,7 @@ import { z } from 'zod' /** * @see https://eips.ethereum.org/EIPS/eip-1193#request-1 + * @see https://www.jsonrpc.org/specification#request_object */ export const RequestArguments = z.object({ method: z.string(), diff --git a/apps/connector/src/messages/main-message.ts b/apps/connector/src/messages/main-message.ts new file mode 100644 index 000000000..c5164d458 --- /dev/null +++ b/apps/connector/src/messages/main-message.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +import { RequestArguments } from '~lib/request-arguments' + +export const MainMessage = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('status:main'), + data: RequestArguments, + }), +]) + +export type MainMessage = z.infer