diff --git a/README.md b/README.md index d9712f3..899e224 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,15 @@ Die App ist eine einzige HTML-Datei, die von überall gehostet werden kann. - [x] Offline-fähig via Service Worker - [x] CoinGecko-Fallback mit Auto-Retry -### v2 — View-Key Zahlungsbestätigung (Browser-basiert) +### v2 — View-Key Zahlungsbestätigung (Browser-basiert) ✅ -- [ ] View-Only-Key eingeben (privater Spend-Key bleibt lokal) -- [ ] Browser pollt Remote Node via Monero RPC (kein eigener Node nötig) -- [ ] Live-Anzeige: "Warte auf Zahlung..." → "✅ Zahlung eingegangen (X Bestätigungen)" -- [ ] Warnhinweis bei Unterzahlung -- [ ] Subaddress-Unterstützung (für mehrere parallele Rechnungen) +- [x] Private View-Key eingeben (validiert gegen Adresse, verlässt nie den Browser) +- [x] Browser pollt Remote Node via PHP-Proxy mit Failover (4 Nodes) +- [x] Live-Anzeige: "Warte auf Zahlung..." → "Zahlung eingegangen (X/10 Bestätigungen)" +- [x] Fortschrittsbalken für Bestätigungen +- [x] Unterzahlungs-Erkennung +- [x] Standard- und Subaddress-Unterstützung +- [x] Leichtgewichtige Krypto (30KB noble-curves Bundle, kein 5MB WASM) ### v3 — Professionelle Features diff --git a/api/node.php b/api/node.php new file mode 100644 index 0000000..8ffa6c5 --- /dev/null +++ b/api/node.php @@ -0,0 +1,133 @@ + 'Method not allowed']); + exit; +} + +// Nodes in priority order +$NODES = [ + 'http://node.xmr.rocks:18089', + 'http://node.community.rino.io:18081', + 'http://node.sethforprivacy.com:18089', + 'http://xmr-node.cakewallet.com:18081', +]; + +// Allowed RPC methods (whitelist) +$ALLOWED_JSON_RPC = ['get_info', 'get_block', 'get_block_header_by_height']; +$ALLOWED_HTTP = ['get_transaction_pool', 'gettransactions', 'get_transaction_pool_hashes.bin']; + +// Rate limiting (simple file-based, 60 requests/minute per IP) +$RATE_DIR = __DIR__ . '/../data/rate/'; +if (!is_dir($RATE_DIR)) @mkdir($RATE_DIR, 0755, true); + +$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; +$rateFile = $RATE_DIR . md5($ip) . '.json'; +$now = time(); +$rateData = []; + +if (file_exists($rateFile)) { + $rateData = json_decode(file_get_contents($rateFile), true) ?: []; + // Clean old entries + $rateData = array_filter($rateData, fn($t) => $t > $now - 60); +} + +if (count($rateData) >= 1000) { + http_response_code(429); + echo json_encode(['error' => 'Rate limit exceeded']); + exit; +} + +$rateData[] = $now; +file_put_contents($rateFile, json_encode($rateData)); + +// Parse request +$input = json_decode(file_get_contents('php://input'), true); +if (!$input || !isset($input['method'])) { + http_response_code(400); + echo json_encode(['error' => 'Missing method']); + exit; +} + +$method = $input['method']; +$params = $input['params'] ?? []; + +// Determine endpoint type +$isJsonRpc = in_array($method, $ALLOWED_JSON_RPC); +$isHttp = in_array($method, $ALLOWED_HTTP); + +if (!$isJsonRpc && !$isHttp) { + http_response_code(403); + echo json_encode(['error' => 'Method not allowed: ' . $method]); + exit; +} + +// Cache last working node +$cacheFile = __DIR__ . '/../data/node_cache.json'; +$cachedNode = null; +if (file_exists($cacheFile)) { + $cache = json_decode(file_get_contents($cacheFile), true); + if ($cache && ($cache['time'] ?? 0) > $now - 300) { + $cachedNode = $cache['node']; + } +} + +// Order nodes: cached first +$orderedNodes = $NODES; +if ($cachedNode && in_array($cachedNode, $NODES)) { + $orderedNodes = array_merge([$cachedNode], array_filter($NODES, fn($n) => $n !== $cachedNode)); +} + +// Try nodes +$lastError = ''; +foreach ($orderedNodes as $node) { + if ($isJsonRpc) { + $url = $node . '/json_rpc'; + $body = json_encode([ + 'jsonrpc' => '2.0', + 'id' => '0', + 'method' => $method, + 'params' => (object)$params + ]); + } else { + $url = $node . '/' . $method; + $body = json_encode((object)$params); + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($response !== false && $httpCode >= 200 && $httpCode < 300) { + // Cache this working node + file_put_contents($cacheFile, json_encode(['node' => $node, 'time' => $now])); + echo $response; + exit; + } + + $lastError = $curlError ?: "HTTP $httpCode"; +} + +// All nodes failed +http_response_code(502); +echo json_encode(['error' => 'All nodes unreachable', 'detail' => $lastError]); diff --git a/app.js b/app.js index 095918f..1bff9b8 100644 --- a/app.js +++ b/app.js @@ -21,7 +21,9 @@ const amountInput = $('#amount'); const currencySelect = $('#currency'); const descInput = $('#desc'); - const timerInput = $('#timer'); + const timerCustom = $('#timerCustom'); + const deadlineBadges = $('#deadlineBadges'); + let selectedDays = 0; const generateBtn = $('#generate'); const resultSection = $('#result'); const qrContainer = $('#qr'); @@ -36,6 +38,21 @@ const newRequestBtn = $('#newRequest'); const homeLink = $('#homeLink'); + // Monitor DOM + const monitorSection = $('#monitorSection'); + const monitorToggle = $('#monitorToggle'); + const monitorPanel = $('#monitorPanel'); + const viewKeyInput = $('#viewKey'); + const startMonitorBtn = $('#startMonitor'); + const stopMonitorBtn = $('#stopMonitor'); + const monitorStatus = $('#monitorStatus'); + const statusIndicator = $('#statusIndicator'); + const statusText = $('#statusText'); + const confirmationsBar = $('#confirmationsBar'); + const confirmationsFill = $('#confirmationsFill'); + const confirmationsText = $('#confirmationsText'); + let cryptoLoaded = false; + // --- Init --- fetchRates(); loadFromHash() || loadSaved(); @@ -52,6 +69,33 @@ newRequestBtn.addEventListener('click', resetForm); homeLink.addEventListener('click', function (e) { e.preventDefault(); resetForm(); }); + // Deadline badge events + deadlineBadges.querySelectorAll('.badge').forEach(function (btn) { + btn.addEventListener('click', function () { + const days = parseInt(btn.getAttribute('data-days')); + if (btn.classList.contains('active')) { + btn.classList.remove('active'); + selectedDays = 0; + timerCustom.value = ''; + } else { + deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); + btn.classList.add('active'); + selectedDays = days; + timerCustom.value = ''; + } + }); + }); + timerCustom.addEventListener('input', function () { + deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); + selectedDays = parseInt(timerCustom.value) || 0; + }); + + // Monitor events + monitorToggle.addEventListener('click', toggleMonitor); + viewKeyInput.addEventListener('input', validateViewKey); + startMonitorBtn.addEventListener('click', startMonitoring); + stopMonitorBtn.addEventListener('click', stopMonitoring); + // --- Functions --- function resetForm() { @@ -59,7 +103,9 @@ amountInput.value = ''; currencySelect.value = 'EUR'; descInput.value = ''; - timerInput.value = ''; + selectedDays = 0; + timerCustom.value = ''; + deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); fiatHint.textContent = ''; fiatHint.classList.remove('error'); addrInput.classList.remove('valid', 'invalid'); @@ -69,6 +115,12 @@ qrContainer.innerHTML = ''; uriBox.textContent = ''; shareLinkInput.value = ''; + // Reset monitor + stopMonitoring(); + monitorPanel.classList.remove('open'); + viewKeyInput.value = ''; + viewKeyInput.classList.remove('valid', 'invalid'); + startMonitorBtn.disabled = true; history.replaceState(null, '', location.pathname); window.scrollTo({ top: 0, behavior: 'smooth' }); addrInput.focus(); @@ -184,7 +236,7 @@ const xmrAmount = getXmrAmount(); const desc = descInput.value.trim(); - const timer = parseInt(timerInput.value) || 0; + const timer = selectedDays; const uri = buildUri(addr, xmrAmount, desc); // Show result @@ -205,8 +257,8 @@ text: uri, width: 256, height: 256, - colorDark: '#ffffff', - colorLight: '#1a1a1a', + colorDark: '#000000', + colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.M }); const hint = document.createElement('div'); @@ -245,7 +297,16 @@ if (desc) descInput.value = desc; const timer = params.get('t'); - if (timer && parseInt(timer) > 0) timerInput.value = timer; + if (timer && parseInt(timer) > 0) { + selectedDays = parseInt(timer); + // Activate matching badge or set custom + const badge = deadlineBadges.querySelector('.badge[data-days="' + selectedDays + '"]'); + if (badge) { + badge.classList.add('active'); + } else { + timerCustom.value = selectedDays; + } + } // Auto-generate setTimeout(generate, 100); @@ -257,10 +318,9 @@ countdownEl.textContent = ''; countdownEl.className = 'countdown'; - const minutes = parseInt(timerInput.value); - if (!minutes || minutes <= 0) return; + if (!selectedDays || selectedDays <= 0) return; - const end = Date.now() + minutes * 60000; + const end = Date.now() + selectedDays * 86400000; countdownEl.classList.add('active'); function tick() { @@ -271,9 +331,17 @@ countdownEl.className = 'countdown expired'; return; } - const m = Math.floor(remaining / 60000); + const d = Math.floor(remaining / 86400000); + const h = Math.floor((remaining % 86400000) / 3600000); + const m = Math.floor((remaining % 3600000) / 60000); const s = Math.floor((remaining % 60000) / 1000); - countdownEl.textContent = I18n.t('countdown_remaining') + pad(m) + ':' + pad(s); + if (d > 0) { + countdownEl.textContent = I18n.t('countdown_remaining_days') + .replace('{d}', d).replace('{h}', pad(h)).replace('{m}', pad(m)).replace('{s}', pad(s)); + } else { + countdownEl.textContent = I18n.t('countdown_remaining_hours') + .replace('{h}', pad(h)).replace('{m}', pad(m)).replace('{s}', pad(s)); + } } tick(); @@ -344,4 +412,135 @@ navigator.serviceWorker.register('sw.js').catch(function () {}); } } + + // --- Monitor Functions (v2) --- + + function toggleMonitor() { + const panel = monitorPanel; + const isOpen = panel.classList.contains('open'); + if (isOpen) { + panel.classList.remove('open'); + return; + } + // Lazy-load crypto bundle + if (!cryptoLoaded && !window.XmrCrypto) { + loadCryptoBundle().then(function () { + cryptoLoaded = true; + panel.classList.add('open'); + viewKeyInput.focus(); + }); + return; + } + panel.classList.add('open'); + viewKeyInput.focus(); + } + + function loadCryptoBundle() { + return new Promise(function (resolve, reject) { + if (window.XmrCrypto) { resolve(); return; } + statusText.textContent = I18n.t('monitor_loading'); + monitorStatus.classList.add('active'); + const script = document.createElement('script'); + script.src = 'lib/xmr-crypto.bundle.js'; + script.onload = function () { + monitorStatus.classList.remove('active'); + resolve(); + }; + script.onerror = function () { + monitorStatus.classList.remove('active'); + reject(new Error('Failed to load crypto module')); + }; + document.head.appendChild(script); + }); + } + + function validateViewKey() { + const key = viewKeyInput.value.trim(); + viewKeyInput.classList.remove('valid', 'invalid'); + if (key.length === 0) { + startMonitorBtn.disabled = true; + return; + } + if (PaymentMonitor.isValidViewKey(key)) { + viewKeyInput.classList.add('valid'); + startMonitorBtn.disabled = false; + } else if (key.length >= 10) { + viewKeyInput.classList.add('invalid'); + startMonitorBtn.disabled = true; + } + } + + function startMonitoring() { + const viewKey = viewKeyInput.value.trim(); + if (!PaymentMonitor.isValidViewKey(viewKey)) return; + + const addr = addrInput.value.trim(); + const xmrAmount = getXmrAmount() || 0; + + // Hide input, show status + startMonitorBtn.style.display = 'none'; + viewKeyInput.closest('.field').style.display = 'none'; + monitorStatus.classList.add('active'); + stopMonitorBtn.classList.add('active'); + + PaymentMonitor.start(addr, viewKey, xmrAmount, function (newState, data) { + updateMonitorUI(newState, data); + }); + } + + function stopMonitoring() { + PaymentMonitor.stop(); + monitorStatus.classList.remove('active'); + confirmationsBar.classList.remove('active'); + stopMonitorBtn.classList.remove('active'); + startMonitorBtn.style.display = ''; + viewKeyInput.closest('.field').style.display = ''; + statusIndicator.className = 'status-indicator'; + statusText.textContent = ''; + } + + function updateMonitorUI(monitorState, data) { + const S = PaymentMonitor.STATE; + statusIndicator.className = 'status-indicator ' + monitorState; + + switch (monitorState) { + case S.CONNECTING: + statusText.textContent = I18n.t('monitor_connecting'); + confirmationsBar.classList.remove('active'); + break; + case S.SCANNING: + statusText.textContent = I18n.t('monitor_scanning'); + break; + case S.WAITING: + statusText.textContent = I18n.t('monitor_waiting'); + break; + case S.MEMPOOL: + statusText.textContent = I18n.t('monitor_mempool'); + showConfirmations(data.confirmations); + break; + case S.CONFIRMED: + statusText.textContent = I18n.t('monitor_confirmed'); + showConfirmations(data.confirmations); + stopMonitorBtn.classList.remove('active'); + break; + case S.UNDERPAID: + statusText.textContent = I18n.t('monitor_underpaid'); + var detail = I18n.t('monitor_underpaid_detail') + .replace('{expected}', data.expected.toFixed(6)) + .replace('{received}', data.received.toFixed(6)); + statusText.textContent += '\n' + detail; + showConfirmations(data.confirmations); + break; + case S.ERROR: + statusText.textContent = data.message || I18n.t('monitor_node_error'); + break; + } + } + + function showConfirmations(n) { + confirmationsBar.classList.add('active'); + var pct = Math.min(100, (n / 10) * 100); + confirmationsFill.style.width = pct + '%'; + confirmationsText.textContent = I18n.t('monitor_confirmations').replace('{n}', n); + } })(); diff --git a/i18n.js b/i18n.js index 5884db5..c154d55 100644 --- a/i18n.js +++ b/i18n.js @@ -14,8 +14,9 @@ var I18n = (function () { label_amount: 'Betrag', label_desc: 'Beschreibung (optional)', placeholder_desc: 'z.B. Rechnung #42, Freelance-Arbeit...', - label_timer: 'Zahlungsfrist in Minuten (optional)', - placeholder_timer: 'z.B. 30', + label_timer: 'Zahlungsfrist (optional)', + days: 'Tage', + placeholder_timer_custom: 'Tage', btn_generate: 'Zahlungsanforderung erstellen', btn_open_wallet: 'In Wallet öffnen', btn_copy_addr: 'Adresse kopieren', @@ -25,8 +26,25 @@ var I18n = (function () { btn_new_request: 'Neue Zahlungsanforderung', toast_copied: 'Kopiert!', countdown_expired: 'Zahlungsfrist abgelaufen', - countdown_remaining: 'Zahlungsfrist: ', - rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich' + countdown_remaining_days: 'Zahlungsfrist: {d} Tage, {h}:{m}:{s}', + countdown_remaining_hours: 'Zahlungsfrist: {h}:{m}:{s}', + rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich', + btn_monitor: 'Zahlung überwachen', + label_view_key: 'Privater View-Key', + placeholder_view_key: '64 Hex-Zeichen...', + hint_view_key: 'Der View-Key verlässt nie deinen Browser', + btn_start_monitor: 'Überwachung starten', + btn_stop_monitor: 'Überwachung beenden', + monitor_connecting: 'Verbinde mit Node...', + monitor_scanning: 'Scanne Mempool...', + monitor_waiting: 'Warte auf Zahlung...', + monitor_mempool: 'Zahlung erkannt (unbestätigt)', + monitor_confirmed: 'Zahlung bestätigt', + monitor_confirmations: '{n}/10 Bestätigungen', + monitor_underpaid: 'Unterzahlung erkannt', + monitor_underpaid_detail: 'Erwartet: {expected} XMR — Erhalten: {received} XMR', + monitor_node_error: 'Node nicht erreichbar — versuche nächsten...', + monitor_loading: 'Lade Kryptografie-Modul...' }, en: { subtitle: 'Monero payment request in seconds', @@ -35,8 +53,9 @@ var I18n = (function () { label_amount: 'Amount', label_desc: 'Description (optional)', placeholder_desc: 'e.g. Invoice #42, freelance work...', - label_timer: 'Payment deadline in minutes (optional)', - placeholder_timer: 'e.g. 30', + label_timer: 'Payment deadline (optional)', + days: 'days', + placeholder_timer_custom: 'Days', btn_generate: 'Create payment request', btn_open_wallet: 'Open in wallet', btn_copy_addr: 'Copy address', @@ -46,8 +65,25 @@ var I18n = (function () { btn_new_request: 'New payment request', toast_copied: 'Copied!', countdown_expired: 'Payment deadline expired', - countdown_remaining: 'Payment deadline: ', - rates_offline: 'Rates unavailable — XMR amount only' + countdown_remaining_days: 'Deadline: {d} days, {h}:{m}:{s}', + countdown_remaining_hours: 'Deadline: {h}:{m}:{s}', + rates_offline: 'Rates unavailable — XMR amount only', + btn_monitor: 'Monitor payment', + label_view_key: 'Private view key', + placeholder_view_key: '64 hex characters...', + hint_view_key: 'Your view key never leaves your browser', + btn_start_monitor: 'Start monitoring', + btn_stop_monitor: 'Stop monitoring', + monitor_connecting: 'Connecting to node...', + monitor_scanning: 'Scanning mempool...', + monitor_waiting: 'Waiting for payment...', + monitor_mempool: 'Payment detected (unconfirmed)', + monitor_confirmed: 'Payment confirmed', + monitor_confirmations: '{n}/10 confirmations', + monitor_underpaid: 'Underpayment detected', + monitor_underpaid_detail: 'Expected: {expected} XMR — Received: {received} XMR', + monitor_node_error: 'Node unreachable — trying next...', + monitor_loading: 'Loading crypto module...' } }; diff --git a/index.html b/index.html index 651d1d4..420a0f2 100644 --- a/index.html +++ b/index.html @@ -44,8 +44,13 @@
- - + +
+ + + + +
@@ -68,6 +73,34 @@ + +
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ @@ -93,5 +126,6 @@ + diff --git a/lib/xmr-crypto.bundle.js b/lib/xmr-crypto.bundle.js new file mode 100644 index 0000000..bb29bd5 --- /dev/null +++ b/lib/xmr-crypto.bundle.js @@ -0,0 +1,13 @@ +(()=>{function ut(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"}function j(e,t=""){if(!Number.isSafeInteger(e)||e<0){let n=t&&`"${t}" `;throw new Error(`${n}expected integer >= 0, got ${e}`)}}function O(e,t,n=""){let r=ut(e),o=e?.length,s=t!==void 0;if(!r||s&&o!==t){let i=n&&`"${n}" `,f=s?` of length ${t}`:"",c=r?`length=${o}`:`type=${typeof e}`;throw new Error(i+"expected Uint8Array"+f+", got "+c)}return e}function nt(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function lt(e,t){O(e,void 0,"digestInto() output");let n=t.outputLen;if(e.length='+n)}function te(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function F(...e){for(let t=0;t>>8&65280|e>>>24&255}function sn(e){for(let t=0;te:sn,ee=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",cn=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,"0"));function k(e){if(O(e),ee)return e.toHex();let t="";for(let n=0;n=K._0&&e<=K._9)return e-K._0;if(e>=K.A&&e<=K.F)return e-(K.A-10);if(e>=K.a&&e<=K.f)return e-(K.a-10)}function M(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);if(ee)return Uint8Array.fromHex(e);let t=e.length,n=t/2;if(t%2)throw new Error("hex string expected, got unpadded hex of length "+t);let r=new Uint8Array(n);for(let o=0,s=0;oe(s).update(o).digest(),r=e(void 0);return n.outputLen=r.outputLen,n.blockLen=r.blockLen,n.create=o=>e(o),Object.assign(n,t),Object.freeze(n)}function Lt(e=32){let t=typeof globalThis=="object"?globalThis.crypto:null;if(typeof t?.getRandomValues!="function")throw new Error("crypto.getRandomValues must be defined");return t.getRandomValues(new Uint8Array(e))}var Rt=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])});var xt=class{blockLen;outputLen;padOffset;isLE;buffer;view;finished=!1;length=0;pos=0;destroyed=!1;constructor(t,n,r,o){this.blockLen=t,this.outputLen=n,this.padOffset=r,this.isLE=o,this.buffer=new Uint8Array(t),this.view=dt(this.buffer)}update(t){nt(this),O(t);let{view:n,buffer:r,blockLen:o}=this,s=t.length;for(let i=0;io-i&&(this.process(r,0),i=0);for(let x=i;xd.length)throw new Error("_sha2: outputLen bigger than state");for(let x=0;x>ne&bt)}:{h:Number(e>>ne&bt)|0,l:Number(e&bt)|0}}function pt(e,t=!1){let n=e.length,r=new Uint32Array(n),o=new Uint32Array(n);for(let s=0;se>>>n,Ht=(e,t,n)=>e<<32-n|t>>>n,Q=(e,t,n)=>e>>>n|t<<32-n,P=(e,t,n)=>e<<32-n|t>>>n,st=(e,t,n)=>e<<64-n|t>>>n-32,it=(e,t,n)=>e>>>n-32|t<<64-n;var re=(e,t,n)=>e<>>32-n,oe=(e,t,n)=>t<>>32-n,se=(e,t,n)=>t<>>64-n,ie=(e,t,n)=>e<>>64-n;function X(e,t,n,r){let o=(t>>>0)+(r>>>0);return{h:e+n+(o/2**32|0)|0,l:o|0}}var ce=(e,t,n)=>(e>>>0)+(t>>>0)+(n>>>0),fe=(e,t,n,r)=>t+n+r+(e/2**32|0)|0,ae=(e,t,n,r)=>(e>>>0)+(t>>>0)+(n>>>0)+(r>>>0),ue=(e,t,n,r,o)=>t+n+r+o+(e/2**32|0)|0,le=(e,t,n,r,o)=>(e>>>0)+(t>>>0)+(n>>>0)+(r>>>0)+(o>>>0),de=(e,t,n,r,o,s)=>t+n+r+o+s+(e/2**32|0)|0;var he=pt(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(e=>BigInt(e))),un=he[0],ln=he[1],z=new Uint32Array(80),$=new Uint32Array(80),Dt=class extends xt{constructor(t){super(128,t,16,!1)}get(){let{Ah:t,Al:n,Bh:r,Bl:o,Ch:s,Cl:i,Dh:f,Dl:c,Eh:a,El:d,Fh:x,Fl:m,Gh:w,Gl:p,Hh:S,Hl:B}=this;return[t,n,r,o,s,i,f,c,a,d,x,m,w,p,S,B]}set(t,n,r,o,s,i,f,c,a,d,x,m,w,p,S,B){this.Ah=t|0,this.Al=n|0,this.Bh=r|0,this.Bl=o|0,this.Ch=s|0,this.Cl=i|0,this.Dh=f|0,this.Dl=c|0,this.Eh=a|0,this.El=d|0,this.Fh=x|0,this.Fl=m|0,this.Gh=w|0,this.Gl=p|0,this.Hh=S|0,this.Hl=B|0}process(t,n){for(let u=0;u<16;u++,n+=4)z[u]=t.getUint32(n),$[u]=t.getUint32(n+=4);for(let u=16;u<80;u++){let E=z[u-15]|0,A=$[u-15]|0,R=Q(E,A,1)^Q(E,A,8)^Tt(E,A,7),T=P(E,A,1)^P(E,A,8)^Ht(E,A,7),l=z[u-2]|0,b=$[u-2]|0,y=Q(l,b,19)^st(l,b,61)^Tt(l,b,6),_=P(l,b,19)^it(l,b,61)^Ht(l,b,6),I=ae(T,_,$[u-7],$[u-16]),v=ue(I,R,y,z[u-7],z[u-16]);z[u]=v|0,$[u]=I|0}let{Ah:r,Al:o,Bh:s,Bl:i,Ch:f,Cl:c,Dh:a,Dl:d,Eh:x,El:m,Fh:w,Fl:p,Gh:S,Gl:B,Hh:h,Hl:g}=this;for(let u=0;u<80;u++){let E=Q(x,m,14)^Q(x,m,18)^st(x,m,41),A=P(x,m,14)^P(x,m,18)^it(x,m,41),R=x&w^~x&S,T=m&p^~m&B,l=le(g,A,T,ln[u],$[u]),b=de(l,h,E,R,un[u],z[u]),y=l|0,_=Q(r,o,28)^st(r,o,34)^st(r,o,39),I=P(r,o,28)^it(r,o,34)^it(r,o,39),v=r&s^r&f^s&f,L=o&i^o&c^i&c;h=S|0,g=B|0,S=w|0,B=p|0,w=x|0,p=m|0,{h:x,l:m}=X(a|0,d|0,b|0,y|0),a=f|0,d=c|0,f=s|0,c=i|0,s=r|0,i=o|0;let N=ce(y,I,L);r=fe(N,b,_,v),o=N|0}({h:r,l:o}=X(this.Ah|0,this.Al|0,r|0,o|0)),{h:s,l:i}=X(this.Bh|0,this.Bl|0,s|0,i|0),{h:f,l:c}=X(this.Ch|0,this.Cl|0,f|0,c|0),{h:a,l:d}=X(this.Dh|0,this.Dl|0,a|0,d|0),{h:x,l:m}=X(this.Eh|0,this.El|0,x|0,m|0),{h:w,l:p}=X(this.Fh|0,this.Fl|0,w|0,p|0),{h:S,l:B}=X(this.Gh|0,this.Gl|0,S|0,B|0),{h,l:g}=X(this.Hh|0,this.Hl|0,h|0,g|0),this.set(r,o,s,i,f,c,a,d,x,m,w,p,S,B,h,g)}roundClean(){F(z,$)}destroy(){F(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}},Nt=class extends Dt{Ah=q[0]|0;Al=q[1]|0;Bh=q[2]|0;Bl=q[3]|0;Ch=q[4]|0;Cl=q[5]|0;Dh=q[6]|0;Dl=q[7]|0;Eh=q[8]|0;El=q[9]|0;Fh=q[10]|0;Fl=q[11]|0;Gh=q[12]|0;Gl=q[13]|0;Hh=q[14]|0;Hl=q[15]|0;constructor(){super(64)}};var xe=ht(()=>new Nt,Rt(3));var pe=BigInt(0),be=BigInt(1);function mt(e,t=""){if(typeof e!="boolean"){let n=t&&`"${t}" `;throw new Error(n+"expected boolean, got type="+typeof e)}return e}function dn(e){if(typeof e=="bigint"){if(!gt(e))throw new Error("positive bigint expected, got "+e)}else j(e);return e}function ge(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);return e===""?pe:BigInt("0x"+e)}function me(e){return ge(k(e))}function rt(e){return ge(k(yt(O(e)).reverse()))}function qt(e,t){j(t),e=dn(e);let n=M(e.toString(16).padStart(t*2,"0"));if(n.length!==t)throw new Error("number too large");return n}function ye(e,t){return qt(e,t).reverse()}function yt(e){return Uint8Array.from(e)}var gt=e=>typeof e=="bigint"&&pe<=e;function hn(e,t,n){return gt(e)&>(t)&>(n)&&t<=e&&e(be<Object.entries(s).forEach(([f,c])=>r(f,c,i));o(t,!1),o(n,!0)}function kt(e){let t=new WeakMap;return(n,...r)=>{let o=t.get(n);if(o!==void 0)return o;let s=e(n,...r);return t.set(n,s),s}}var C=BigInt(0),U=BigInt(1),J=BigInt(2),_e=BigInt(3),Ae=BigInt(4),Se=BigInt(5),xn=BigInt(7),Ie=BigInt(8),bn=BigInt(9),ve=BigInt(16);function H(e,t){let n=e%t;return n>=C?n:t+n}function V(e,t,n){let r=e;for(;t-- >C;)r*=r,r%=n;return r}function Be(e,t){if(e===C)throw new Error("invert: expected non-zero number");if(t<=C)throw new Error("invert: expected positive modulus, got "+t);let n=H(e,t),r=t,o=C,s=U,i=U,f=C;for(;n!==C;){let a=r/n,d=r%n,x=o-i*a,m=s-f*a;r=n,n=d,o=i,s=f,i=x,f=m}if(r!==U)throw new Error("invert: does not exist");return H(o,t)}function Zt(e,t,n){if(!e.eql(e.sqr(t),n))throw new Error("Cannot find square root")}function Oe(e,t){let n=(e.ORDER+U)/Ae,r=e.pow(t,n);return Zt(e,r,t),r}function pn(e,t){let n=(e.ORDER-Se)/Ie,r=e.mul(t,J),o=e.pow(r,n),s=e.mul(t,o),i=e.mul(e.mul(s,J),o),f=e.mul(s,e.sub(i,e.ONE));return Zt(e,f,t),f}function gn(e){let t=Bt(e),n=Le(e),r=n(t,t.neg(t.ONE)),o=n(t,r),s=n(t,t.neg(r)),i=(e+xn)/ve;return(f,c)=>{let a=f.pow(c,i),d=f.mul(a,r),x=f.mul(a,o),m=f.mul(a,s),w=f.eql(f.sqr(d),c),p=f.eql(f.sqr(x),c);a=f.cmov(a,d,w),d=f.cmov(m,x,p);let S=f.eql(f.sqr(d),c),B=f.cmov(a,d,S);return Zt(f,B,c),B}}function Le(e){if(e<_e)throw new Error("sqrt is not defined for small field");let t=e-U,n=0;for(;t%J===C;)t/=J,n++;let r=J,o=Bt(e);for(;Ee(o,r)===1;)if(r++>1e3)throw new Error("Cannot find square root: probably non-prime P");if(n===1)return Oe;let s=o.pow(r,t),i=(t+U)/J;return function(c,a){if(c.is0(a))return a;if(Ee(c,a)!==1)throw new Error("Cannot find square root");let d=n,x=c.mul(c.ONE,s),m=c.pow(a,t),w=c.pow(a,i);for(;!c.eql(m,c.ONE);){if(c.is0(m))return c.ZERO;let p=1,S=c.sqr(m);for(;!c.eql(S,c.ONE);)if(p++,S=c.sqr(S),p===d)throw new Error("Cannot find square root");let B=U<(H(e,t)&U)===U,yn=["create","isValid","is0","neg","inv","sqrt","sqr","eql","add","sub","mul","pow","div","addN","subN","mulN","sqrN"];function Te(e){let t={ORDER:"bigint",BYTES:"number",BITS:"number"},n=yn.reduce((r,o)=>(r[o]="function",r),t);return ct(e,n),e}function wn(e,t,n){if(nC;)n&U&&(r=e.mul(r,o)),o=e.sqr(o),n>>=U;return r}function wt(e,t,n=!1){let r=new Array(t.length).fill(n?e.ZERO:void 0),o=t.reduce((i,f,c)=>e.is0(f)?i:(r[c]=i,e.mul(i,f)),e.ONE),s=e.inv(o);return t.reduceRight((i,f,c)=>e.is0(f)?i:(r[c]=e.mul(i,r[c]),e.mul(i,f)),s),r}function Ee(e,t){let n=(e.ORDER-U)/J,r=e.pow(t,n),o=e.eql(r,e.ONE),s=e.eql(r,e.ZERO),i=e.eql(r,e.neg(e.ONE));if(!o&&!s&&!i)throw new Error("invalid Legendre symbol result");return o?1:s?0:-1}function Bn(e,t){t!==void 0&&j(t);let n=t!==void 0?t:e.toString(2).length,r=Math.ceil(n/8);return{nBitLength:n,nByteLength:r}}var Ct=class{ORDER;BITS;BYTES;isLE;ZERO=C;ONE=U;_lengths;_sqrt;_mod;constructor(t,n={}){if(t<=C)throw new Error("invalid field: expected ORDER > 0, got "+t);let r;this.isLE=!1,n!=null&&typeof n=="object"&&(typeof n.BITS=="number"&&(r=n.BITS),typeof n.sqrt=="function"&&(this.sqrt=n.sqrt),typeof n.isLE=="boolean"&&(this.isLE=n.isLE),n.allowedLengths&&(this._lengths=n.allowedLengths?.slice()),typeof n.modFromBytes=="boolean"&&(this._mod=n.modFromBytes));let{nBitLength:o,nByteLength:s}=Bn(t,r);if(s>2048)throw new Error("invalid field: expected ORDER of <= 2048 bytes");this.ORDER=t,this.BITS=o,this.BYTES=s,this._sqrt=void 0,Object.preventExtensions(this)}create(t){return H(t,this.ORDER)}isValid(t){if(typeof t!="bigint")throw new Error("invalid field element: expected bigint, got "+typeof t);return C<=t&&to)throw new Error("Field.fromBytes: expected "+r+" bytes, got "+t.length);let a=new Uint8Array(o);a.set(t,s?0:a.length-t.length),t=a}if(t.length!==o)throw new Error("Field.fromBytes: expected "+o+" bytes, got "+t.length);let c=s?rt(t):me(t);if(f&&(c=H(c,i)),!n&&!this.isValid(c))throw new Error("invalid field element: outside of range 0..ORDER");return c}invertBatch(t){return wt(this,t)}cmov(t,n,r){return r?n:t}};function Bt(e,t={}){return new Ct(e,t)}var Et=BigInt(0),Gt=BigInt(1);function He(e,t){let n=t.negate();return e?n:t}function At(e,t){let n=wt(e.Fp,t.map(r=>r.Z));return t.map((r,o)=>e.fromAffine(r.toAffine(n[o])))}function Ue(e,t){if(!Number.isSafeInteger(e)||e<=0||e>t)throw new Error("invalid window size, expected [1.."+t+"], got W="+e)}function Mt(e,t){Ue(e,t);let n=Math.ceil(t/e)+1,r=2**(e-1),o=2**e,s=we(e),i=BigInt(e);return{windows:n,windowSize:r,mask:s,maxNumber:o,shiftBy:i}}function De(e,t,n){let{windowSize:r,mask:o,maxNumber:s,shiftBy:i}=n,f=Number(e&o),c=e>>i;f>r&&(f-=s,c+=Gt);let a=t*r,d=a+Math.abs(f)-1,x=f===0,m=f<0,w=t%2!==0;return{nextN:c,offset:d,isZero:x,isNeg:m,isNegF:w,offsetF:a}}var Vt=new WeakMap,ke=new WeakMap;function Xt(e){return ke.get(e)||1}function Ne(e){if(e!==Et)throw new Error("invalid wNAF")}var _t=class{BASE;ZERO;Fn;bits;constructor(t,n){this.BASE=t.BASE,this.ZERO=t.ZERO,this.Fn=t.Fn,this.bits=n}_unsafeLadder(t,n,r=this.ZERO){let o=t;for(;n>Et;)n&Gt&&(r=r.add(o)),o=o.double(),n>>=Gt;return r}precomputeWindow(t,n){let{windows:r,windowSize:o}=Mt(n,this.bits),s=[],i=t,f=i;for(let c=0;cEt))throw new Error(`CURVE.${c} must be positive bigint`)}let o=qe(t.p,n.Fp,r),s=qe(t.n,n.Fn,r),f=["Gx","Gy","a",e==="weierstrass"?"b":"d"];for(let c of f)if(!o.isValid(t[c]))throw new Error(`CURVE.${c} must be valid field element of CURVE.Fp`);return t=Object.freeze(Object.assign({},t)),{CURVE:t,Fp:o,Fn:s}}function Ze(e,t){return function(r){let o=e(r);return{secretKey:o,publicKey:t(o)}}}var W=BigInt(0),D=BigInt(1),Yt=BigInt(2),En=BigInt(8);function _n(e,t,n,r){let o=e.sqr(n),s=e.sqr(r),i=e.add(e.mul(t.a,o),s),f=e.add(e.ONE,e.mul(t.d,e.mul(o,s)));return e.eql(i,f)}function Me(e,t={}){let n=Ce("edwards",e,t,t.FpFnLE),{Fp:r,Fn:o}=n,s=n.CURVE,{h:i}=s;ct(t,{},{uvRatio:"function"});let f=Yt<r.create(B),a=t.uvRatio||((B,h)=>{try{return{isValid:!0,value:r.sqrt(r.div(B,h))}}catch{return{isValid:!1,value:W}}});if(!_n(r,s,s.Gx,s.Gy))throw new Error("bad curve params: generator point");function d(B,h,g=!1){let u=g?D:W;return Ut("coordinate "+B,h,u,f),h}function x(B){if(!(B instanceof p))throw new Error("EdwardsPoint expected")}let m=kt((B,h)=>{let{X:g,Y:u,Z:E}=B,A=B.is0();h==null&&(h=A?En:r.inv(E));let R=c(g*h),T=c(u*h),l=r.mul(E,h);if(A)return{x:W,y:D};if(l!==D)throw new Error("invZ was invalid");return{x:R,y:T}}),w=kt(B=>{let{a:h,d:g}=s;if(B.is0())throw new Error("bad point: ZERO");let{X:u,Y:E,Z:A,T:R}=B,T=c(u*u),l=c(E*E),b=c(A*A),y=c(b*b),_=c(T*h),I=c(b*c(_+l)),v=c(y+c(g*c(T*l)));if(I!==v)throw new Error("bad point: equation left != right (1)");let L=c(u*E),N=c(A*R);if(L!==N)throw new Error("bad point: equation left != right (2)");return!0});class p{static BASE=new p(s.Gx,s.Gy,D,c(s.Gx*s.Gy));static ZERO=new p(W,D,D,W);static Fp=r;static Fn=o;X;Y;Z;T;constructor(h,g,u,E){this.X=d("x",h),this.Y=d("y",g),this.Z=d("z",u,!0),this.T=d("t",E),Object.freeze(this)}static CURVE(){return s}static fromAffine(h){if(h instanceof p)throw new Error("extended point not allowed");let{x:g,y:u}=h||{};return d("x",g),d("y",u),new p(g,u,D,c(g*u))}static fromBytes(h,g=!1){let u=r.BYTES,{a:E,d:A}=s;h=yt(O(h,u,"point")),mt(g,"zip215");let R=yt(h),T=h[u-1];R[u-1]=T&-129;let l=rt(R),b=g?f:r.ORDER;Ut("point.y",l,W,b);let y=c(l*l),_=c(y-D),I=c(A*y-E),{isValid:v,value:L}=a(_,I);if(!v)throw new Error("bad point: invalid y coordinate");let N=(L&D)===D,Z=(T&128)!==0;if(!g&&L===W&&Z)throw new Error("bad point: x=0 and x_0=1");return Z!==N&&(L=c(-L)),p.fromAffine({x:L,y:l})}static fromHex(h,g=!1){return p.fromBytes(M(h),g)}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(h=8,g=!0){return S.createCache(this,h),g||this.multiply(Yt),this}assertValidity(){w(this)}equals(h){x(h);let{X:g,Y:u,Z:E}=this,{X:A,Y:R,Z:T}=h,l=c(g*T),b=c(A*E),y=c(u*T),_=c(R*E);return l===b&&y===_}is0(){return this.equals(p.ZERO)}negate(){return new p(c(-this.X),this.Y,this.Z,c(-this.T))}double(){let{a:h}=s,{X:g,Y:u,Z:E}=this,A=c(g*g),R=c(u*u),T=c(Yt*c(E*E)),l=c(h*A),b=g+u,y=c(c(b*b)-A-R),_=l+R,I=_-T,v=l-R,L=c(y*I),N=c(_*v),Z=c(y*v),G=c(I*_);return new p(L,N,G,Z)}add(h){x(h);let{a:g,d:u}=s,{X:E,Y:A,Z:R,T}=this,{X:l,Y:b,Z:y,T:_}=h,I=c(E*l),v=c(A*b),L=c(T*u*_),N=c(R*y),Z=c((E+A)*(l+b)-I-v),G=N-L,Y=N+L,et=c(v-g*I),at=c(Z*G),vt=c(Y*et),Qt=c(Z*et),Pt=c(G*Y);return new p(at,vt,Pt,Qt)}subtract(h){return this.add(h.negate())}multiply(h){if(!o.isValidNot0(h))throw new Error("invalid scalar: expected 1 <= sc < curve.n");let{p:g,f:u}=S.cached(this,h,E=>At(p,E));return At(p,[g,u])[0]}multiplyUnsafe(h,g=p.ZERO){if(!o.isValid(h))throw new Error("invalid scalar: expected 0 <= sc < curve.n");return h===W?p.ZERO:this.is0()||h===D?this:S.unsafe(this,h,u=>At(p,u),g)}isSmallOrder(){return this.multiplyUnsafe(i).is0()}isTorsionFree(){return S.unsafe(this,s.n).is0()}toAffine(h){return m(this,h)}clearCofactor(){return i===D?this:this.multiplyUnsafe(i)}toBytes(){let{x:h,y:g}=this.toAffine(),u=r.toBytes(g);return u[u.length-1]|=h&D?128:0,u}toHex(){return k(this.toBytes())}toString(){return``}}let S=new _t(p,o.BITS);return p.BASE.precompute(8),p}function Ve(e,t,n={}){if(typeof t!="function")throw new Error('"hash" function param is required');ct(n,{},{adjustScalarBytes:"function",randomBytes:"function",domain:"function",prehash:"function",mapToCurve:"function"});let{prehash:r}=n,{BASE:o,Fp:s,Fn:i}=e,f=n.randomBytes||Lt,c=n.adjustScalarBytes||(l=>l),a=n.domain||((l,b,y)=>{if(mt(y,"phflag"),b.length||y)throw new Error("Contexts/pre-hash are not supported");return l});function d(l){return i.create(rt(l))}function x(l){let b=u.secretKey;O(l,u.secretKey,"secretKey");let y=O(t(l),2*b,"hashedSecretKey"),_=c(y.slice(0,b)),I=y.slice(b,2*b),v=d(_);return{head:_,prefix:I,scalar:v}}function m(l){let{head:b,prefix:y,scalar:_}=x(l),I=o.multiply(_),v=I.toBytes();return{head:b,prefix:y,scalar:_,point:I,pointBytes:v}}function w(l){return m(l).pointBytes}function p(l=Uint8Array.of(),...b){let y=ot(...b);return d(t(a(y,O(l,void 0,"context"),!!r)))}function S(l,b,y={}){l=O(l,void 0,"message"),r&&(l=r(l));let{prefix:_,scalar:I,pointBytes:v}=m(b),L=p(y.context,_,l),N=o.multiply(L).toBytes(),Z=p(y.context,N,v,l),G=i.create(L+Z*I);if(!i.isValid(G))throw new Error("sign failed: invalid s");let Y=ot(N,i.toBytes(G));return O(Y,u.signature,"result")}let B={zip215:!0};function h(l,b,y,_=B){let{context:I,zip215:v}=_,L=u.signature;l=O(l,L,"signature"),b=O(b,void 0,"message"),y=O(y,u.publicKey,"publicKey"),v!==void 0&&mt(v,"zip215"),r&&(b=r(b));let N=L/2,Z=l.subarray(0,N),G=rt(l.subarray(N,L)),Y,et,at;try{Y=e.fromBytes(y,v),et=e.fromBytes(Z,v),at=o.multiplyUnsafe(G)}catch{return!1}if(!v&&Y.isSmallOrder())return!1;let vt=p(I,et.toBytes(),Y.toBytes(),b);return et.add(Y.multiplyUnsafe(vt)).subtract(at).clearCofactor().is0()}let g=s.BYTES,u={secretKey:g,publicKey:g,signature:2*g,seed:g};function E(l=f(u.seed)){return O(l,u.seed,"seed")}function A(l){return ut(l)&&l.length===i.BYTES}function R(l,b){try{return!!e.fromBytes(l,b)}catch{return!1}}let T={getExtendedPublicKey:m,randomSecretKey:E,isValidSecretKey:A,isValidPublicKey:R,toMontgomery(l){let{y:b}=e.fromBytes(l),y=u.publicKey,_=y===32;if(!_&&y!==57)throw new Error("only defined for 25519 and 448");let I=_?s.div(D+b,D-b):s.div(b-D,b+D);return s.toBytes(I)},toMontgomerySecret(l){let b=u.secretKey;O(l,b);let y=t(l.subarray(0,b));return c(y).subarray(0,b)}};return Object.freeze({keygen:Ze(E,w),getPublicKey:w,sign:S,verify:h,utils:T,Point:e,lengths:u})}var An=BigInt(1),Xe=BigInt(2);var Sn=BigInt(5),In=BigInt(8),Kt=BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed"),vn={p:Kt,n:BigInt("0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"),h:In,a:BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec"),d:BigInt("0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3"),Gx:BigInt("0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a"),Gy:BigInt("0x6666666666666666666666666666666666666666666666666666666666666658")};function On(e){let t=BigInt(10),n=BigInt(20),r=BigInt(40),o=BigInt(80),s=Kt,f=e*e%s*e%s,c=V(f,Xe,s)*f%s,a=V(c,An,s)*e%s,d=V(a,Sn,s)*a%s,x=V(d,t,s)*d%s,m=V(x,n,s)*x%s,w=V(m,r,s)*m%s,p=V(w,o,s)*w%s,S=V(p,o,s)*w%s,B=V(S,t,s)*d%s;return{pow_p_5_8:V(B,Xe,s)*e%s,b2:f}}function Ln(e){return e[0]&=248,e[31]&=127,e[31]|=64,e}var Ge=BigInt("19681161376707505956807079304988542015446066515923890162744021073123829784752");function Rn(e,t){let n=Kt,r=H(t*t*t,n),o=H(r*r*t,n),s=On(e*o).pow_p_5_8,i=H(e*r*s,n),f=H(t*i*i,n),c=i,a=H(i*Ge,n),d=f===e,x=f===H(-e,n),m=f===H(-e*Ge,n);return d&&(i=c),(x||m)&&(i=a),Re(i,n)&&(i=H(-i,n)),{isValid:d||x,value:i}}var Tn=Me(vn,{uvRatio:Rn});function Hn(e){return Ve(Tn,xe,Object.assign({adjustScalarBytes:Ln},e))}var Ye=Hn({});var Dn=BigInt(0),ft=BigInt(1),Nn=BigInt(2),qn=BigInt(7),Un=BigInt(256),kn=BigInt(113),Fe=[],ze=[],$e=[];for(let e=0,t=ft,n=1,r=0;e<24;e++){[n,r]=[r,(2*n+3*r)%5],Fe.push(2*(5*r+n)),ze.push((e+1)*(e+2)/2%64);let o=Dn;for(let s=0;s<7;s++)t=(t<>qn)*kn)%Un,t&Nn&&(o^=ft<<(ft<n>32?se(e,t,n):re(e,t,n),je=(e,t,n)=>n>32?ie(e,t,n):oe(e,t,n);function Mn(e,t=24){let n=new Uint32Array(10);for(let r=24-t;r<24;r++){for(let i=0;i<10;i++)n[i]=e[i]^e[i+10]^e[i+20]^e[i+30]^e[i+40];for(let i=0;i<10;i+=2){let f=(i+8)%10,c=(i+2)%10,a=n[c],d=n[c+1],x=Ke(a,d,1)^n[f],m=je(a,d,1)^n[f+1];for(let w=0;w<50;w+=10)e[i+w]^=x,e[i+w+1]^=m}let o=e[2],s=e[3];for(let i=0;i<24;i++){let f=ze[i],c=Ke(o,s,f),a=je(o,s,f),d=Fe[i];o=e[d],s=e[d+1],e[d]=c,e[d+1]=a}for(let i=0;i<50;i+=10){for(let f=0;f<10;f++)n[f]=e[i+f];for(let f=0;f<10;f++)e[i+f]^=~n[(f+2)%10]&n[(f+4)%10]}e[0]^=Cn[r],e[1]^=Zn[r]}F(n)}var jt=class e{state;pos=0;posOut=0;finished=!1;state32;destroyed=!1;blockLen;suffix;outputLen;enableXOF=!1;rounds;constructor(t,n,r,o=!1,s=24){if(this.blockLen=t,this.suffix=n,this.outputLen=r,this.enableXOF=o,this.rounds=s,j(r,"outputLen"),!(0=r&&this.keccak();let i=Math.min(r-this.posOut,s-o);t.set(n.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return j(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(lt(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,F(this.state)}_cloneInto(t){let{blockLen:n,suffix:r,outputLen:o,rounds:s,enableXOF:i}=this;return t||=new e(n,r,o,i,s),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=s,t.suffix=r,t.outputLen=o,t.enableXOF=i,t.destroyed=this.destroyed,t}},Vn=(e,t,n,r={})=>ht(()=>new jt(t,e,n),r);var Qe=Vn(1,136,32);var tt=Ye.Point,Pe=tt.Fn.ORDER;function Ft(e){return Qe(e)}function zt(e){let t=Ft(e),n=0n;for(let s=t.length-1;s>=0;s--)n=n<<8n|BigInt(t[s]);n=n%Pe;let r=new Uint8Array(32),o=n;for(let s=0;s<32;s++)r[s]=Number(o&0xFFn),o>>=8n;return r}function $t(e){let t=[];for(;e>=128;)t.push(e&127|128),e>>>=7;return t.push(e&127),new Uint8Array(t)}function Xn(e,t){let n=0,r=0,o=t;for(;oo+s.length,0),n=new Uint8Array(t),r=0;for(let o of e)n.set(o,r),r+=o.length;return n}function It(e){let t=0n;for(let n=e.length-1;n>=0;n--)t=t<<8n|BigInt(e[n]);return t}function Je(e,t){let n=tt.fromHex(e),r=It(M(t));return n.multiply(r).multiply(8n).toBytes()}function tn(e,t,n){let r=$t(t),o=St(e,r),s=zt(o),i=It(s),f=tt.BASE.multiply(i),c=tt.fromHex(n),a=f.add(c);return k(a.toBytes())}function en(e,t,n){let r=$t(n),o=St(t,r),s=Ft(St(new TextEncoder().encode("amount"),zt(o))),i=M(e),f=new Uint8Array(8);for(let a=0;a<8;a++)f[a]=(i[a]||0)^s[a];let c=0n;for(let a=7;a>=0;a--)c=c<<8n|BigInt(f[a]);return c}function Gn(e){let t=M(e),n=[],r=0;for(;r=0;d--)a[d]=Number(c&0xFFn),c>>=8n;return a}for(let i=0;i0){let i=e.substr(n*11),f=t[r];if(f===-1)throw new Error("Invalid base58 length");o.push(...s(i,f))}return new Uint8Array(o)}function nn(e){let t=Wt(e);if(t.length!==69&&t.length!==77)throw new Error("Invalid address length: "+t.length);let n=k(t.slice(1,33)),r=k(t.slice(33,65));return{publicSpendKey:n,publicViewKey:r}}function jn(e,t){let n=nn(e),o=Wt(e)[0],s=It(M(t));if(o===42||o===36){let i=tt.fromHex(n.publicSpendKey);return k(i.multiply(s).toBytes())===n.publicViewKey}else return k(tt.BASE.multiply(s).toBytes())===n.publicViewKey}window.XmrCrypto={generateKeyDerivation:Je,deriveOutputPublicKey:tn,decodeRctAmount:en,checkOutput:Yn,parseTxExtra:Gn,getKeysFromAddress:nn,validateViewKey:jn,base58Decode:Wt,keccak:Ft,hashToScalar:zt,hexToBytes:M,bytesToHex:k,bytesToScalar:It,concat:St,encodeVarint:$t,decodeVarint:Xn,Point:tt,CURVE_ORDER:Pe};})(); +/*! Bundled license information: + +@noble/hashes/utils.js: + (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *) + +@noble/curves/utils.js: +@noble/curves/abstract/modular.js: +@noble/curves/abstract/curve.js: +@noble/curves/abstract/edwards.js: +@noble/curves/ed25519.js: + (*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) *) +*/ diff --git a/monitor.js b/monitor.js new file mode 100644 index 0000000..3feafdb --- /dev/null +++ b/monitor.js @@ -0,0 +1,422 @@ +/** + * monitor.js — Monero Payment Monitor (v2) + * Uses XmrCrypto (noble-curves bundle) for output scanning. + * View key never leaves the browser. + */ +var PaymentMonitor = (function () { + 'use strict'; + + var STATE = { + IDLE: 'idle', + CONNECTING: 'connecting', + SCANNING: 'scanning', + WAITING: 'waiting', + MEMPOOL: 'mempool', + CONFIRMED: 'confirmed', + UNDERPAID: 'underpaid', + ERROR: 'error' + }; + + var config = { + pollInterval: 30000, // 30s mempool polling + confirmPoll: 30000, // 30s confirmation polling + maxConfirmations: 10, + proxyUrl: '/api/node.php' + }; + + var state = STATE.IDLE; + var pollTimer = null; + var abortController = null; + var onStateChange = null; + + // Monitoring params + var monitorAddr = null; + var monitorViewKey = null; + var monitorSpendKey = null; + var monitorViewKeyPub = null; + var expectedAmount = null; // in piconero (bigint) + var startHeight = 0; + var detectedTxHash = null; + var detectedAmount = 0n; + var lastConfirmations = 0; + + function setState(newState, data) { + state = newState; + if (onStateChange) onStateChange(newState, data); + } + + async function rpc(method, params) { + if (abortController && abortController.signal.aborted) throw new Error('Aborted'); + + var res = await fetch(config.proxyUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method: method, params: params || {} }), + signal: abortController ? abortController.signal : undefined + }); + + if (!res.ok) throw new Error('RPC error: HTTP ' + res.status); + return res.json(); + } + + function start(address, privateViewKey, expectedXmr, callback) { + if (state !== STATE.IDLE) stop(); + + onStateChange = callback; + monitorAddr = address; + monitorViewKey = privateViewKey; + expectedAmount = xmrToPiconero(expectedXmr); + detectedTxHash = null; + detectedAmount = 0n; + lastConfirmations = 0; + abortController = new AbortController(); + + // Derive public keys from address + try { + var keys = XmrCrypto.getKeysFromAddress(address); + monitorSpendKey = keys.publicSpendKey; + monitorViewKeyPub = keys.publicViewKey; + + + } catch (e) { + setState(STATE.ERROR, { message: 'Invalid address' }); + return; + } + + // Validate view key against address (works for both standard and subaddress) + try { + var valid = XmrCrypto.validateViewKey(address, privateViewKey); + + if (!valid) { + setState(STATE.ERROR, { message: I18n.t('monitor_view_key_invalid') }); + return; + } + } catch (e) { + console.error('[monitor] View key validation error:', e); + setState(STATE.ERROR, { message: I18n.t('monitor_view_key_invalid') }); + return; + } + + setState(STATE.CONNECTING); + connectAndPoll(); + } + + async function connectAndPoll() { + try { + // Get current height + var info = await rpc('get_info'); + var result = info.result || info; + // Scan 100 blocks back (~3.3 hours) to catch confirmed payments + startHeight = (result.height || result.target_height || 0) - 100; + + setState(STATE.WAITING); + poll(); + pollTimer = setInterval(poll, config.pollInterval); + } catch (e) { + setState(STATE.ERROR, { message: I18n.t('monitor_node_error') }); + // Retry after delay + pollTimer = setTimeout(function () { + if (state === STATE.ERROR) connectAndPoll(); + }, 5000); + } + } + + async function poll() { + if (state === STATE.CONFIRMED) return; + + try { + if (detectedTxHash) { + // We already found a TX, check confirmations + await checkConfirmations(); + } else { + setState(STATE.SCANNING); + + // Scan recent blocks first (catches already confirmed payments) + await scanRecentBlocks(); + + // Then scan mempool for unconfirmed payments + if (!detectedTxHash) { + await scanMempool(); + } + + if (!detectedTxHash) { + setState(STATE.WAITING); + } + } + } catch (e) { + if (e.name !== 'AbortError') { + console.warn('Monitor poll error:', e); + } + } + } + + async function scanMempool() { + var pool = await rpc('get_transaction_pool'); + var transactions = pool.transactions || []; + + for (var i = 0; i < transactions.length; i++) { + var tx = transactions[i]; + var txJson = tx.tx_json ? JSON.parse(tx.tx_json) : null; + if (!txJson) continue; + + var match = scanTransaction(txJson, tx.id_hash); + if (match) { + detectedTxHash = match.txHash; + detectedAmount = match.amount; + reportDetection(0); + // Switch to confirmation polling + clearInterval(pollTimer); + pollTimer = setInterval(poll, config.confirmPoll); + return; + } + } + } + + async function scanRecentBlocks() { + try { + var info = await rpc('get_info'); + var result = info.result || info; + var currentHeight = result.height || 0; + + var fromHeight = Math.max(startHeight, currentHeight - 100); + var batchSize = 10; + + for (var batchStart = fromHeight; batchStart < currentHeight; batchStart += batchSize) { + var batchEnd = Math.min(batchStart + batchSize, currentHeight); + var allTxHashes = []; + var txHeightMap = {}; + + // Fetch blocks in this batch + for (var h = batchStart; h < batchEnd; h++) { + var blockData = await rpc('get_block', { height: h }); + var blockResult = blockData.result || blockData; + var hashes = blockResult.tx_hashes || []; + for (var i = 0; i < hashes.length; i++) { + allTxHashes.push(hashes[i]); + txHeightMap[hashes[i]] = h; + } + } + + if (allTxHashes.length === 0) continue; + + + // Fetch transactions in small sub-batches (restricted nodes limit response size) + var txBatchSize = 25; + for (var tbi = 0; tbi < allTxHashes.length; tbi += txBatchSize) { + var subBatch = allTxHashes.slice(tbi, tbi + txBatchSize); + var txData = await rpc('gettransactions', { + txs_hashes: subBatch, + decode_as_json: true + }); + + var txs = txData.txs || []; + if (txs.length === 0) { + + continue; + } + + for (var j = 0; j < txs.length; j++) { + var tx = txs[j]; + var txJson = tx.as_json ? JSON.parse(tx.as_json) : null; + if (!txJson) continue; + + var match = scanTransaction(txJson, tx.tx_hash); + if (match) { + detectedTxHash = match.txHash; + detectedAmount = match.amount; + var txHeight = txHeightMap[tx.tx_hash] || 0; + var confirmations = txHeight > 0 ? currentHeight - txHeight : 0; + reportDetection(confirmations); + clearInterval(pollTimer); + if (confirmations < config.maxConfirmations) { + pollTimer = setInterval(poll, config.confirmPoll); + } + return; + } + } + } + } + } catch (e) { + console.warn('Block scan error:', e); + } + } + + function scanTransaction(txJson, txHash) { + // Extract tx public keys from extra + var extraHex = ''; + if (txJson.extra) { + if (typeof txJson.extra === 'string') { + extraHex = txJson.extra; + } else if (Array.isArray(txJson.extra)) { + extraHex = txJson.extra.map(function (b) { + return ('0' + (b & 0xff).toString(16)).slice(-2); + }).join(''); + } + } + + var txPubKeys = XmrCrypto.parseTxExtra(extraHex); + if (txPubKeys.length === 0) { + + return null; + } + + // Get outputs + var outputs = []; + if (txJson.vout) { + outputs = txJson.vout; + } + + // Get encrypted amounts from RingCT + var ecdhInfo = []; + if (txJson.rct_signatures && txJson.rct_signatures.ecdhInfo) { + ecdhInfo = txJson.rct_signatures.ecdhInfo; + } + + + + var totalAmount = 0n; + var found = false; + + for (var ki = 0; ki < txPubKeys.length; ki++) { + var txPubKey = txPubKeys[ki]; + + for (var oi = 0; oi < outputs.length; oi++) { + var out = outputs[oi]; + var outputKey = null; + + if (out.target && out.target.tagged_key) { + outputKey = out.target.tagged_key.key; + } else if (out.target && out.target.key) { + outputKey = out.target.key; + } + + if (!outputKey) continue; + + var encryptedAmount = null; + if (ecdhInfo[oi] && ecdhInfo[oi].amount) { + encryptedAmount = ecdhInfo[oi].amount; + } + + try { + var result = XmrCrypto.checkOutput( + txPubKey, oi, outputKey, encryptedAmount, + monitorViewKey, monitorSpendKey + ); + + if (result.match) { + + totalAmount += result.amount; + found = true; + } + } catch (e) { + } + } + } + + if (found) { + return { txHash: txHash, amount: totalAmount }; + } + return null; + } + + function reportDetection(confirmations) { + lastConfirmations = confirmations; + + if (expectedAmount > 0n && detectedAmount < expectedAmount) { + setState(STATE.UNDERPAID, { + expected: piconeroToXmr(expectedAmount), + received: piconeroToXmr(detectedAmount), + confirmations: confirmations, + txHash: detectedTxHash + }); + } else if (confirmations >= config.maxConfirmations) { + setState(STATE.CONFIRMED, { + amount: piconeroToXmr(detectedAmount), + confirmations: confirmations, + txHash: detectedTxHash + }); + } else { + setState(STATE.MEMPOOL, { + amount: piconeroToXmr(detectedAmount), + confirmations: confirmations, + txHash: detectedTxHash + }); + } + } + + async function checkConfirmations() { + try { + var txData = await rpc('gettransactions', { + txs_hashes: [detectedTxHash], + decode_as_json: true + }); + + var txs = txData.txs || []; + if (txs.length === 0) return; + + var tx = txs[0]; + var confirmations = 0; + + if (tx.in_pool) { + confirmations = 0; + } else if (tx.block_height) { + var info = await rpc('get_info'); + var result = info.result || info; + var currentHeight = result.height || 0; + confirmations = currentHeight - tx.block_height; + } + + reportDetection(confirmations); + + if (confirmations >= config.maxConfirmations) { + clearInterval(pollTimer); + pollTimer = null; + } + } catch (e) { + console.warn('Confirmation check error:', e); + } + } + + function stop() { + if (pollTimer) { + clearInterval(pollTimer); + clearTimeout(pollTimer); + pollTimer = null; + } + if (abortController) { + abortController.abort(); + abortController = null; + } + state = STATE.IDLE; + monitorViewKey = null; + monitorSpendKey = null; + detectedTxHash = null; + detectedAmount = 0n; + onStateChange = null; + } + + function xmrToPiconero(xmr) { + if (!xmr || xmr <= 0) return 0n; + return BigInt(Math.round(xmr * 1e12)); + } + + function piconeroToXmr(piconero) { + return Number(piconero) / 1e12; + } + + function isValidViewKey(key) { + return /^[0-9a-fA-F]{64}$/.test(key); + } + + function getState() { + return state; + } + + return { + start: start, + stop: stop, + isValidViewKey: isValidViewKey, + getState: getState, + STATE: STATE + }; +})(); diff --git a/style.css b/style.css index 261940d..363b66a 100644 --- a/style.css +++ b/style.css @@ -236,6 +236,45 @@ input.valid { color: var(--error); } +.deadline-badges { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; +} + +.badge { + padding: 0.45rem 0.8rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + font-family: var(--font); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: border-color 0.2s, color 0.2s, background 0.2s; + white-space: nowrap; +} + +.badge:hover { + border-color: var(--accent); + color: var(--text); +} + +.badge.active { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.badge-input { + width: 70px; + padding: 0.45rem 0.5rem; + font-size: 0.8rem; + text-align: center; + flex-shrink: 0; +} + textarea { resize: vertical; min-height: 60px; @@ -290,7 +329,8 @@ textarea { .qr-container { display: flex; - justify-content: center; + flex-direction: column; + align-items: center; padding: 1rem 0; } @@ -367,6 +407,156 @@ textarea { flex: 1; } +/* --- Monitor Section --- */ + +.monitor-section { + margin-top: 1rem; + border-top: 1px solid var(--border); + padding-top: 0.8rem; +} + +.btn-monitor { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.6rem; + background: transparent; + border: 1px dashed var(--border); + border-radius: var(--radius); + color: var(--text-muted); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; + font-family: var(--font); +} + +.btn-monitor:hover { + border-color: var(--accent); + color: var(--text); +} + +.monitor-panel { + display: none; + margin-top: 0.8rem; +} + +.monitor-panel.open { + display: block; +} + +.mono-masked { + -webkit-text-security: disc; +} + +.view-key-hint { + font-size: 0.7rem; + color: var(--text-muted); + margin-top: 0.25rem; + font-style: italic; +} + +.monitor-status { + display: none; + text-align: center; + padding: 1rem 0; +} + +.monitor-status.active { + display: block; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + margin: 0 auto 0.5rem; + background: var(--text-muted); +} + +.status-indicator.connecting, +.status-indicator.scanning { + background: var(--accent); + animation: pulse 1.5s ease-in-out infinite; +} + +.status-indicator.waiting { + background: var(--accent); + animation: pulse 2s ease-in-out infinite; +} + +.status-indicator.mempool { + background: #ffc107; + animation: pulse 1s ease-in-out infinite; +} + +.status-indicator.confirmed { + background: var(--success); + animation: none; +} + +.status-indicator.underpaid { + background: var(--error); + animation: pulse 1s ease-in-out infinite; +} + +.status-indicator.error { + background: var(--error); +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.2); } +} + +.status-text { + font-size: 0.9rem; + color: var(--text); + margin-bottom: 0.5rem; +} + +.confirmations-bar { + display: none; + position: relative; + height: 24px; + background: var(--bg-input); + border-radius: var(--radius); + overflow: hidden; + margin-bottom: 0.8rem; +} + +.confirmations-bar.active { + display: block; +} + +.confirmations-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--success)); + border-radius: var(--radius); + transition: width 0.5s ease; + width: 0%; +} + +.confirmations-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.75rem; + font-weight: 600; + color: var(--text); +} + +#stopMonitor { + display: none; +} + +#stopMonitor.active { + display: block; +} + .btn-new { margin-top: 0.8rem; background: transparent; diff --git a/sw.js b/sw.js index 1f064f2..a3d3845 100644 --- a/sw.js +++ b/sw.js @@ -1,11 +1,13 @@ -var CACHE_NAME = 'xmrpay-v1'; +var CACHE_NAME = 'xmrpay-v2'; var ASSETS = [ '/', '/index.html', '/app.js', '/i18n.js', + '/monitor.js', '/style.css', '/lib/qrcode.min.js' + // xmr-crypto.bundle.js is lazy-loaded and runtime-cached ]; self.addEventListener('install', function (e) { @@ -32,8 +34,8 @@ self.addEventListener('activate', function (e) { self.addEventListener('fetch', function (e) { var url = new URL(e.request.url); - // CoinGecko API — network only, don't cache - if (url.hostname !== location.hostname) { + // External APIs and RPC proxy — network only, don't cache + if (url.hostname !== location.hostname || url.pathname.startsWith('/api/')) { e.respondWith(fetch(e.request)); return; }