add apps/connector logger

This commit is contained in:
Felicio Mununga 2025-01-10 04:19:39 +01:00
parent b286ef2a74
commit d8d683ae37
No known key found for this signature in database
GPG Key ID: B8A18D62FCD9EA6E
13 changed files with 248 additions and 10 deletions

View File

@ -0,0 +1,5 @@
---
'connector': patch
---
add logger

View File

@ -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)
```

View File

@ -4,6 +4,11 @@ import configs, { tailwindcssConfigs } from '@status-im/eslint-config'
export default [
...configs,
...tailwindcssConfigs,
{
rules: {
'no-console': 'error',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {

View File

@ -3,5 +3,6 @@ import type { Provider } from './src/contents/provider'
declare global {
interface Window {
ethereum: Provider
registration?: ServiceWorkerRegistration
}
}

View File

@ -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])
}
})

View File

@ -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])
})
}

View File

@ -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<void> {
public async close(...args: unknown[]): Promise<void> {
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: [

View File

@ -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)

View File

@ -1,5 +1,7 @@
import { useEffect, useState } from 'react'
import { logger } from '~lib/logger'
export function useLocalStorage<T>(
key: string,
initialValue: T,
@ -10,7 +12,8 @@ export function useLocalStorage<T>(
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<T>(
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
window.dispatchEvent(new Event('storage'))
} catch {}
} catch (error) {
logger.error(error)
}
}
return [storedValue, setValue]

View File

@ -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)

View File

@ -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)
}

View File

@ -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(),

View File

@ -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<typeof MainMessage>