Merge branch 'main' into fix/low-severity-security
All checks were successful
check / check (push) Successful in 22s
All checks were successful
check / check (push) Successful in 22s
This commit is contained in:
29
README.md
29
README.md
@@ -15,9 +15,10 @@ Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
|
|||||||
Everything you need, nothing you don't. We import as few libraries as possible,
|
Everything you need, nothing you don't. We import as few libraries as possible,
|
||||||
don't implement any crypto, and don't send user-specific data anywhere but a
|
don't implement any crypto, and don't send user-specific data anywhere but a
|
||||||
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
|
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
|
||||||
extension contacts precisely two external services: the configured RPC node for
|
extension contacts exactly three external services: the configured RPC node for
|
||||||
blockchain interactions, and a public CoinDesk API (no API key) to get realtime
|
blockchain interactions, a public CoinDesk API (no API key) for realtime price
|
||||||
price information.
|
information, and a Blockscout block-explorer API for transaction history and
|
||||||
|
token balances. All three endpoints are user-configurable.
|
||||||
|
|
||||||
In the extension is a hardcoded list of the top ERC20 contract addresses. You
|
In the extension is a hardcoded list of the top ERC20 contract addresses. You
|
||||||
can add any ERC20 contract by contract address if you wish, but the hardcoded
|
can add any ERC20 contract by contract address if you wish, but the hardcoded
|
||||||
@@ -534,7 +535,7 @@ transitions.
|
|||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
AutistMask is not a fully self-contained offline tool. It necessarily
|
AutistMask is not a fully self-contained offline tool. It necessarily
|
||||||
communicates with two external services to function as a wallet:
|
communicates with three external services to function as a wallet:
|
||||||
|
|
||||||
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
||||||
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`),
|
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`),
|
||||||
@@ -543,11 +544,24 @@ communicates with two external services to function as a wallet:
|
|||||||
receipts. The default endpoint is a public RPC (configurable by the user to
|
receipts. The default endpoint is a public RPC (configurable by the user to
|
||||||
any endpoint they prefer, including a local node). By default the extension
|
any endpoint they prefer, including a local node). By default the extension
|
||||||
talks to `https://ethereum-rpc.publicnode.com`.
|
talks to `https://ethereum-rpc.publicnode.com`.
|
||||||
|
- **Data sent**: Ethereum addresses, transaction data, contract call
|
||||||
|
parameters. The RPC endpoint can see all on-chain queries and submitted
|
||||||
|
transactions.
|
||||||
|
|
||||||
- **CoinDesk CADLI price API**: Used to fetch ETH/USD and token/USD prices for
|
- **CoinDesk CADLI price API**: Used to fetch ETH/USD and token/USD prices for
|
||||||
displaying fiat values. The price is cached for 5 minutes to avoid excessive
|
displaying fiat values. The price is cached for 5 minutes to avoid excessive
|
||||||
requests. No API key required. No user data is sent — only a list of token
|
requests. No API key required. No user data is sent — only a list of token
|
||||||
symbols. Note that CoinDesk will receive your client IP.
|
symbols. Note that CoinDesk will receive your client IP.
|
||||||
|
- **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No
|
||||||
|
addresses or user-specific data.
|
||||||
|
|
||||||
|
- **Blockscout block-explorer API**: Used to fetch transaction history (normal
|
||||||
|
transactions and ERC-20 token transfers), ERC-20 token balances, and token
|
||||||
|
holder counts (for spam filtering). The default endpoint is
|
||||||
|
`https://eth.blockscout.com/api/v2` (configurable by the user in Settings).
|
||||||
|
- **Data sent**: Ethereum addresses. Blockscout receives the user's
|
||||||
|
addresses to query their transaction history and token balances. No
|
||||||
|
private keys, passwords, or signing operations are sent.
|
||||||
|
|
||||||
What the extension does NOT do:
|
What the extension does NOT do:
|
||||||
|
|
||||||
@@ -557,9 +571,10 @@ What the extension does NOT do:
|
|||||||
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
||||||
- No backend servers operated by the developer
|
- No backend servers operated by the developer
|
||||||
|
|
||||||
The user's RPC endpoint and the CoinDesk price API are the only external
|
These three services (RPC endpoint, CoinDesk price API, and Blockscout API) are
|
||||||
services. Users who want maximum privacy can point the RPC at their own node
|
the only external services. All three endpoints are user-configurable. Users who
|
||||||
(price fetching can be disabled in a future version).
|
want maximum privacy can point the RPC and Blockscout URLs at their own
|
||||||
|
self-hosted instances (price fetching can be disabled in a future version).
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|||||||
9
RULES.md
9
RULES.md
@@ -1,3 +1,8 @@
|
|||||||
|
> **⚠️ THIS FILE MUST NEVER BE MODIFIED BY AGENTS.** RULES.md is maintained
|
||||||
|
> exclusively by the project owner. AI agents, bots, and automated tools must
|
||||||
|
> treat this file as read-only. If an audit finds a divergence between the code
|
||||||
|
> and this file, the code must be changed to match — never the other way around.
|
||||||
|
|
||||||
# AutistMask Rules Checklist
|
# AutistMask Rules Checklist
|
||||||
|
|
||||||
This file is derived from README.md and REPO_POLICIES.md for use as an audit
|
This file is derived from README.md and REPO_POLICIES.md for use as an audit
|
||||||
@@ -17,8 +22,8 @@ contradicts either, the originals govern.
|
|||||||
|
|
||||||
## External Communication
|
## External Communication
|
||||||
|
|
||||||
- [ ] Extension contacts exactly two external services: configured RPC endpoint
|
- [ ] Extension contacts exactly three external services: configured RPC
|
||||||
and CoinDesk price API
|
endpoint, CoinDesk price API, and Blockscout block-explorer API
|
||||||
- [ ] No analytics, telemetry, or tracking
|
- [ ] No analytics, telemetry, or tracking
|
||||||
- [ ] No user-specific data sent except to the configured RPC endpoint
|
- [ ] No user-specific data sent except to the configured RPC endpoint
|
||||||
- [ ] No Infura/Alchemy hard dependency
|
- [ ] No Infura/Alchemy hard dependency
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const connectedSites = {};
|
|||||||
|
|
||||||
// Pending approval requests: { id: { origin, hostname, resolve } }
|
// Pending approval requests: { id: { origin, hostname, resolve } }
|
||||||
const pendingApprovals = {};
|
const pendingApprovals = {};
|
||||||
let nextApprovalId = 1;
|
|
||||||
|
|
||||||
async function getState() {
|
async function getState() {
|
||||||
const result = await storageApi.get("autistmask");
|
const result = await storageApi.get("autistmask");
|
||||||
@@ -127,7 +126,7 @@ function openApprovalWindow(id) {
|
|||||||
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
|
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
|
||||||
function requestApproval(origin, hostname) {
|
function requestApproval(origin, hostname) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = nextApprovalId++;
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = { origin, hostname, resolve };
|
pendingApprovals[id] = { origin, hostname, resolve };
|
||||||
|
|
||||||
if (actionApi && typeof actionApi.openPopup === "function") {
|
if (actionApi && typeof actionApi.openPopup === "function") {
|
||||||
@@ -152,7 +151,7 @@ function requestApproval(origin, hostname) {
|
|||||||
// Uses the toolbar popup only — no fallback window.
|
// Uses the toolbar popup only — no fallback window.
|
||||||
function requestTxApproval(origin, hostname, txParams) {
|
function requestTxApproval(origin, hostname, txParams) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = nextApprovalId++;
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = {
|
pendingApprovals[id] = {
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
hostname,
|
||||||
@@ -184,7 +183,7 @@ function requestTxApproval(origin, hostname, txParams) {
|
|||||||
// popup URL is still set, so the user can click the toolbar icon to respond.
|
// popup URL is still set, so the user can click the toolbar icon to respond.
|
||||||
function requestSignApproval(origin, hostname, signParams) {
|
function requestSignApproval(origin, hostname, signParams) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = nextApprovalId++;
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = {
|
pendingApprovals[id] = {
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
hostname,
|
||||||
@@ -216,7 +215,7 @@ function requestSignApproval(origin, hostname, signParams) {
|
|||||||
// popups naturally close on focus loss and the user can reopen them.
|
// popups naturally close on focus loss and the user can reopen them.
|
||||||
runtime.onConnect.addListener((port) => {
|
runtime.onConnect.addListener((port) => {
|
||||||
if (port.name.startsWith("approval:")) {
|
if (port.name.startsWith("approval:")) {
|
||||||
const id = parseInt(port.name.split(":")[1], 10);
|
const id = port.name.split(":")[1];
|
||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
const approval = pendingApprovals[id];
|
const approval = pendingApprovals[id];
|
||||||
if (approval) {
|
if (approval) {
|
||||||
@@ -442,6 +441,13 @@ async function handleRpc(method, params, origin) {
|
|||||||
? { method, message: params[0], from: params[1] }
|
? { method, message: params[0], from: params[1] }
|
||||||
: { method, message: params[1], from: params[0] };
|
: { method, message: params[1], from: params[0] };
|
||||||
|
|
||||||
|
if (method === "eth_sign") {
|
||||||
|
signParams.dangerWarning =
|
||||||
|
"\u26a0\ufe0f DANGER: This site is requesting to sign a raw hash. " +
|
||||||
|
"This can be used to sign transactions that drain your funds. " +
|
||||||
|
"Only proceed if you fully understand what you are signing.";
|
||||||
|
}
|
||||||
|
|
||||||
const decision = await requestSignApproval(
|
const decision = await requestSignApproval(
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
hostname,
|
||||||
@@ -611,12 +617,39 @@ if (windowsApi && windowsApi.onRemoved) {
|
|||||||
// Listen for messages from content scripts and popup
|
// Listen for messages from content scripts and popup
|
||||||
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
if (msg.type === "AUTISTMASK_RPC") {
|
if (msg.type === "AUTISTMASK_RPC") {
|
||||||
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
// Derive origin from trusted sender info to prevent origin spoofing.
|
||||||
|
// Chrome MV3 provides sender.origin; Firefox MV2 fallback uses sender.tab.url.
|
||||||
|
let trustedOrigin = msg.origin; // fallback only if sender info unavailable
|
||||||
|
if (sender.origin) {
|
||||||
|
trustedOrigin = sender.origin;
|
||||||
|
} else if (sender.tab && sender.tab.url) {
|
||||||
|
try {
|
||||||
|
trustedOrigin = new URL(sender.tab.url).origin;
|
||||||
|
} catch {
|
||||||
|
// keep fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleRpc(msg.method, msg.params, trustedOrigin).then((response) => {
|
||||||
sendResponse(response);
|
sendResponse(response);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that popup-only messages originate from the extension itself.
|
||||||
|
const POPUP_ONLY_TYPES = [
|
||||||
|
"AUTISTMASK_GET_APPROVAL",
|
||||||
|
"AUTISTMASK_APPROVAL_RESPONSE",
|
||||||
|
"AUTISTMASK_TX_RESPONSE",
|
||||||
|
"AUTISTMASK_SIGN_RESPONSE",
|
||||||
|
];
|
||||||
|
if (POPUP_ONLY_TYPES.includes(msg.type)) {
|
||||||
|
const extUrl = runtime.getURL("");
|
||||||
|
if (!sender.url || !sender.url.startsWith(extUrl)) {
|
||||||
|
sendResponse({ error: "Unauthorized sender" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||||
const approval = pendingApprovals[msg.id];
|
const approval = pendingApprovals[msg.id];
|
||||||
if (approval) {
|
if (approval) {
|
||||||
@@ -681,7 +714,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
if (wallet) break;
|
if (wallet) break;
|
||||||
}
|
}
|
||||||
if (!wallet) throw new Error("Wallet not found");
|
if (!wallet) throw new Error("Wallet not found");
|
||||||
const decrypted = await decryptWithPassword(
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
||||||
|
let decrypted = await decryptWithPassword(
|
||||||
wallet.encryptedSecret,
|
wallet.encryptedSecret,
|
||||||
msg.password,
|
msg.password,
|
||||||
);
|
);
|
||||||
@@ -690,6 +724,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
addrIndex,
|
addrIndex,
|
||||||
decrypted,
|
decrypted,
|
||||||
);
|
);
|
||||||
|
// Best-effort: clear decrypted secret after use.
|
||||||
|
// Note: JS strings are immutable; this nulls the reference but
|
||||||
|
// the original string may persist in memory until GC.
|
||||||
|
decrypted = null;
|
||||||
const provider = getProvider(state.rpcUrl);
|
const provider = getProvider(state.rpcUrl);
|
||||||
const connected = signer.connect(provider);
|
const connected = signer.connect(provider);
|
||||||
const tx = await connected.sendTransaction(approval.txParams);
|
const tx = await connected.sendTransaction(approval.txParams);
|
||||||
@@ -735,7 +773,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
if (wallet) break;
|
if (wallet) break;
|
||||||
}
|
}
|
||||||
if (!wallet) throw new Error("Wallet not found");
|
if (!wallet) throw new Error("Wallet not found");
|
||||||
const decrypted = await decryptWithPassword(
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
||||||
|
let decrypted = await decryptWithPassword(
|
||||||
wallet.encryptedSecret,
|
wallet.encryptedSecret,
|
||||||
msg.password,
|
msg.password,
|
||||||
);
|
);
|
||||||
@@ -744,6 +783,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
addrIndex,
|
addrIndex,
|
||||||
decrypted,
|
decrypted,
|
||||||
);
|
);
|
||||||
|
// Best-effort: clear decrypted secret after use.
|
||||||
|
// Note: JS strings are immutable; this nulls the reference but
|
||||||
|
// the original string may persist in memory until GC.
|
||||||
|
decrypted = null;
|
||||||
|
|
||||||
const sp = approval.signParams;
|
const sp = approval.signParams;
|
||||||
let signature;
|
let signature;
|
||||||
|
|||||||
@@ -1015,6 +1015,17 @@
|
|||||||
wants you to sign a message.
|
wants you to sign a message.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="approve-sign-danger-warning"
|
||||||
|
class="hidden mb-3 p-2 text-xs font-bold"
|
||||||
|
style="
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 2px solid #dc2626;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Type</div>
|
<div class="text-xs text-muted mb-1">Type</div>
|
||||||
<div id="approve-sign-type" class="text-xs font-bold"></div>
|
<div id="approve-sign-type" class="text-xs font-bold"></div>
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ function init(ctx) {
|
|||||||
showFlash("Please choose a password.");
|
showFlash("Please choose a password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pw.length < 8) {
|
if (pw.length < 12) {
|
||||||
showFlash("Password must be at least 8 characters.");
|
showFlash("Password must be at least 12 characters.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pw !== pw2) {
|
if (pw !== pw2) {
|
||||||
|
|||||||
@@ -294,6 +294,18 @@ function showSignApproval(details) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display danger warning for eth_sign (raw hash signing)
|
||||||
|
const warningEl = $("approve-sign-danger-warning");
|
||||||
|
if (warningEl) {
|
||||||
|
if (sp.dangerWarning) {
|
||||||
|
warningEl.textContent = sp.dangerWarning;
|
||||||
|
warningEl.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
warningEl.textContent = "";
|
||||||
|
warningEl.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$("approve-sign-password").value = "";
|
$("approve-sign-password").value = "";
|
||||||
$("approve-sign-error").classList.add("hidden");
|
$("approve-sign-error").classList.add("hidden");
|
||||||
$("btn-approve-sign").disabled = false;
|
$("btn-approve-sign").disabled = false;
|
||||||
@@ -373,6 +385,7 @@ function init(ctx) {
|
|||||||
type: "AUTISTMASK_TX_RESPONSE",
|
type: "AUTISTMASK_TX_RESPONSE",
|
||||||
id: approvalId,
|
id: approvalId,
|
||||||
approved: true,
|
approved: true,
|
||||||
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
||||||
password: password,
|
password: password,
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
@@ -412,6 +425,7 @@ function init(ctx) {
|
|||||||
type: "AUTISTMASK_SIGN_RESPONSE",
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
||||||
id: approvalId,
|
id: approvalId,
|
||||||
approved: true,
|
approved: true,
|
||||||
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
||||||
password: password,
|
password: password,
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
|
|||||||
@@ -334,8 +334,13 @@ function init(ctx) {
|
|||||||
tx = await contract.transfer(pendingTx.to, amount);
|
tx = await contract.transfer(pendingTx.to, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Best-effort: clear decrypted secret after use.
|
||||||
|
// Note: JS strings are immutable; this nulls the reference but
|
||||||
|
// the original string may persist in memory until GC.
|
||||||
|
decryptedSecret = null;
|
||||||
txStatus.showWait(pendingTx, tx.hash);
|
txStatus.showWait(pendingTx, tx.hash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
decryptedSecret = null;
|
||||||
const hash = tx ? tx.hash : null;
|
const hash = tx ? tx.hash : null;
|
||||||
txStatus.showError(pendingTx, hash, e.shortMessage || e.message);
|
txStatus.showError(pendingTx, hash, e.shortMessage || e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function showFlash(msg, duration = 2000) {
|
|||||||
|
|
||||||
function balanceLine(symbol, amount, price, tokenId) {
|
function balanceLine(symbol, amount, price, tokenId) {
|
||||||
const qty = amount.toFixed(4);
|
const qty = amount.toFixed(4);
|
||||||
const usd = price ? formatUsd(amount * price) : "";
|
const usd = price ? formatUsd(amount * price) || " " : " ";
|
||||||
const tokenAttr = tokenId ? ` data-token="${tokenId}"` : "";
|
const tokenAttr = tokenId ? ` data-token="${tokenId}"` : "";
|
||||||
const clickClass = tokenId
|
const clickClass = tokenId
|
||||||
? " cursor-pointer hover:bg-hover balance-row"
|
? " cursor-pointer hover:bg-hover balance-row"
|
||||||
@@ -222,6 +222,41 @@ function formatAddressHtml(address, ensName, maxLen, title) {
|
|||||||
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
|
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isoDate(timestamp) {
|
||||||
|
const d = new Date(timestamp * 1000);
|
||||||
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
|
return (
|
||||||
|
d.getFullYear() +
|
||||||
|
"-" +
|
||||||
|
pad(d.getMonth() + 1) +
|
||||||
|
"-" +
|
||||||
|
pad(d.getDate()) +
|
||||||
|
" " +
|
||||||
|
pad(d.getHours()) +
|
||||||
|
":" +
|
||||||
|
pad(d.getMinutes()) +
|
||||||
|
":" +
|
||||||
|
pad(d.getSeconds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(timestamp) {
|
||||||
|
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
||||||
|
if (seconds < 60) return seconds + " seconds ago";
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60)
|
||||||
|
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
||||||
|
const months = Math.floor(days / 30);
|
||||||
|
if (months < 12)
|
||||||
|
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
||||||
|
const years = Math.floor(days / 365);
|
||||||
|
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
$,
|
$,
|
||||||
showError,
|
showError,
|
||||||
@@ -236,4 +271,6 @@ module.exports = {
|
|||||||
addressTitle,
|
addressTitle,
|
||||||
formatAddressHtml,
|
formatAddressHtml,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const {
|
|||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
@@ -87,41 +89,6 @@ function renderActiveAddress() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(timestamp) {
|
|
||||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
||||||
if (seconds < 60) return seconds + " seconds ago";
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60)
|
|
||||||
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
|
||||||
const months = Math.floor(days / 30);
|
|
||||||
if (months < 12)
|
|
||||||
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
|
||||||
const years = Math.floor(days / 365);
|
|
||||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isoDate(timestamp) {
|
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
return (
|
|
||||||
d.getFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getDate()) +
|
|
||||||
" " +
|
|
||||||
pad(d.getHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getSeconds())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let homeTxs = [];
|
let homeTxs = [];
|
||||||
|
|
||||||
function renderHomeTxList(ctx) {
|
function renderHomeTxList(ctx) {
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function init(ctx) {
|
|||||||
showFlash("Please choose a password.");
|
showFlash("Please choose a password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pw.length < 8) {
|
if (pw.length < 12) {
|
||||||
showFlash("Password must be at least 8 characters.");
|
showFlash("Password must be at least 12 characters.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pw !== pw2) {
|
if (pw !== pw2) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const {
|
|||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
@@ -21,41 +23,6 @@ const EXT_ICON =
|
|||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
|
|
||||||
function isoDate(timestamp) {
|
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
return (
|
|
||||||
d.getFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getDate()) +
|
|
||||||
" " +
|
|
||||||
pad(d.getHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getSeconds())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeAgo(timestamp) {
|
|
||||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
||||||
if (seconds < 60) return seconds + " seconds ago";
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60)
|
|
||||||
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
|
||||||
const months = Math.floor(days / 30);
|
|
||||||
if (months < 12)
|
|
||||||
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
|
||||||
const years = Math.floor(days / 365);
|
|
||||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyableHtml(text, extraClass) {
|
function copyableHtml(text, extraClass) {
|
||||||
const cls =
|
const cls =
|
||||||
"underline decoration-dashed cursor-pointer" +
|
"underline decoration-dashed cursor-pointer" +
|
||||||
|
|||||||
Reference in New Issue
Block a user