Skip to content

Commit

Permalink
refactor: create new json rpc api
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Apr 21, 2022
1 parent 9a9771e commit 59aa057
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 80 deletions.
101 changes: 37 additions & 64 deletions src/background/background.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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;
})
);
Expand Down
64 changes: 64 additions & 0 deletions src/background/legacy-external-message-handler.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
2 changes: 1 addition & 1 deletion src/background/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 11 additions & 5 deletions src/content-scripts/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import {
CONTENT_SCRIPT_PORT,
ExternalMethods,
MessageFromContentScript,
MessageToContentScript,
LegacyMessageFromContentScript,
LegacyMessageToContentScript,
MESSAGE_SOURCE,
} from '@shared/message-types';
import {
Expand Down Expand Up @@ -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);
Expand All @@ -59,7 +59,7 @@ chrome.runtime.onMessage.addListener((message: MessageToContentScript) => {

interface ForwardDomEventToBackgroundArgs {
payload: string;
method: MessageFromContentScript['method'];
method: LegacyMessageFromContentScript['method'];
urlParam: string;
path: RouteUrls;
}
Expand Down Expand Up @@ -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');
Expand Down
44 changes: 39 additions & 5 deletions src/inpage/inpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
import {
AuthenticationResponseMessage,
ExternalMethods,
MessageToContentScript,
LegacyMessageToContentScript,
MESSAGE_SOURCE,
RpcMethodNames,
TransactionResponseMessage,
} from '@shared/message-types';
import { logger } from '@shared/logger';
Expand All @@ -18,7 +19,6 @@ type CallableMethods = keyof typeof ExternalMethods;
interface ExtensionResponse {
source: 'blockstack-extension';
method: CallableMethods;

[key: string]: any;
}

Expand Down Expand Up @@ -53,15 +53,15 @@ 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;
return correctSource && correctMethod && !!data.payload;
};

const provider: StacksProvider = {
getURL: async () => {
async getURL() {
const { url } = await callAndReceive('getURL');
return url;
},
Expand Down Expand Up @@ -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<RpcEventArgs>(DomEventName.rpcRequest, {
detail: { jsonrpc: '2.0', id, method, params },
});
document.dispatchEvent(event);
const handleMessage = (event: MessageEvent<any>) => {
if (event.data.id !== id) return;

window.removeEventListener('message', handleMessage);
resolve(event.data);
};
window.addEventListener('message', handleMessage);
});
},

getProductInfo() {
return {
version: VERSION,
Expand All @@ -118,6 +136,22 @@ const provider: StacksProvider = {
},
};
},
};
} as StacksProvider & { request(): Promise<void> };

window.StacksProvider = provider;

interface RpcRequestArgs {
method: RpcMethodNames;
params?: any[];
}

interface RpcEventArgs extends RpcRequestArgs {
jsonrpc: '2.0';
id: string;
}

declare global {
interface Crypto {
randomUUID: () => string;
}
}
1 change: 1 addition & 0 deletions src/shared/inpage-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export enum DomEventName {
authenticationRequest = 'stacksAuthenticationRequest',
transactionRequest = 'stacksTransactionRequest',
rpcRequest = 'stxRpcRequest',
}

export interface AuthenticationRequestEventDetails {
Expand Down
Loading

0 comments on commit 59aa057

Please sign in to comment.