From 59aa057a1a0217facdc6a1cf4e130ec2e6a0d9e8 Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Tue, 19 Apr 2022 13:31:59 +0200 Subject: [PATCH] refactor: create new json rpc api --- src/background/background.ts | 101 +++++++----------- .../legacy-external-message-handler.ts | 64 +++++++++++ src/background/message-handler.ts | 2 +- src/content-scripts/content-script.ts | 16 ++- src/inpage/inpage.ts | 44 +++++++- src/shared/inpage-types.ts | 1 + src/shared/message-types.ts | 46 +++++++- test-app/src/components/home.tsx | 21 +++- 8 files changed, 215 insertions(+), 80 deletions(-) create mode 100644 src/background/legacy-external-message-handler.ts diff --git a/src/background/background.ts b/src/background/background.ts index 0e89afd1eba..f02934ff6ef 100755 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,41 +1,32 @@ -/** - The background script is the extension's event handler; it contains listeners for browser - events that are important to the extension. It lies dormant until an event is fired then - performs the instructed logic. An effective background script is only loaded when it is - needed and unloaded when it goes idle. - https://developer.chrome.com/docs/extensions/mv3/architecture-overview/#background_script - */ +// +// This file is the entrypoint to the extension's background script +// https://developer.chrome.com/docs/extensions/mv3/architecture-overview/#background_script import * as Sentry from '@sentry/react'; -import { storePayload, StorageKey } from '@shared/utils/storage'; import { RouteUrls } from '@shared/route-urls'; import { initSentry } from '@shared/utils/sentry-init'; +import { logger } from '@shared/logger'; import { CONTENT_SCRIPT_PORT, - ExternalMethods, - MessageFromContentScript, + LegacyMessageFromContentScript, + MESSAGE_SOURCE, + RpcMethods, + SupportedRpcMessages, } from '@shared/message-types'; -import { popupCenter } from '@background/popup-center'; -import { initContextMenuActions } from '@background/init-context-menus'; -import { backgroundMessageHandler } from './message-handler'; +import { initContextMenuActions } from './init-context-menus'; +import { internalBackgroundMessageHandler } from './message-handler'; import { backupOldWalletSalt } from './backup-old-wallet-salt'; - -const IS_TEST_ENV = process.env.TEST_ENV === 'true'; +import { + handleLegacyExternalMethodFormat, + inferLegacyMessage, +} from './legacy-external-message-handler'; initSentry(); - initContextMenuActions(); backupOldWalletSalt(); -// -// Playwright does not currently support Chrome extension popup testing: -// https://github.com/microsoft/playwright/issues/5593 -async function openRequestInFullPage(path: string, urlParams: URLSearchParams) { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`index.html#${path}?${urlParams.toString()}`), - }); -} +const IS_TEST_ENV = process.env.TEST_ENV === 'true'; chrome.runtime.onInstalled.addListener(details => { Sentry.wrap(async () => { @@ -47,60 +38,42 @@ chrome.runtime.onInstalled.addListener(details => { }); }); +// // Listen for connection to the content-script - port for two-way communication chrome.runtime.onConnect.addListener(port => Sentry.wrap(() => { - // Listen for auth and transaction events - if (port.name === CONTENT_SCRIPT_PORT) { - port.onMessage.addListener(async (message: MessageFromContentScript, port) => { - const { payload } = message; + if (port.name !== CONTENT_SCRIPT_PORT) return; + port.onMessage.addListener( + (message: LegacyMessageFromContentScript | SupportedRpcMessages, port) => { + if (inferLegacyMessage(message)) { + void handleLegacyExternalMethodFormat(message, port); + return; + } + + if (!port.sender?.tab?.id) + return logger.error('Message reached background script without a corresponding tab'); + switch (message.method) { - case ExternalMethods.authenticationRequest: { - void storePayload({ - payload, - storageKey: StorageKey.authenticationRequests, - port, + case RpcMethods[RpcMethods.stx_requestAccounts]: { + chrome.tabs.sendMessage(port.sender.tab.id, { + source: MESSAGE_SOURCE, + id: message.id, + results: { publicKey: 'sldkfjs' }, }); - const path = RouteUrls.Onboarding; - const urlParams = new URLSearchParams(); - urlParams.set('authRequest', payload); - if (IS_TEST_ENV) { - await openRequestInFullPage(path, urlParams); - } else { - popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` }); - } break; } - case ExternalMethods.transactionRequest: { - void storePayload({ - payload, - storageKey: StorageKey.transactionRequests, - port, - }); - const path = RouteUrls.TransactionRequest; - const urlParams = new URLSearchParams(); - urlParams.set('request', payload); - if (IS_TEST_ENV) { - await openRequestInFullPage(path, urlParams); - } else { - popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` }); - } - break; - } - default: - break; } - }); - } + } + ); }) ); // -// Events from the popup script -// Listener fn must return `true` to indicate the response will be async +// Events from the extension frames script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => Sentry.wrap(() => { - void backgroundMessageHandler(message, sender, sendResponse); + void internalBackgroundMessageHandler(message, sender, sendResponse); + // Listener fn must return `true` to indicate the response will be async return true; }) ); diff --git a/src/background/legacy-external-message-handler.ts b/src/background/legacy-external-message-handler.ts new file mode 100644 index 00000000000..bbf468bd6e4 --- /dev/null +++ b/src/background/legacy-external-message-handler.ts @@ -0,0 +1,64 @@ +import { ExternalMethods, LegacyMessageFromContentScript } from '@shared/message-types'; +import { RouteUrls } from '@shared/route-urls'; +import { StorageKey, storePayload } from '@shared/utils/storage'; +import { popupCenter } from './popup-center'; + +const IS_TEST_ENV = process.env.TEST_ENV === 'true'; + +// +// Playwright does not currently support Chrome extension popup testing: +// https://github.com/microsoft/playwright/issues/5593 +async function openRequestInFullPage(path: string, urlParams: URLSearchParams) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`index.html#${path}?${urlParams.toString()}`), + }); +} + +export function inferLegacyMessage(message: any): message is LegacyMessageFromContentScript { + // Now that we use a RPC communication style, we can infer + // legacy message types by presence of an id + return !Object.hasOwn(message, 'id'); +} + +export async function handleLegacyExternalMethodFormat( + message: LegacyMessageFromContentScript, + port: chrome.runtime.Port +) { + switch (message.method) { + case ExternalMethods.authenticationRequest: { + const { payload } = message; + void storePayload({ + payload, + storageKey: StorageKey.authenticationRequests, + port, + }); + const path = RouteUrls.Onboarding; + const urlParams = new URLSearchParams(); + urlParams.set('authRequest', payload); + if (IS_TEST_ENV) { + await openRequestInFullPage(path, urlParams); + } else { + popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` }); + } + break; + } + case ExternalMethods.transactionRequest: { + const { payload } = message; + + void storePayload({ + payload, + storageKey: StorageKey.transactionRequests, + port, + }); + const path = RouteUrls.TransactionRequest; + const urlParams = new URLSearchParams(); + urlParams.set('request', payload); + if (IS_TEST_ENV) { + await openRequestInFullPage(path, urlParams); + } else { + popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` }); + } + break; + } + } +} diff --git a/src/background/message-handler.ts b/src/background/message-handler.ts index bfd3f0befe3..e2639b357c3 100644 --- a/src/background/message-handler.ts +++ b/src/background/message-handler.ts @@ -37,7 +37,7 @@ const deriveWalletWithAccounts = memoize(async (secretKey: string, highestAccoun // Persists keys in memory for the duration of the background scripts life const inMemoryKeys = new Map(); -export async function backgroundMessageHandler( +export async function internalBackgroundMessageHandler( message: BackgroundActions, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index c696a0619f0..fd216ceec0e 100755 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -7,8 +7,8 @@ import { CONTENT_SCRIPT_PORT, ExternalMethods, - MessageFromContentScript, - MessageToContentScript, + LegacyMessageFromContentScript, + LegacyMessageToContentScript, MESSAGE_SOURCE, } from '@shared/message-types'; import { @@ -45,12 +45,12 @@ window.addEventListener('message', event => { const backgroundPort = chrome.runtime.connect({ name: CONTENT_SCRIPT_PORT }); // Sends message to background script that an event has fired -function sendMessageToBackground(message: MessageFromContentScript) { +function sendMessageToBackground(message: LegacyMessageFromContentScript) { backgroundPort.postMessage(message); } // Receives message from background script to execute in browser -chrome.runtime.onMessage.addListener((message: MessageToContentScript) => { +chrome.runtime.onMessage.addListener((message: LegacyMessageToContentScript) => { if (message.source === MESSAGE_SOURCE) { // Forward to web app (browser) window.postMessage(message, window.location.origin); @@ -59,7 +59,7 @@ chrome.runtime.onMessage.addListener((message: MessageToContentScript) => { interface ForwardDomEventToBackgroundArgs { payload: string; - method: MessageFromContentScript['method']; + method: LegacyMessageFromContentScript['method']; urlParam: string; path: RouteUrls; } @@ -94,6 +94,12 @@ document.addEventListener(DomEventName.transactionRequest, ((event: TransactionR }); }) as EventListener); +// Forward valid RPC events +// Validate from known list +document.addEventListener(DomEventName.rpcRequest, async (event: any) => { + backgroundPort.postMessage({ source: MESSAGE_SOURCE, ...event.detail }); +}); + // Inject inpage script (Stacks Provider) const inpage = document.createElement('script'); inpage.src = chrome.runtime.getURL('inpage.js'); diff --git a/src/inpage/inpage.ts b/src/inpage/inpage.ts index 0b72aceb564..0253411f553 100644 --- a/src/inpage/inpage.ts +++ b/src/inpage/inpage.ts @@ -7,8 +7,9 @@ import { import { AuthenticationResponseMessage, ExternalMethods, - MessageToContentScript, + LegacyMessageToContentScript, MESSAGE_SOURCE, + RpcMethodNames, TransactionResponseMessage, } from '@shared/message-types'; import { logger } from '@shared/logger'; @@ -18,7 +19,6 @@ type CallableMethods = keyof typeof ExternalMethods; interface ExtensionResponse { source: 'blockstack-extension'; method: CallableMethods; - [key: string]: any; } @@ -53,7 +53,7 @@ const callAndReceive = async ( }); }; -const isValidEvent = (event: MessageEvent, method: MessageToContentScript['method']) => { +const isValidEvent = (event: MessageEvent, method: LegacyMessageToContentScript['method']) => { const { data } = event; const correctSource = data.source === MESSAGE_SOURCE; const correctMethod = data.method === method; @@ -61,7 +61,7 @@ const isValidEvent = (event: MessageEvent, method: MessageToContentScript['metho }; const provider: StacksProvider = { - getURL: async () => { + async getURL() { const { url } = await callAndReceive('getURL'); return url; }, @@ -108,6 +108,24 @@ const provider: StacksProvider = { window.addEventListener('message', handleMessage); }); }, + + async request(method: RpcMethodNames, params?: any[]) { + return new Promise((resolve, _reject) => { + const id = crypto.randomUUID(); + const event = new CustomEvent(DomEventName.rpcRequest, { + detail: { jsonrpc: '2.0', id, method, params }, + }); + document.dispatchEvent(event); + const handleMessage = (event: MessageEvent) => { + if (event.data.id !== id) return; + + window.removeEventListener('message', handleMessage); + resolve(event.data); + }; + window.addEventListener('message', handleMessage); + }); + }, + getProductInfo() { return { version: VERSION, @@ -118,6 +136,22 @@ const provider: StacksProvider = { }, }; }, -}; +} as StacksProvider & { request(): Promise }; window.StacksProvider = provider; + +interface RpcRequestArgs { + method: RpcMethodNames; + params?: any[]; +} + +interface RpcEventArgs extends RpcRequestArgs { + jsonrpc: '2.0'; + id: string; +} + +declare global { + interface Crypto { + randomUUID: () => string; + } +} diff --git a/src/shared/inpage-types.ts b/src/shared/inpage-types.ts index fc7b2138ea7..d84e0fad953 100644 --- a/src/shared/inpage-types.ts +++ b/src/shared/inpage-types.ts @@ -4,6 +4,7 @@ export enum DomEventName { authenticationRequest = 'stacksAuthenticationRequest', transactionRequest = 'stacksTransactionRequest', + rpcRequest = 'stxRpcRequest', } export interface AuthenticationRequestEventDetails { diff --git a/src/shared/message-types.ts b/src/shared/message-types.ts index bf509065b4c..6aee0dcb29d 100644 --- a/src/shared/message-types.ts +++ b/src/shared/message-types.ts @@ -1,6 +1,6 @@ import { FinishedTxPayload, SponsoredFinishedTxPayload } from '@stacks/connect'; -export const MESSAGE_SOURCE = 'stacks-wallet' as const; +export const MESSAGE_SOURCE = 'hiro-wallet' as const; export const CONTENT_SCRIPT_PORT = 'content-script' as const; @@ -34,6 +34,41 @@ export interface Message payload: Payload; } +// +// RPC Methods, SIP pending + +export enum RpcMethods { + stx_requestAccounts, + stx_testAnotherMethod, +} + +export type RpcMethodNames = keyof typeof RpcMethods; + +interface RpcMessage { + id: string; + method: Method; + params?: Params; +} + +type RequestAccounts = RpcMessage<'stx_requestAccounts'>; +type TestAction = RpcMessage<'stx_testAnotherMethod'>; + +export type SupportedRpcMessages = RequestAccounts | TestAction; + +// interface SupportedMessagesReturnTypeMap { +// [RpcMethods.stx_requestAccounts]: { xxx: string }; +// [RpcMethods.stx_testAnotherMethod]: { yyy: string }; +// } + +// function xx(): // method: RpcMethods +// SupportedMessagesReturnTypeMap[Method] { + +// } + +// xx('stx_requestAccounts'); + +// +// Deprecated methods type AuthenticationRequestMessage = Message; export type AuthenticationResponseMessage = Message< @@ -56,5 +91,10 @@ export type TransactionResponseMessage = Message< } >; -export type MessageFromContentScript = AuthenticationRequestMessage | TransactionRequestMessage; -export type MessageToContentScript = AuthenticationResponseMessage | TransactionResponseMessage; +export type LegacyMessageFromContentScript = + | AuthenticationRequestMessage + | TransactionRequestMessage; + +export type LegacyMessageToContentScript = + | AuthenticationResponseMessage + | TransactionResponseMessage; diff --git a/test-app/src/components/home.tsx b/test-app/src/components/home.tsx index 5d5989d3494..a9e47771163 100644 --- a/test-app/src/components/home.tsx +++ b/test-app/src/components/home.tsx @@ -1,12 +1,13 @@ import React, { useContext, useState } from 'react'; import { AppContext } from '@common/context'; -import { Box, Text, Flex, BoxProps } from '@stacks/ui'; +import { Box, Text, Flex, BoxProps, Button } from '@stacks/ui'; import { Auth } from './auth'; import { Tab } from './tab'; import { Status } from './status'; import { Counter } from './counter'; import { Debugger } from './debugger'; import { Bns } from './bns'; +import { getStacksProvider } from '@stacks/connect'; type Tabs = 'status' | 'counter' | 'debug' | 'bns'; @@ -52,13 +53,29 @@ const Page: React.FC<{ tab: Tabs; setTab: (value: Tabs) => void }> = ({ tab, set export const Home: React.FC = () => { const state = useContext(AppContext); const [tab, setTab] = useState('debug'); - + const [account, setAccount] = useState(null); return ( Testnet Demo {state.userData ? : } + +
+ {account !== null &&
{JSON.stringify(account, null, 2)}
}
); };