let receiveTokenListRequestId = 0; function setPaymentMessage(message, mode = "") { if (!paymentMessage) { return; } paymentMessage.textContent = message; paymentMessage.classList.toggle("error", mode === "error"); paymentMessage.classList.toggle("success", mode === "success"); } function receiveAddress() { return activeWalletAddressShort || activeWalletAddressHex || ""; } function paymentRequestUri() { const address = receiveAddress(); if (!address) { return ""; } const params = new URLSearchParams(); const asset = paymentAssetSelect?.value || activeBaseCoin(); const amount = paymentAmountInput?.value?.trim() || ""; const label = paymentLabelInput?.value?.trim() || ""; if (asset) { params.set("asset", asset); } if (amount) { params.set("amount", amount); } if (label) { params.set("label", label); } const query = params.toString(); return `contractless:${address}${query ? `?${query}` : ""}`; } function qrGaloisTables() { const exp = new Array(512).fill(0); const log = new Array(256).fill(0); let value = 1; for (let index = 0; index < 255; index += 1) { exp[index] = value; log[value] = index; value <<= 1; if (value & 0x100) { value ^= 0x11d; } } for (let index = 255; index < 512; index += 1) { exp[index] = exp[index - 255]; } return { exp, log }; } function qrMultiply(left, right, tables) { if (!left || !right) { return 0; } return tables.exp[tables.log[left] + tables.log[right]]; } function qrGeneratorPolynomial(degree, tables) { let poly = [1]; for (let index = 0; index < degree; index += 1) { const next = new Array(poly.length + 1).fill(0); poly.forEach((coefficient, coefficientIndex) => { next[coefficientIndex] ^= coefficient; next[coefficientIndex + 1] ^= qrMultiply(coefficient, tables.exp[index], tables); }); poly = next; } return poly; } function qrReedSolomon(data, eccLength) { const tables = qrGaloisTables(); const generator = qrGeneratorPolynomial(eccLength, tables); const ecc = new Array(eccLength).fill(0); data.forEach((byte) => { const factor = byte ^ ecc.shift(); ecc.push(0); for (let index = 0; index < eccLength; index += 1) { ecc[index] ^= qrMultiply(generator[index + 1], factor, tables); } }); return ecc; } function qrAppendBits(bits, value, length) { for (let bit = length - 1; bit >= 0; bit -= 1) { bits.push((value >>> bit) & 1); } } function qrEncodeBytes(text) { const bytes = Array.from(new TextEncoder().encode(text)); if (bytes.length > 134) { throw new Error("Payment URI is too long for the QR code. Shorten the label or amount."); } const bits = []; qrAppendBits(bits, 0b0100, 4); qrAppendBits(bits, bytes.length, 8); bytes.forEach((byte) => qrAppendBits(bits, byte, 8)); const dataBitCapacity = 136 * 8; const terminator = Math.min(4, dataBitCapacity - bits.length); qrAppendBits(bits, 0, terminator); while (bits.length % 8) { bits.push(0); } const data = []; for (let index = 0; index < bits.length; index += 8) { data.push(bits.slice(index, index + 8).reduce((value, bit) => (value << 1) | bit, 0)); } for (let pad = 0xec; data.length < 136; pad = pad === 0xec ? 0x11 : 0xec) { data.push(pad); } const block1 = data.slice(0, 68); const block2 = data.slice(68, 136); const ecc1 = qrReedSolomon(block1, 18); const ecc2 = qrReedSolomon(block2, 18); const codewords = []; for (let index = 0; index < 68; index += 1) { codewords.push(block1[index], block2[index]); } for (let index = 0; index < 18; index += 1) { codewords.push(ecc1[index], ecc2[index]); } return codewords.flatMap((byte) => { const out = []; qrAppendBits(out, byte, 8); return out; }); } function qrFormatBits() { let data = 0b01000; // ECC level L, mask 0. let value = data << 10; const generator = 0x537; for (let bit = 14; bit >= 10; bit -= 1) { if ((value >>> bit) & 1) { value ^= generator << (bit - 10); } } return ((data << 10) | value) ^ 0x5412; } function qrSet(matrix, reserved, row, col, value, isReserved = true) { if (row < 0 || col < 0 || row >= matrix.length || col >= matrix.length) { return; } matrix[row][col] = Boolean(value); if (isReserved) { reserved[row][col] = true; } } function qrFinder(matrix, reserved, row, col) { for (let y = -1; y <= 7; y += 1) { for (let x = -1; x <= 7; x += 1) { const r = row + y; const c = col + x; const dark = x >= 0 && x <= 6 && y >= 0 && y <= 6 && (x === 0 || x === 6 || y === 0 || y === 6 || (x >= 2 && x <= 4 && y >= 2 && y <= 4)); qrSet(matrix, reserved, r, c, dark); } } } function qrAlignment(matrix, reserved, row, col) { for (let y = -2; y <= 2; y += 1) { for (let x = -2; x <= 2; x += 1) { qrSet(matrix, reserved, row + y, col + x, Math.max(Math.abs(x), Math.abs(y)) !== 1); } } } function qrBuildMatrix(text) { const size = 41; const matrix = Array.from({ length: size }, () => new Array(size).fill(false)); const reserved = Array.from({ length: size }, () => new Array(size).fill(false)); qrFinder(matrix, reserved, 0, 0); qrFinder(matrix, reserved, 0, size - 7); qrFinder(matrix, reserved, size - 7, 0); qrAlignment(matrix, reserved, 34, 34); for (let index = 8; index < size - 8; index += 1) { qrSet(matrix, reserved, 6, index, index % 2 === 0); qrSet(matrix, reserved, index, 6, index % 2 === 0); } qrSet(matrix, reserved, 33, 8, true); for (let index = 0; index < 9; index += 1) { if (index !== 6) { reserved[8][index] = true; reserved[index][8] = true; } } for (let index = 0; index < 8; index += 1) { reserved[8][size - 1 - index] = true; reserved[size - 1 - index][8] = true; } const bits = qrEncodeBytes(text); let bitIndex = 0; let upward = true; for (let col = size - 1; col > 0; col -= 2) { if (col === 6) { col -= 1; } for (let step = 0; step < size; step += 1) { const row = upward ? size - 1 - step : step; for (let offset = 0; offset < 2; offset += 1) { const c = col - offset; if (reserved[row][c]) { continue; } const dark = bitIndex < bits.length ? bits[bitIndex] === 1 : false; matrix[row][c] = ((row + c) % 2 === 0) ? !dark : dark; bitIndex += 1; } } upward = !upward; } const format = qrFormatBits(); for (let index = 0; index < 15; index += 1) { const bit = ((format >>> index) & 1) === 1; const first = [ [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8] ][index]; const second = index < 8 ? [size - 1 - index, 8] : [8, size - 15 + index]; qrSet(matrix, reserved, first[0], first[1], bit); qrSet(matrix, reserved, second[0], second[1], bit); } return matrix; } function drawPaymentQr(canvas, text, label) { if (!canvas) { return; } const size = canvas.width || 256; const context = canvas.getContext("2d"); if (!context) { return; } context.fillStyle = "#ffffff"; context.fillRect(0, 0, size, size); context.fillStyle = "#101820"; try { const matrix = qrBuildMatrix(text); const modules = matrix.length; const quiet = 4; const cell = Math.floor(size / (modules + quiet * 2)); const offset = Math.floor((size - cell * modules) / 2); matrix.forEach((row, y) => { row.forEach((dark, x) => { if (dark) { context.fillRect(offset + x * cell, offset + y * cell, cell, cell); } }); }); setPaymentMessage(""); } catch (error) { context.fillStyle = "#101820"; context.font = "16px sans-serif"; context.textAlign = "center"; context.fillText("QR unavailable", size / 2, size / 2 - 8); context.font = "12px sans-serif"; context.fillText("Shorten request text", size / 2, size / 2 + 14); setPaymentMessage(String(error), "error"); } if (label) { const footerHeight = 28; context.fillStyle = "rgba(255,255,255,0.92)"; context.fillRect(0, size - footerHeight, size, footerHeight); context.fillStyle = "#101820"; context.font = "13px sans-serif"; context.textAlign = "center"; const trimmed = label.length > 30 ? `${label.slice(0, 27)}...` : label; context.fillText(trimmed, size / 2, size - 10); } } function drawEmptyPaymentQr(canvas) { if (!canvas) { return; } const size = canvas.width || 256; const context = canvas.getContext("2d"); if (!context) { return; } context.fillStyle = "#ffffff"; context.fillRect(0, 0, size, size); context.fillStyle = "#101820"; context.font = "16px sans-serif"; context.textAlign = "center"; context.fillText("Load wallet", size / 2, size / 2); } function renderPaymentQr(uri, label) { if (!uri) { drawEmptyPaymentQr(paymentQrCanvas); return; } drawPaymentQr(paymentQrCanvas, uri, label); } function updateReceiveDetails() { if (receiveRegistration) { receiveRegistration.textContent = registrationRegistered ? "Registered" : "Unregistered"; } } function updatePaymentRequest() { const uri = paymentRequestUri(); if (paymentUriInput) { paymentUriInput.value = uri; } if (paymentQrLabel) { paymentQrLabel.textContent = paymentLabelInput?.value?.trim() || "Payment Request"; } renderPaymentQr(uri, paymentQrLabel?.textContent || ""); updateReceiveDetails(); } async function refreshReceiveTokenList() { const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!invoke || !walletLoaded || !broadcastNode || !paymentAssetSelect) { return; } const requestId = ++receiveTokenListRequestId; const selected = paymentAssetSelect.value || activeBaseCoin(); paymentAssetSelect.innerHTML = ""; const baseOption = document.createElement("option"); baseOption.value = activeBaseCoin(); baseOption.textContent = activeBaseCoin(); paymentAssetSelect.appendChild(baseOption); try { const tokens = await invokeWalletRpcWithBackoff( "Token list lookup", () => invoke("token_list", { broadcastNode }), () => requestId === receiveTokenListRequestId && walletLoaded ); if (requestId !== receiveTokenListRequestId || !walletLoaded) { return; } (Array.isArray(tokens) ? tokens : []).forEach((token) => { const ticker = String(token?.ticker || "").trim(); if (!ticker || ticker.toLowerCase() === activeBaseCoin().toLowerCase()) { return; } const option = document.createElement("option"); option.value = ticker; option.textContent = ticker; paymentAssetSelect.appendChild(option); }); paymentAssetSelect.value = [...paymentAssetSelect.options].some((option) => option.value === selected) ? selected : activeBaseCoin(); } catch (error) { setPaymentMessage(String(error), "error"); } finally { updatePaymentRequest(); } } async function copyPaymentUri() { const uri = paymentRequestUri(); if (!uri || !(await writeClipboardText(uri))) { setPaymentMessage("Payment URI is not available to copy.", "error"); return; } setPaymentMessage("Payment URI copied.", "success"); } async function copyPaymentQr() { if (!paymentQrCanvas || !navigator.clipboard || typeof ClipboardItem === "undefined") { await copyPaymentUri(); return; } paymentQrCanvas.toBlob(async (blob) => { if (!blob) { setPaymentMessage("Payment QR is not available to copy.", "error"); return; } try { await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); setPaymentMessage("Payment request QR copied.", "success"); } catch (_) { await copyPaymentUri(); } }, "image/png"); } function refreshReceivePage() { updateReceiveDetails(); updatePaymentRequest(); refreshReceiveTokenList(); } [paymentAssetSelect, paymentAmountInput, paymentLabelInput].forEach((input) => { input?.addEventListener("input", updatePaymentRequest); input?.addEventListener("change", updatePaymentRequest); }); copyPaymentUriButton?.addEventListener("click", copyPaymentUri); copyPaymentQrButton?.addEventListener("click", copyPaymentQr);