From a03a6325293e2c7bc204edde2ce8c927c47ed401 Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Fri, 12 Mar 2021 02:48:50 -0800 Subject: [PATCH 1/2] Connector: disconnects Port disconnects cause the disconnect event to be dispatched to the dApp. Removing a site from the whitelist disconnects the associated port, if it exists (e.g. actively connected). Ports are now only initiated upon a request for read access to avoid creating many ports for non-dApp websites. --- .../yoroi-ergo-connector/example/index.js | 25 +++- packages/yoroi-ergo-connector/src/inject.js | 126 ++++++++++++------ .../ergo-connector/stores/ConnectorStore.js | 4 + .../chrome/extension/background.js | 22 ++- 4 files changed, 125 insertions(+), 52 deletions(-) diff --git a/packages/yoroi-ergo-connector/example/index.js b/packages/yoroi-ergo-connector/example/index.js index 9191697420f..36ccaa5d90f 100644 --- a/packages/yoroi-ergo-connector/example/index.js +++ b/packages/yoroi-ergo-connector/example/index.js @@ -1,9 +1,6 @@ import * as wasm from "ergo-lib-wasm-browser"; -if (typeof ergo_request_read_access === "undefined") { - alert("ergo not found"); -} else { - console.log("ergo found") +function initDapp() { ergo_request_read_access().then(function(access_granted) { if (!access_granted) { //alert("ergo access denied"); @@ -13,9 +10,6 @@ if (typeof ergo_request_read_access === "undefined") { const status = document.getElementById("status"); status.innerText = "Wallet successfully connected"; console.log("ergo access given"); - window.addEventListener("ergo_wallet_disconnected", function(event) { - alert("wallet disconnected"); - }); // ergo.get_unused_addresses().then(function(addresses) { // //console.log(`get_unused_addresses() = {`); // // for (const address of addresses) { @@ -140,3 +134,20 @@ if (typeof ergo_request_read_access === "undefined") { } }); } + +if (typeof ergo_request_read_access === "undefined") { + alert("ergo not found"); +} else { + console.log("ergo found"); + window.addEventListener("ergo_wallet_disconnected", function(event) { + const status = document.getElementById("status"); + status.innerText = ""; + const div = document.getElementById("balance"); + div.innerText = "Wallet disconnected."; + const button = document.createElement("button"); + button.textContent = "Reconnect"; + button.onclick = initDapp; + div.appendChild(button); + }); + initDapp(); +} \ No newline at end of file diff --git a/packages/yoroi-ergo-connector/src/inject.js b/packages/yoroi-ergo-connector/src/inject.js index e25e8d34945..8aa385e1d12 100644 --- a/packages/yoroi-ergo-connector/src/inject.js +++ b/packages/yoroi-ergo-connector/src/inject.js @@ -15,16 +15,12 @@ window.addEventListener("message", function(event) { }); function ergo_request_read_access() { - if (typeof ergo !== "undefined") { - return Promise.resolve(true); - } else { - return new Promise(function(resolve, reject) { - window.postMessage({ - type: "connector_connect_request", - }, location.origin); - connectRequests.push({ resolve: resolve, reject: reject }); - }); - } + return new Promise(function(resolve, reject) { + window.postMessage({ + type: "connector_connect_request", + }, location.origin); + connectRequests.push({ resolve: resolve, reject: reject }); + }); } function ergo_check_read_access() { @@ -34,27 +30,6 @@ function ergo_check_read_access() { return Promise.resolve(false); } } - -// TODO: fix or change back how RPCs work -// // disconnect detector -// setInterval(function() { -// if (timeout == 20) { -// window.dispatchEvent(new Event("ergo_wallet_disconnected")); -// } -// if (timeout == 25) { -// rpcResolver.forEach(function(rpc) { -// rpc.reject("timed out"); -// }); -// } -// timeout += 1; -// }, 1000); - -// // ping sender -// setInterval(function() { -// _ergo_rpc_call("ping", []).then(function() { -// timeout = 0; -// }); -// }, 10000); ` // client-facing ergo object API @@ -139,6 +114,8 @@ class ErgoAPI { const ergo = Object.freeze(new ErgoAPI()); ` +const API_INTERNAL_ERROR = -2; +const API_REFUSED = -3; function injectIntoPage(code) { try { @@ -164,24 +141,37 @@ function shouldInject() { return docElemCheck && docTypeCheck; } -if (shouldInject()) { - console.log(`content script injected into ${location.hostname}`); - injectIntoPage(initialInject); +let yoroiPort = null; +let fullApiInjected = false; + +function disconnectWallet() { + yoroiPort = null; + window.dispatchEvent(new Event("ergo_wallet_disconnected")); +} +function createYoroiPort() { // events from Yoroi - let yoroiPort = chrome.runtime.connect(extensionId); + yoroiPort = chrome.runtime.connect(extensionId); yoroiPort.onMessage.addListener(message => { //alert("content script message: " + JSON.stringify(message)); if (message.type == "connector_rpc_response") { window.postMessage(message, location.origin); } else if (message.type == "yoroi_connect_response") { if (message.success) { - // inject full API here - if (injectIntoPage(apiInject)) { - chrome.runtime.sendMessage({type: "init_page_action"}); - } else { - alert("failed to inject Ergo API"); - // TODO: return an error instead here if injection fails? + if (!fullApiInjected) { + // inject full API here + if (injectIntoPage(apiInject)) { + fullApiInjected = true; + } else { + console.error() + window.postMessage({ + type: "connector_connected", + err: { + code: API_INTERNAL_ERROR, + info: "failed to inject Ergo API" + } + }, location.origin); + } } } window.postMessage({ @@ -191,17 +181,67 @@ if (shouldInject()) { } }); + yoroiPort.onDisconnect.addListener(event => { + disconnectWallet(); + }); +} + +if (shouldInject()) { + console.log(`content script injected into ${location.hostname}`); + injectIntoPage(initialInject); + // events from page (injected code) window.addEventListener("message", function(event) { if (event.data.type === "connector_rpc_request") { console.log("connector received from page: " + JSON.stringify(event.data) + " with source = " + event.source + " and origin = " + event.origin); - yoroiPort.postMessage(event.data); + if (yoroiPort) { + try { + yoroiPort.postMessage(event.data); + return; + } catch (e) { + console.error(`Could not send RPC to Yoroi: ${e}`); + window.postMessage({ + type: "connector_rpc_response", + uid: event.data.uid, + return: { + err: { + code: API_INTERNAL_ERROR, + info: `Could not send RPC to Yoroi: ${e}` + } + } + }, location.origin); + } + } else { + window.postMessage({ + type: "connector_rpc_response", + uid: event.data.uid, + return: { + err: { + code: API_REFUSED, + info: 'Wallet disconnected' + } + } + }, location.origin); + } } else if (event.data.type == "connector_connect_request") { + if (fullApiInjected) { + if (yoroiPort) { + // we can skip communication - API injected + hasn't been disconnected + window.postMessage({ + type: "connector_connected", + success: true + }, location.origin); + return; + } + } + if (yoroiPort) { + createYoroiPort(); + } // URL must be provided here as the url field of Tab is only available // with the "tabs" permission which Yoroi doesn't have yoroiPort.postMessage({ type: "yoroi_connect_request", - url: location.hostname + url: location.hostname, }); } }); diff --git a/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js b/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js index 47c62299bf0..2afb7f1604e 100644 --- a/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js +++ b/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js @@ -215,6 +215,10 @@ export default class ConnectorStore extends Store { whitelist: filter, }); await this.getConnectorWhitelist.execute(); + window.chrome.runtime.sendMessage({ + type: 'remove_wallet_from_whitelist', + url, + }); }; // ========== active websites ========== // diff --git a/packages/yoroi-extension/chrome/extension/background.js b/packages/yoroi-extension/chrome/extension/background.js index f9f6756243c..eeeea8a7004 100644 --- a/packages/yoroi-extension/chrome/extension/background.js +++ b/packages/yoroi-extension/chrome/extension/background.js @@ -129,8 +129,13 @@ async function getChecksum( return checksum; } -// tab id key -const connectedSites: Map = new Map(); +type TabId = number; + +const connectedSites: Map = new Map(); + +// tabid => chrome.runtime.Port +const ports: Map = new Map(); + let pendingTxs: PendingTransaction[] = []; @@ -249,6 +254,7 @@ async function getSelectedWallet(tabId: number): Promise> { return Promise.reject(new Error(`could not find tabId ${tabId} in connected sites`)); } +// messages from other parts of Yoroi (i.e. the UI for the connector) chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { async function signTxInputs( tx, @@ -348,6 +354,17 @@ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { } } sendResponse(null); + } else if (request.type === 'remove_wallet_from_whitelist') { + for (const [tabId, site] of connectedSites) { + if (site.url === request.url) { + const port = ports.get(tabId); + if (port) { + port.disconnect(); + ports.delete(tabId); + } + break; + } + } } }); @@ -427,6 +444,7 @@ chrome.runtime.onMessageExternal.addListener((message, sender) => { chrome.runtime.onConnectExternal.addListener(port => { if (port.sender.id === environment.ergoConnectorExtensionId) { const tabId = port.sender.tab.id; + ports.set(tabId, port); port.onMessage.addListener(async message => { function rpcResponse(response) { port.postMessage({ From a456299f646fd2927647c03b707232ea635a1a4b Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Mon, 15 Mar 2021 23:27:55 -0700 Subject: [PATCH 2/2] Remove unused injected variable/rename others/quick fix --- packages/yoroi-ergo-connector/src/inject.js | 56 ++++++++++----------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/yoroi-ergo-connector/src/inject.js b/packages/yoroi-ergo-connector/src/inject.js index 8aa385e1d12..72637fc8a03 100644 --- a/packages/yoroi-ergo-connector/src/inject.js +++ b/packages/yoroi-ergo-connector/src/inject.js @@ -1,15 +1,13 @@ // sets up RPC communication with the connector + access check/request functions const initialInject = ` -var timeout = 0; - -var connectRequests = []; +var ergoConnectRequests = []; window.addEventListener("message", function(event) { if (event.data.type == "connector_connected") { if (event.data.err !== undefined) { - connectRequests.forEach(promise => promise.reject(event.data.err)); + ergoConnectRequests.forEach(promise => promise.reject(event.data.err)); } else { - connectRequests.forEach(promise => promise.resolve(event.data.success)); + ergoConnectRequests.forEach(promise => promise.resolve(event.data.success)); } } }); @@ -19,7 +17,7 @@ function ergo_request_read_access() { window.postMessage({ type: "connector_connect_request", }, location.origin); - connectRequests.push({ resolve: resolve, reject: reject }); + ergoConnectRequests.push({ resolve: resolve, reject: reject }); }); } @@ -35,13 +33,13 @@ function ergo_check_read_access() { // client-facing ergo object API const apiInject = ` // RPC set-up -var rpcUid = 0; -var rpcResolver = new Map(); +var ergoRpcUid = 0; +var ergoRpcResolver = new Map(); window.addEventListener("message", function(event) { if (event.data.type == "connector_rpc_response") { console.log("page received from connector: " + JSON.stringify(event.data) + " with source = " + event.source + " and origin = " + event.origin); - const rpcPromise = rpcResolver.get(event.data.uid); + const rpcPromise = ergoRpcResolver.get(event.data.uid); if (rpcPromise !== undefined) { const ret = event.data.return; if (ret.err !== undefined) { @@ -101,13 +99,13 @@ class ErgoAPI { return new Promise(function(resolve, reject) { window.postMessage({ type: "connector_rpc_request", - uid: rpcUid, + uid: ergoRpcUid, function: func, params: params }, location.origin); - console.log("rpcUid = " + rpcUid); - rpcResolver.set(rpcUid, { resolve: resolve, reject: reject }); - rpcUid += 1; + console.log("ergoRpcUid = " + ergoRpcUid); + ergoRpcResolver.set(ergoRpcUid, { resolve: resolve, reject: reject }); + ergoRpcUid += 1; }); } } @@ -224,25 +222,23 @@ if (shouldInject()) { }, location.origin); } } else if (event.data.type == "connector_connect_request") { - if (fullApiInjected) { - if (yoroiPort) { - // we can skip communication - API injected + hasn't been disconnected - window.postMessage({ - type: "connector_connected", - success: true - }, location.origin); - return; + if (fullApiInjected && yoroiPort) { + // we can skip communication - API injected + hasn't been disconnected + window.postMessage({ + type: "connector_connected", + success: true + }, location.origin); + } else { + if (yoroiPort == null) { + createYoroiPort(); } + // URL must be provided here as the url field of Tab is only available + // with the "tabs" permission which Yoroi doesn't have + yoroiPort.postMessage({ + type: "yoroi_connect_request", + url: location.hostname, + }); } - if (yoroiPort) { - createYoroiPort(); - } - // URL must be provided here as the url field of Tab is only available - // with the "tabs" permission which Yoroi doesn't have - yoroiPort.postMessage({ - type: "yoroi_connect_request", - url: location.hostname, - }); } }); }