Contractless-GUI-Wallet/frontend/js/receive_page.js

434 lines
12 KiB
JavaScript

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);