UNPKG

@coinmeca/wallet-provider

Version:

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

238 lines 9.25 kB
"use strict"; const TRACK_TRANSACTION = "TRACK_TRANSACTION"; const CLEAR_TRACKED_TRANSACTIONS = "CLEAR_TRACKED_TRANSACTIONS"; const POLL_INTERVAL_MS = 5000; const MAX_POLL_ATTEMPTS = 180; const TRACKABLE_TX_HASH = /^0x[0-9a-f]{64}$/i; const TRACKABLE_ADDRESS = /^0x[0-9a-f]{40}$/i; const TRACKABLE_CHAIN_ID = /^[0-9]+$/; const trackedTransactions = new Map(); const WORKER_PROTOCOL = "coinmeca.wallet.notification/v1"; const WORKER_KIND = "bundled"; function normalizeCategory(value) { return typeof value === "string" ? value.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ") : ""; } function notificationTitle(category) { const value = normalizeCategory(category); const normalized = value.toLowerCase(); if (!normalized) return "Transaction Update"; if (normalized.includes("approve")) return "Approval Update"; if (normalized.includes("deploy")) return "Contract Deployment"; if (normalized.includes("contract")) return "Contract Interaction"; return `${value.charAt(0).toUpperCase()}${value.slice(1)} Update`; } function notificationAction(category) { const value = normalizeCategory(category).toLowerCase(); if (!value) return "transaction"; if (value.includes("approve")) return "approval"; if (value.includes("deploy")) return "contract deployment"; if (value.includes("contract")) return "contract interaction"; return value; } self.addEventListener("install", (event) => { event.waitUntil(self.skipWaiting()); }); self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); self.addEventListener("message", (event) => { const { type, protocol, params } = event.data || {}; if (type === "COINMECA_WORKER_PING") { if (protocol && protocol !== WORKER_PROTOCOL) return; event.ports?.[0]?.postMessage({ type: "COINMECA_WORKER_PONG", protocol: WORKER_PROTOCOL, kind: WORKER_KIND }); return; } if (protocol && protocol !== WORKER_PROTOCOL) return; if (type === CLEAR_TRACKED_TRANSACTIONS) { event.waitUntil(clearTrackedTransactions()); return; } if (type !== TRACK_TRANSACTION) return; const request = trackingRequest(params); if (!request) return; event.waitUntil(trackTransaction(request)); }); function trackingRequest(params) { if (!params || typeof params !== "object") return; const txHash = typeof params.txHash === "string" ? params.txHash.trim() : ""; const rpcUrl = typeof params.rpcUrl === "string" ? params.rpcUrl.trim() : ""; if (!isTrackableTxHash(txHash) || !isTrackableRpcUrl(rpcUrl)) return; const accountAddress = isTrackableAddress(params.accountAddress) ? params.accountAddress.trim() : ""; const chainId = normalizeTrackableChainId(params.chainId); const category = normalizeCategory(params.category); const to = isTrackableAddress(params.to) ? params.to.trim() : ""; return { txHash, rpcUrl, title: typeof params.title === "string" && params.title.trim() ? params.title.trim() : notificationTitle(category), accountName: typeof params.accountName === "string" && params.accountName.trim() ? params.accountName.trim() : "", accountAddress, chainName: typeof params.chainName === "string" && params.chainName.trim() ? params.chainName.trim() : "", chainId, category, to, url: typeof params.url === "string" && params.url.trim() ? params.url.trim() : "/", }; } function trackingKey({ txHash, rpcUrl }) { return `${rpcUrl}:${txHash.toLowerCase()}`; } async function trackTransaction(request) { const key = trackingKey(request); if (trackedTransactions.has(key)) return; const controller = new AbortController(); trackedTransactions.set(key, controller); try { for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) { if (controller.signal.aborted) return; const receipt = await fetchReceipt(request.rpcUrl, request.txHash, controller.signal); if (receipt && typeof receipt.status !== "undefined" && receipt.status !== null) { await showTrackedNotification(request, receipt); return; } await delay(POLL_INTERVAL_MS, controller.signal); } } finally { trackedTransactions.delete(key); } } async function clearTrackedTransactions() { trackedTransactions.forEach((controller) => controller.abort()); trackedTransactions.clear(); } async function fetchReceipt(rpcUrl, txHash, signal) { try { const response = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json" }, signal, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getTransactionReceipt", params: [txHash], id: 1, }), }); const json = await response.json(); return json.result; } catch { if (signal?.aborted) return; return; } } async function showTrackedNotification(request, receipt) { const success = receipt.status === "0x1"; const shortHash = `${request.txHash.slice(0, 10)}...`; const subject = [request.chainName, request.accountName || shortAddress(request.accountAddress)] .filter((value) => typeof value === "string" && value.trim() !== "") .join(" / "); const prefix = subject ? `${subject}: ` : ""; const action = notificationAction(request.category); const target = isTrackableAddress(request.to) ? ` to ${shortAddress(request.to)}` : ""; await self.registration.showNotification(request.title, { body: `${prefix}${action}${target} ${success ? "confirmed" : "failed"} (${shortHash}).`, tag: `tx:${request.txHash.toLowerCase()}`, data: { url: trackingUrl(request.url, request.txHash, request.accountAddress, request.chainId, request.rpcUrl), }, }); } function shortAddress(address) { const value = typeof address === "string" ? address.trim() : ""; return value.startsWith("0x") && value.length > 12 ? `${value.slice(0, 6)}...${value.slice(-4)}` : value; } function isTrackableTxHash(value) { return typeof value === "string" && TRACKABLE_TX_HASH.test(value.trim()); } function isTrackableAddress(value) { return typeof value === "string" && TRACKABLE_ADDRESS.test(value.trim()); } function normalizeTrackableChainId(value) { if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value.toString(); if (typeof value === "string" && TRACKABLE_CHAIN_ID.test(value.trim())) return value.trim(); return ""; } function isTrackableRpcUrl(value) { try { const protocol = new URL(value.trim()).protocol.toLowerCase(); return ["http:", "https:"].includes(protocol); } catch { return false; } } function normalizeUrl(url) { try { return new URL(url || "/", self.location.origin).toString(); } catch { return new URL("/", self.location.origin).toString(); } } function trackingUrl(url, txHash, address, chainId, rpcUrl) { const target = new URL(normalizeUrl(url)); if (isTrackableTxHash(txHash)) target.searchParams.set("tx", txHash.trim()); if (isTrackableAddress(address)) target.searchParams.set("address", address.trim()); if (typeof chainId === "string" && TRACKABLE_CHAIN_ID.test(chainId.trim())) target.searchParams.set("chainId", chainId.trim()); if (typeof rpcUrl === "string" && isTrackableRpcUrl(rpcUrl)) target.searchParams.set("rpcUrl", rpcUrl.trim()); return target.toString(); } function delay(ms, signal) { if (signal?.aborted) return Promise.resolve(); return new Promise((resolve) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", handleAbort); resolve(); }, ms); const handleAbort = () => { clearTimeout(timer); signal?.removeEventListener("abort", handleAbort); resolve(); }; signal?.addEventListener("abort", handleAbort, { once: true }); }); } self.addEventListener("notificationclick", (event) => { event.notification.close(); const targetUrl = normalizeUrl(event.notification.data && event.notification.data.url); event.waitUntil(self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(async (windows) => { for (const client of windows) { const current = typeof client.url === "string" ? client.url : ""; if (!current || !current.startsWith(self.location.origin)) continue; if ("navigate" in client && current !== targetUrl) await client.navigate(targetUrl); if ("focus" in client) return client.focus(); return client; } return self.clients.openWindow(targetUrl); })); }); //# sourceMappingURL=notification-sw.js.map