refactor: one shared extension-API module, and drive the dApp flows on Firefox (closes #153)
This commit was merged in pull request #281.
This commit is contained in:
@@ -40,17 +40,21 @@ const {
|
||||
registerAlarmHandlers,
|
||||
} = require("../shared/alarms");
|
||||
|
||||
const storageApi =
|
||||
typeof browser !== "undefined"
|
||||
? browser.storage.local
|
||||
: chrome.storage.local;
|
||||
const runtime =
|
||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||
const windowsApi =
|
||||
typeof browser !== "undefined" ? browser.windows : chrome.windows;
|
||||
const tabsApi = typeof browser !== "undefined" ? browser.tabs : chrome.tabs;
|
||||
const actionApi =
|
||||
typeof browser !== "undefined" ? browser.browserAction : chrome.action;
|
||||
const {
|
||||
actionApi,
|
||||
runtimeApi,
|
||||
storageGet,
|
||||
tabsQuery,
|
||||
tabsSendMessage,
|
||||
windowsApi,
|
||||
windowsCreate,
|
||||
windowsGetLastFocused,
|
||||
windowsRemove,
|
||||
} = require("../shared/browserApi");
|
||||
|
||||
const runtime = runtimeApi();
|
||||
const windowsNs = windowsApi();
|
||||
const actionNs = actionApi();
|
||||
|
||||
// Connected sites (in-memory, non-persisted): { "origin:address": true }
|
||||
const connectedSites = {};
|
||||
@@ -167,7 +171,7 @@ function approvedNonce(approvedTx) {
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
const result = await storageApi.get("autistmask");
|
||||
const result = await storageGet("autistmask");
|
||||
return (
|
||||
result.autistmask || {
|
||||
wallets: [],
|
||||
@@ -231,8 +235,8 @@ async function proxyRpc(method, params) {
|
||||
}
|
||||
|
||||
function resetPopupUrl() {
|
||||
if (actionApi && typeof actionApi.setPopup === "function") {
|
||||
actionApi.setPopup({ popup: "src/popup/index.html" });
|
||||
if (actionNs && typeof actionNs.setPopup === "function") {
|
||||
actionNs.setPopup({ popup: "src/popup/index.html" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,58 +340,71 @@ function releaseApproval(approval) {
|
||||
// Open approval in a separate popup window.
|
||||
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
|
||||
// not from a user gesture) and the fallback for site-connection approvals.
|
||||
function openApprovalWindow(id) {
|
||||
// Never rejects. Its callers raise it from inside a Promise executor and drop
|
||||
// the result on the floor, so a rejection here would be unhandled.
|
||||
async function openApprovalWindow(id) {
|
||||
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
||||
const popupWidth = 360;
|
||||
const popupHeight = 600;
|
||||
|
||||
windowsApi.getLastFocused((currentWin) => {
|
||||
const opts = {
|
||||
url: popupUrl,
|
||||
type: "popup",
|
||||
width: popupWidth,
|
||||
height: popupHeight,
|
||||
};
|
||||
if (currentWin) {
|
||||
opts.left = Math.round(
|
||||
currentWin.left + (currentWin.width - popupWidth) / 2,
|
||||
);
|
||||
opts.top = Math.round(
|
||||
currentWin.top + (currentWin.height - popupHeight) / 2,
|
||||
);
|
||||
}
|
||||
windowsApi.create(opts, (win) => {
|
||||
const approval = pendingApprovals[id];
|
||||
if (!approval) {
|
||||
// Settled while the window was opening — an address switch,
|
||||
// say. Nothing is waiting on it, and a window showing an
|
||||
// approval that no longer exists is not left on screen.
|
||||
if (win) {
|
||||
windowsApi.remove(win.id, () => {
|
||||
if (runtime.lastError) {
|
||||
// window already closed
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!win) {
|
||||
// No window means no way to ever answer this approval, and an
|
||||
// approval nothing can answer holds the requesting page's
|
||||
// promise open forever. Settle it now instead.
|
||||
settleApproval(
|
||||
id,
|
||||
abandonedResult(
|
||||
approval,
|
||||
APPROVAL_WINDOW_FAILED_CODE,
|
||||
APPROVAL_WINDOW_FAILED_MESSAGE,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
approval.windowId = win.id;
|
||||
});
|
||||
});
|
||||
let currentWin = null;
|
||||
try {
|
||||
currentWin = await windowsGetLastFocused();
|
||||
} catch {
|
||||
// Nothing focused to centre on. The window still opens, at whatever
|
||||
// position the browser picks.
|
||||
}
|
||||
|
||||
const opts = {
|
||||
url: popupUrl,
|
||||
type: "popup",
|
||||
width: popupWidth,
|
||||
height: popupHeight,
|
||||
};
|
||||
if (currentWin) {
|
||||
opts.left = Math.round(
|
||||
currentWin.left + (currentWin.width - popupWidth) / 2,
|
||||
);
|
||||
opts.top = Math.round(
|
||||
currentWin.top + (currentWin.height - popupHeight) / 2,
|
||||
);
|
||||
}
|
||||
|
||||
let win = null;
|
||||
try {
|
||||
win = await windowsCreate(opts);
|
||||
} catch (e) {
|
||||
// The promise namespace reports the failure by rejecting where the
|
||||
// callback namespace reported it by handing back no window; both land
|
||||
// on the !win branch below, which settles the approval.
|
||||
log.errorf("could not open the approval window:", e);
|
||||
}
|
||||
|
||||
const approval = pendingApprovals[id];
|
||||
if (!approval) {
|
||||
// Settled while the window was opening — an address switch, say.
|
||||
// Nothing is waiting on it, and a window showing an approval that no
|
||||
// longer exists is not left on screen. The await above makes this a
|
||||
// real race: writing the id back would resurrect a bare entry that
|
||||
// nothing would ever resolve.
|
||||
if (win) windowsRemove(win.id).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (!win) {
|
||||
// No window means no way to ever answer this approval, and an
|
||||
// approval nothing can answer holds the requesting page's promise
|
||||
// open forever. Settle it now instead.
|
||||
settleApproval(
|
||||
id,
|
||||
abandonedResult(
|
||||
approval,
|
||||
APPROVAL_WINDOW_FAILED_CODE,
|
||||
APPROVAL_WINDOW_FAILED_MESSAGE,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
approval.windowId = win.id;
|
||||
}
|
||||
|
||||
// Open an approval popup and return a promise that resolves with the user decision.
|
||||
@@ -397,12 +414,12 @@ function requestApproval(origin, hostname) {
|
||||
const id = crypto.randomUUID();
|
||||
pendingApprovals[id] = { id, origin, hostname, resolve };
|
||||
|
||||
if (actionApi && typeof actionApi.openPopup === "function") {
|
||||
actionApi.setPopup({
|
||||
if (actionNs && typeof actionNs.openPopup === "function") {
|
||||
actionNs.setPopup({
|
||||
popup: "src/popup/index.html?approval=" + id,
|
||||
});
|
||||
try {
|
||||
const result = actionApi.openPopup();
|
||||
const result = actionNs.openPopup();
|
||||
if (result && typeof result.catch === "function") {
|
||||
result.catch(() => openApprovalWindow(id));
|
||||
}
|
||||
@@ -469,7 +486,7 @@ function requestSignApproval(origin, hostname, signParams, approvedFrom) {
|
||||
|
||||
// Detect when an approval popup (browser-action) closes without a response.
|
||||
// TX and sign approvals now use windows.create() and are handled by the
|
||||
// windowsApi.onRemoved listener below, but we still handle site-connection
|
||||
// windows.onRemoved listener below, but we still handle site-connection
|
||||
// approval disconnects here.
|
||||
runtime.onConnect.addListener((port) => {
|
||||
if (port.name.startsWith("approval:")) {
|
||||
@@ -883,24 +900,26 @@ async function handleSendTransaction(params, origin) {
|
||||
}
|
||||
|
||||
// Broadcast chainChanged to all tabs when the network is switched.
|
||||
function broadcastChainChanged(chainId) {
|
||||
tabsApi.query({}, (tabs) => {
|
||||
for (const tab of tabs) {
|
||||
tabsApi.sendMessage(
|
||||
tab.id,
|
||||
{
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "chainChanged",
|
||||
data: chainId,
|
||||
},
|
||||
() => {
|
||||
if (runtime.lastError) {
|
||||
// expected for tabs without our content script
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
//
|
||||
// Never rejects: its caller is an RPC handler that must answer the page
|
||||
// whatever the browser made of the broadcast.
|
||||
async function broadcastChainChanged(chainId) {
|
||||
let tabs;
|
||||
try {
|
||||
tabs = await tabsQuery({});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const tab of tabs) {
|
||||
// A tab with no content script has no receiver, and that is the
|
||||
// ordinary case rather than a fault. The rejection it produces is the
|
||||
// promise-shaped form of the runtime.lastError this used to read.
|
||||
tabsSendMessage(tab.id, {
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "chainChanged",
|
||||
data: chainId,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast accountsChanged to all tabs, respecting per-address permissions
|
||||
@@ -921,41 +940,36 @@ async function broadcastAccountsChanged() {
|
||||
);
|
||||
if (!settleApproval(id, rejection)) continue;
|
||||
if (approval.windowId) {
|
||||
windowsApi.remove(approval.windowId, () => {
|
||||
if (runtime.lastError) {
|
||||
// window already closed
|
||||
}
|
||||
});
|
||||
// Rejects when the window has already gone, which is a race the
|
||||
// user wins routinely by closing it themselves.
|
||||
windowsRemove(approval.windowId).catch(() => {});
|
||||
}
|
||||
}
|
||||
resetPopupUrl();
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
const allowed = activeAddress ? s.allowedSites[activeAddress] || [] : [];
|
||||
tabsApi.query({}, (tabs) => {
|
||||
for (const tab of tabs) {
|
||||
const origin = tab.url ? new URL(tab.url).origin : "";
|
||||
const hostname = extractHostname(origin);
|
||||
const hasPermission =
|
||||
activeAddress &&
|
||||
(allowed.includes(hostname) ||
|
||||
connectedSites[origin + ":" + activeAddress]);
|
||||
tabsApi.sendMessage(
|
||||
tab.id,
|
||||
{
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "accountsChanged",
|
||||
data: hasPermission ? [activeAddress] : [],
|
||||
},
|
||||
() => {
|
||||
// Ignore errors for tabs without content script
|
||||
if (runtime.lastError) {
|
||||
// expected for tabs without our content script
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
let tabs;
|
||||
try {
|
||||
tabs = await tabsQuery({});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const tab of tabs) {
|
||||
const origin = tab.url ? new URL(tab.url).origin : "";
|
||||
const hostname = extractHostname(origin);
|
||||
const hasPermission =
|
||||
activeAddress &&
|
||||
(allowed.includes(hostname) ||
|
||||
connectedSites[origin + ":" + activeAddress]);
|
||||
// Same as chainChanged above: a tab without our content script
|
||||
// rejects, and that is expected rather than a fault.
|
||||
tabsSendMessage(tab.id, {
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "accountsChanged",
|
||||
data: hasPermission ? [activeAddress] : [],
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Background balance refresh: every 60 seconds when the popup isn't open.
|
||||
@@ -1050,8 +1064,8 @@ startBackgroundJobs();
|
||||
// outcome to the page — and the window is recorded as gone, so that an attempt
|
||||
// which then fails retryably settles instead of waiting in a window that no
|
||||
// longer exists.
|
||||
if (windowsApi && windowsApi.onRemoved) {
|
||||
windowsApi.onRemoved.addListener((windowId) => {
|
||||
if (windowsNs && windowsNs.onRemoved) {
|
||||
windowsNs.onRemoved.addListener((windowId) => {
|
||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||
if (approval.windowId !== windowId) continue;
|
||||
const rejection = abandonedResult(
|
||||
|
||||
Reference in New Issue
Block a user