Add site connection permissions, approval flow, and active address
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
- Add activeAddress, allowedSites, deniedSites, rememberSiteChoice to persisted state - Replace auto-connect with permission checks: allowed sites connect automatically, denied sites are rejected, unknown sites trigger an approval popup - Add approval popup UI with hostname display, active address preview, remember checkbox, and allow/deny buttons - Add ACTIVE/[select] indicator on address rows in the main view to set the active web3 address - Add allowed/denied site list management in settings with delete buttons - Broadcast accountsChanged to connected dapps when active address changes - Handle approval window close as implicit denial
This commit is contained in:
@@ -15,29 +15,51 @@ const storageApi =
|
|||||||
: chrome.storage.local;
|
: chrome.storage.local;
|
||||||
const runtime =
|
const runtime =
|
||||||
typeof browser !== "undefined" ? browser.runtime : chrome.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;
|
||||||
|
|
||||||
// Connected sites: { origin: [address, ...] }
|
// Connected sites (in-memory, non-persisted): { origin: true }
|
||||||
const connectedSites = {};
|
const connectedSites = {};
|
||||||
|
|
||||||
|
// Pending approval requests: { id: { origin, hostname, resolve } }
|
||||||
|
const pendingApprovals = {};
|
||||||
|
let nextApprovalId = 1;
|
||||||
|
|
||||||
async function getState() {
|
async function getState() {
|
||||||
const result = await storageApi.get("autistmask");
|
const result = await storageApi.get("autistmask");
|
||||||
return result.autistmask || { wallets: [], rpcUrl: DEFAULT_RPC_URL };
|
return (
|
||||||
|
result.autistmask || {
|
||||||
|
wallets: [],
|
||||||
|
rpcUrl: DEFAULT_RPC_URL,
|
||||||
|
activeAddress: null,
|
||||||
|
allowedSites: [],
|
||||||
|
deniedSites: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccounts() {
|
async function getActiveAddress() {
|
||||||
const state = await getState();
|
const s = await getState();
|
||||||
const accounts = [];
|
if (s.activeAddress) return s.activeAddress;
|
||||||
for (const wallet of state.wallets) {
|
// Fall back to first address
|
||||||
for (const addr of wallet.addresses) {
|
if (s.wallets.length > 0 && s.wallets[0].addresses.length > 0) {
|
||||||
accounts.push(addr.address);
|
return s.wallets[0].addresses[0].address;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return accounts;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRpcUrl() {
|
async function getRpcUrl() {
|
||||||
const state = await getState();
|
const s = await getState();
|
||||||
return state.rpcUrl || DEFAULT_RPC_URL;
|
return s.rpcUrl || DEFAULT_RPC_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHostname(origin) {
|
||||||
|
try {
|
||||||
|
return new URL(origin).hostname;
|
||||||
|
} catch {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy an RPC call to the Ethereum node
|
// Proxy an RPC call to the Ethereum node
|
||||||
@@ -60,6 +82,86 @@ async function proxyRpc(method, params) {
|
|||||||
return json.result;
|
return json.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open an approval popup and return a promise that resolves with the user decision
|
||||||
|
function requestApproval(origin, hostname) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const id = nextApprovalId++;
|
||||||
|
pendingApprovals[id] = { origin, hostname, resolve };
|
||||||
|
|
||||||
|
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
||||||
|
windowsApi.create(
|
||||||
|
{
|
||||||
|
url: popupUrl,
|
||||||
|
type: "popup",
|
||||||
|
width: 400,
|
||||||
|
height: 500,
|
||||||
|
},
|
||||||
|
(win) => {
|
||||||
|
if (win) {
|
||||||
|
pendingApprovals[id].windowId = win.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle connection requests (eth_requestAccounts, wallet_requestPermissions)
|
||||||
|
async function handleConnectionRequest(origin) {
|
||||||
|
const s = await getState();
|
||||||
|
const activeAddress = await getActiveAddress();
|
||||||
|
if (!activeAddress) {
|
||||||
|
return { error: { message: "No accounts available" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = extractHostname(origin);
|
||||||
|
|
||||||
|
// Check denied list
|
||||||
|
if (s.deniedSites.includes(hostname)) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 4001,
|
||||||
|
message: "User rejected the request.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowed list or in-memory connected
|
||||||
|
if (s.allowedSites.includes(hostname) || connectedSites[origin]) {
|
||||||
|
return { result: [activeAddress] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open approval popup
|
||||||
|
const decision = await requestApproval(origin, hostname);
|
||||||
|
|
||||||
|
if (decision.approved) {
|
||||||
|
if (decision.remember) {
|
||||||
|
// Reload state to get latest, add to allowed, persist
|
||||||
|
await loadState();
|
||||||
|
if (!state.allowedSites.includes(hostname)) {
|
||||||
|
state.allowedSites.push(hostname);
|
||||||
|
}
|
||||||
|
await saveState();
|
||||||
|
} else {
|
||||||
|
connectedSites[origin] = true;
|
||||||
|
}
|
||||||
|
return { result: [activeAddress] };
|
||||||
|
} else {
|
||||||
|
if (decision.remember) {
|
||||||
|
await loadState();
|
||||||
|
if (!state.deniedSites.includes(hostname)) {
|
||||||
|
state.deniedSites.push(hostname);
|
||||||
|
}
|
||||||
|
await saveState();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 4001,
|
||||||
|
message: "User rejected the request.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Methods that are safe to proxy directly to the RPC node
|
// Methods that are safe to proxy directly to the RPC node
|
||||||
const PROXY_METHODS = [
|
const PROXY_METHODS = [
|
||||||
"eth_blockNumber",
|
"eth_blockNumber",
|
||||||
@@ -86,15 +188,20 @@ const PROXY_METHODS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function handleRpc(method, params, origin) {
|
async function handleRpc(method, params, origin) {
|
||||||
// Methods that need wallet involvement
|
// Connection requests — go through approval flow
|
||||||
if (method === "eth_requestAccounts" || method === "eth_accounts") {
|
if (method === "eth_requestAccounts") {
|
||||||
const accounts = await getAccounts();
|
return handleConnectionRequest(origin);
|
||||||
if (accounts.length === 0) {
|
}
|
||||||
return { error: { message: "No accounts available" } };
|
|
||||||
|
if (method === "eth_accounts") {
|
||||||
|
const s = await getState();
|
||||||
|
const activeAddress = await getActiveAddress();
|
||||||
|
if (!activeAddress) return { result: [] };
|
||||||
|
const hostname = extractHostname(origin);
|
||||||
|
if (s.allowedSites.includes(hostname) || connectedSites[origin]) {
|
||||||
|
return { result: [activeAddress] };
|
||||||
}
|
}
|
||||||
// Auto-connect for now (approval flow is a future TODO)
|
return { result: [] };
|
||||||
connectedSites[origin] = accounts;
|
|
||||||
return { result: accounts };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "eth_chainId") {
|
if (method === "eth_chainId") {
|
||||||
@@ -128,11 +235,8 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "wallet_requestPermissions") {
|
if (method === "wallet_requestPermissions") {
|
||||||
const accounts = await getAccounts();
|
const connResult = await handleConnectionRequest(origin);
|
||||||
if (accounts.length === 0) {
|
if (connResult.error) return connResult;
|
||||||
return { error: { message: "No accounts available" } };
|
|
||||||
}
|
|
||||||
connectedSites[origin] = accounts;
|
|
||||||
return {
|
return {
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
@@ -140,7 +244,7 @@ async function handleRpc(method, params, origin) {
|
|||||||
caveats: [
|
caveats: [
|
||||||
{
|
{
|
||||||
type: "restrictReturnedAccounts",
|
type: "restrictReturnedAccounts",
|
||||||
value: accounts,
|
value: connResult.result,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -149,8 +253,12 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "wallet_getPermissions") {
|
if (method === "wallet_getPermissions") {
|
||||||
const accounts = connectedSites[origin] || [];
|
const s = await getState();
|
||||||
if (accounts.length === 0) {
|
const activeAddress = await getActiveAddress();
|
||||||
|
const hostname = extractHostname(origin);
|
||||||
|
const isConnected =
|
||||||
|
s.allowedSites.includes(hostname) || connectedSites[origin];
|
||||||
|
if (!isConnected || !activeAddress) {
|
||||||
return { result: [] };
|
return { result: [] };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -160,7 +268,7 @@ async function handleRpc(method, params, origin) {
|
|||||||
caveats: [
|
caveats: [
|
||||||
{
|
{
|
||||||
type: "restrictReturnedAccounts",
|
type: "restrictReturnedAccounts",
|
||||||
value: accounts,
|
value: [activeAddress],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -169,14 +277,12 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "personal_sign" || method === "eth_sign") {
|
if (method === "personal_sign" || method === "eth_sign") {
|
||||||
// TODO: implement signature approval flow
|
|
||||||
return {
|
return {
|
||||||
error: { message: "Signing not yet implemented in AutistMask." },
|
error: { message: "Signing not yet implemented in AutistMask." },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
||||||
// TODO: implement typed data signing
|
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
message:
|
message:
|
||||||
@@ -186,8 +292,6 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "eth_sendTransaction") {
|
if (method === "eth_sendTransaction") {
|
||||||
// TODO: implement transaction signing approval flow
|
|
||||||
// For now, return an error directing the user to use the popup
|
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
message:
|
message:
|
||||||
@@ -209,6 +313,30 @@ async function handleRpc(method, params, origin) {
|
|||||||
return { error: { message: "Unsupported method: " + method } };
|
return { error: { message: "Unsupported method: " + method } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast accountsChanged to all tabs
|
||||||
|
async function broadcastAccountsChanged() {
|
||||||
|
const activeAddress = await getActiveAddress();
|
||||||
|
const accounts = activeAddress ? [activeAddress] : [];
|
||||||
|
tabsApi.query({}, (tabs) => {
|
||||||
|
for (const tab of tabs) {
|
||||||
|
tabsApi.sendMessage(
|
||||||
|
tab.id,
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_EVENT",
|
||||||
|
eventName: "accountsChanged",
|
||||||
|
data: accounts,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Ignore errors for tabs without content script
|
||||||
|
if (runtime.lastError) {
|
||||||
|
// expected for tabs without our content script
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Background balance refresh: every 60 seconds when the popup isn't open.
|
// Background balance refresh: every 60 seconds when the popup isn't open.
|
||||||
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
||||||
// fresh, so this naturally skips.
|
// fresh, so this naturally skips.
|
||||||
@@ -227,14 +355,59 @@ async function backgroundRefresh() {
|
|||||||
|
|
||||||
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
|
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
|
||||||
|
|
||||||
// Listen for messages from content scripts
|
// When approval window is closed without a response, treat as rejection
|
||||||
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
if (windowsApi && windowsApi.onRemoved) {
|
||||||
if (msg.type !== "AUTISTMASK_RPC") return;
|
windowsApi.onRemoved.addListener((windowId) => {
|
||||||
|
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||||
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
if (approval.windowId === windowId) {
|
||||||
sendResponse(response);
|
approval.resolve({ approved: false, remember: false });
|
||||||
|
delete pendingApprovals[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Return true to indicate async response
|
// Listen for messages from content scripts and popup
|
||||||
return true;
|
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
if (msg.type === "AUTISTMASK_RPC") {
|
||||||
|
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
||||||
|
sendResponse(response);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||||
|
const approval = pendingApprovals[msg.id];
|
||||||
|
if (approval) {
|
||||||
|
sendResponse({
|
||||||
|
hostname: approval.hostname,
|
||||||
|
origin: approval.origin,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendResponse(null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "AUTISTMASK_APPROVAL_RESPONSE") {
|
||||||
|
const approval = pendingApprovals[msg.id];
|
||||||
|
if (approval) {
|
||||||
|
approval.resolve({
|
||||||
|
approved: msg.approved,
|
||||||
|
remember: msg.remember,
|
||||||
|
});
|
||||||
|
delete pendingApprovals[msg.id];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
||||||
|
broadcastAccountsChanged();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
||||||
|
// Popup already saved state; nothing else needed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -520,6 +520,22 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<h3 class="font-bold mb-1">Allowed Sites</h3>
|
||||||
|
<p class="text-xs text-muted mb-2">
|
||||||
|
Sites that can connect to your wallet without asking.
|
||||||
|
</p>
|
||||||
|
<div id="settings-allowed-sites"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<h3 class="font-bold mb-1">Denied Sites</h3>
|
||||||
|
<p class="text-xs text-muted mb-2">
|
||||||
|
Sites that are blocked from connecting to your wallet.
|
||||||
|
</p>
|
||||||
|
<div id="settings-denied-sites"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============ TRANSACTION DETAIL ============ -->
|
<!-- ============ TRANSACTION DETAIL ============ -->
|
||||||
@@ -559,18 +575,28 @@
|
|||||||
|
|
||||||
<!-- ============ APPROVAL ============ -->
|
<!-- ============ APPROVAL ============ -->
|
||||||
<div id="view-approve" class="view hidden">
|
<div id="view-approve" class="view hidden">
|
||||||
<h2 class="font-bold mb-2">A website is requesting access</h2>
|
<h2 class="font-bold mb-2">Connection Request</h2>
|
||||||
<div class="mb-2">
|
<div class="mb-3">
|
||||||
|
<p class="mb-2">
|
||||||
|
<span id="approve-hostname" class="font-bold"></span>
|
||||||
|
wants to connect to your wallet.
|
||||||
|
</p>
|
||||||
<div class="text-xs text-muted mb-1">
|
<div class="text-xs text-muted mb-1">
|
||||||
From:
|
Address that will be shared:
|
||||||
<span id="approve-origin" class="font-bold"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-bold mb-1" id="approve-type"></div>
|
<div
|
||||||
|
id="approve-address"
|
||||||
|
class="text-xs flex items-center mb-2"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<input type="checkbox" id="approve-remember" checked />
|
||||||
|
Remember my choice for this site
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
|
||||||
id="approve-details"
|
|
||||||
class="border border-border p-2 text-xs overflow-auto mb-3 max-h-64"
|
|
||||||
></pre>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
id="btn-approve"
|
id="btn-approve"
|
||||||
|
|||||||
@@ -63,9 +63,29 @@ async function init() {
|
|||||||
|
|
||||||
await loadState();
|
await loadState();
|
||||||
|
|
||||||
|
// Auto-default active address
|
||||||
|
if (
|
||||||
|
state.activeAddress === null &&
|
||||||
|
state.wallets.length > 0 &&
|
||||||
|
state.wallets[0].addresses.length > 0
|
||||||
|
) {
|
||||||
|
state.activeAddress = state.wallets[0].addresses[0].address;
|
||||||
|
await saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for approval mode
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const approvalId = params.get("approval");
|
||||||
|
if (approvalId) {
|
||||||
|
approval.show(parseInt(approvalId, 10));
|
||||||
|
showView("approve");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$("btn-settings").addEventListener("click", () => {
|
$("btn-settings").addEventListener("click", () => {
|
||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
|
settings.renderSiteLists();
|
||||||
showView("settings");
|
showView("settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,52 @@
|
|||||||
const { $, showView } = require("./helpers");
|
const { $, addressDotHtml } = require("./helpers");
|
||||||
|
const { state, saveState } = require("../../shared/state");
|
||||||
|
|
||||||
function init(ctx) {
|
const runtime =
|
||||||
$("btn-approve").addEventListener("click", () => {
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
ctx.renderWalletList();
|
|
||||||
showView("main");
|
|
||||||
});
|
|
||||||
|
|
||||||
$("btn-reject").addEventListener("click", () => {
|
let approvalId = null;
|
||||||
ctx.renderWalletList();
|
|
||||||
showView("main");
|
function show(id) {
|
||||||
|
approvalId = id;
|
||||||
|
runtime.sendMessage({ type: "AUTISTMASK_GET_APPROVAL", id }, (details) => {
|
||||||
|
if (!details) {
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("approve-hostname").textContent = details.hostname;
|
||||||
|
const dot = addressDotHtml(state.activeAddress);
|
||||||
|
$("approve-address").innerHTML = dot + state.activeAddress;
|
||||||
|
$("approve-remember").checked = state.rememberSiteChoice;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init };
|
function init(ctx) {
|
||||||
|
$("approve-remember").addEventListener("change", async () => {
|
||||||
|
state.rememberSiteChoice = $("approve-remember").checked;
|
||||||
|
await saveState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btn-approve").addEventListener("click", () => {
|
||||||
|
const remember = $("approve-remember").checked;
|
||||||
|
runtime.sendMessage({
|
||||||
|
type: "AUTISTMASK_APPROVAL_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: true,
|
||||||
|
remember,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btn-reject").addEventListener("click", () => {
|
||||||
|
const remember = $("approve-remember").checked;
|
||||||
|
runtime.sendMessage({
|
||||||
|
type: "AUTISTMASK_APPROVAL_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: false,
|
||||||
|
remember,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, show };
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ function render(ctx) {
|
|||||||
|
|
||||||
wallet.addresses.forEach((addr, ai) => {
|
wallet.addresses.forEach((addr, ai) => {
|
||||||
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
||||||
html += `<div class="text-xs font-bold">Address ${wi + 1}.${ai + 1}</div>`;
|
const isActive = state.activeAddress === addr.address;
|
||||||
|
const activeHtml = isActive
|
||||||
|
? `<span class="font-bold text-xs">ACTIVE</span>`
|
||||||
|
: `<span class="btn-select-active text-xs underline decoration-dashed cursor-pointer" data-addr="${addr.address}">select</span>`;
|
||||||
|
html += `<div class="text-xs font-bold flex justify-between items-center"><span>Address ${wi + 1}.${ai + 1}</span>${activeHtml}</div>`;
|
||||||
const dot = addressDotHtml(addr.address);
|
const dot = addressDotHtml(addr.address);
|
||||||
if (addr.ensName) {
|
if (addr.ensName) {
|
||||||
html += `<div class="text-xs font-bold flex items-center">${dot}${addr.ensName}</div>`;
|
html += `<div class="text-xs font-bold flex items-center">${dot}${addr.ensName}</div>`;
|
||||||
@@ -67,6 +71,20 @@ function render(ctx) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll(".btn-select-active").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
state.activeAddress = btn.dataset.addr;
|
||||||
|
await saveState();
|
||||||
|
render(ctx);
|
||||||
|
const runtime =
|
||||||
|
typeof browser !== "undefined"
|
||||||
|
? browser.runtime
|
||||||
|
: chrome.runtime;
|
||||||
|
runtime.sendMessage({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
container.querySelectorAll(".btn-add-address").forEach((btn) => {
|
container.querySelectorAll(".btn-add-address").forEach((btn) => {
|
||||||
btn.addEventListener("click", async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -3,6 +3,44 @@ const { state, saveState } = require("../../shared/state");
|
|||||||
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
|
|
||||||
|
const runtime =
|
||||||
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
|
function renderSiteList(containerId, list, stateKey) {
|
||||||
|
const container = $(containerId);
|
||||||
|
if (list.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = "";
|
||||||
|
list.forEach((hostname, i) => {
|
||||||
|
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||||||
|
html += `<span>${hostname}</span>`;
|
||||||
|
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-key="${stateKey}" data-index="${i}">[x]</button>`;
|
||||||
|
html += `</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", async () => {
|
||||||
|
const key = btn.dataset.key;
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
state[key].splice(idx, 1);
|
||||||
|
await saveState();
|
||||||
|
runtime.sendMessage({ type: "AUTISTMASK_REMOVE_SITE" });
|
||||||
|
renderSiteList(containerId, state[key], key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSiteLists() {
|
||||||
|
renderSiteList(
|
||||||
|
"settings-allowed-sites",
|
||||||
|
state.allowedSites,
|
||||||
|
"allowedSites",
|
||||||
|
);
|
||||||
|
renderSiteList("settings-denied-sites", state.deniedSites, "deniedSites");
|
||||||
|
}
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
$("btn-save-rpc").addEventListener("click", async () => {
|
$("btn-save-rpc").addEventListener("click", async () => {
|
||||||
const url = $("settings-rpc").value.trim();
|
const url = $("settings-rpc").value.trim();
|
||||||
@@ -77,4 +115,4 @@ function init(ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init };
|
module.exports = { init, renderSiteLists };
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const DEFAULT_STATE = {
|
|||||||
rpcUrl: DEFAULT_RPC_URL,
|
rpcUrl: DEFAULT_RPC_URL,
|
||||||
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
||||||
lastBalanceRefresh: 0,
|
lastBalanceRefresh: 0,
|
||||||
|
activeAddress: null,
|
||||||
|
allowedSites: [],
|
||||||
|
deniedSites: [],
|
||||||
|
rememberSiteChoice: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -30,6 +34,10 @@ async function saveState() {
|
|||||||
rpcUrl: state.rpcUrl,
|
rpcUrl: state.rpcUrl,
|
||||||
blockscoutUrl: state.blockscoutUrl,
|
blockscoutUrl: state.blockscoutUrl,
|
||||||
lastBalanceRefresh: state.lastBalanceRefresh,
|
lastBalanceRefresh: state.lastBalanceRefresh,
|
||||||
|
activeAddress: state.activeAddress,
|
||||||
|
allowedSites: state.allowedSites,
|
||||||
|
deniedSites: state.deniedSites,
|
||||||
|
rememberSiteChoice: state.rememberSiteChoice,
|
||||||
};
|
};
|
||||||
await storageApi.set({ autistmask: persisted });
|
await storageApi.set({ autistmask: persisted });
|
||||||
}
|
}
|
||||||
@@ -45,6 +53,13 @@ async function loadState() {
|
|||||||
state.blockscoutUrl =
|
state.blockscoutUrl =
|
||||||
saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl;
|
saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl;
|
||||||
state.lastBalanceRefresh = saved.lastBalanceRefresh || 0;
|
state.lastBalanceRefresh = saved.lastBalanceRefresh || 0;
|
||||||
|
state.activeAddress = saved.activeAddress || null;
|
||||||
|
state.allowedSites = saved.allowedSites || [];
|
||||||
|
state.deniedSites = saved.deniedSites || [];
|
||||||
|
state.rememberSiteChoice =
|
||||||
|
saved.rememberSiteChoice !== undefined
|
||||||
|
? saved.rememberSiteChoice
|
||||||
|
: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user