656 lines
20 KiB
JavaScript
656 lines
20 KiB
JavaScript
|
|
let addressBookCache = { entries: {} };
|
||
|
|
let addressBookLoaded = false;
|
||
|
|
let addressBookLoadingPromise = null;
|
||
|
|
let addressBookWriteTimer = null;
|
||
|
|
let addressBookWritePromise = Promise.resolve();
|
||
|
|
let addressBookVanityRefreshRunning = false;
|
||
|
|
let addressBookVanityRefreshDoneForWallet = "";
|
||
|
|
let addressBookVanityLookupRunning = false;
|
||
|
|
const addressBookVanityLookupQueue = new Set();
|
||
|
|
|
||
|
|
const ADDRESS_BOOK_UNKNOWN = "<unknown>";
|
||
|
|
const ADDRESS_BOOK_WRITE_DELAY_MS = 500;
|
||
|
|
const ADDRESS_BOOK_VANITY_DELAY_MS = 650;
|
||
|
|
|
||
|
|
function normalizeAddressBook(source) {
|
||
|
|
const entries = source?.entries && typeof source.entries === "object" && !Array.isArray(source.entries)
|
||
|
|
? source.entries
|
||
|
|
: {};
|
||
|
|
const normalized = {};
|
||
|
|
|
||
|
|
Object.entries(entries).forEach(([address, entry]) => {
|
||
|
|
if (isRawVanityAddressHex(address)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const canonical = canonicalAddressBookAddress(address);
|
||
|
|
if (!canonical || !entry || typeof entry !== "object") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
normalized[canonical] = {
|
||
|
|
label: stringOrDefault(entry.label, ADDRESS_BOOK_UNKNOWN),
|
||
|
|
label_source: stringOrDefault(entry.label_source, "sync"),
|
||
|
|
vanity: stringOrDefault(entry.vanity, ADDRESS_BOOK_UNKNOWN),
|
||
|
|
latest_txid: stringOrDefault(entry.latest_txid, ""),
|
||
|
|
created_at: Number(entry.created_at || 0) || currentUnixSeconds(),
|
||
|
|
updated_at: Number(entry.updated_at || 0) || currentUnixSeconds()
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
return { entries: normalized };
|
||
|
|
}
|
||
|
|
|
||
|
|
function stringOrDefault(value, fallback) {
|
||
|
|
const text = String(value ?? "").trim();
|
||
|
|
return text || fallback;
|
||
|
|
}
|
||
|
|
|
||
|
|
function currentUnixSeconds() {
|
||
|
|
return Math.floor(Date.now() / 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function shortAddressFromRawAddressHex(address) {
|
||
|
|
const text = String(address || "").trim().toLowerCase();
|
||
|
|
if (!/^[a-f0-9]{44}$/.test(text) || text.slice(40, 42) !== "2e") {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const network = {
|
||
|
|
"01": "clc",
|
||
|
|
"02": "cltc"
|
||
|
|
}[text.slice(42, 44)];
|
||
|
|
|
||
|
|
return network ? `${text.slice(0, 40)}.${network}` : "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function vanityAddressFromRawAddressHex(address) {
|
||
|
|
const text = String(address || "").trim().toLowerCase();
|
||
|
|
if (!/^[a-f0-9]{44}$/.test(text) || text.slice(40, 42) !== "2e") {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const network = {
|
||
|
|
"01": "clc",
|
||
|
|
"02": "cltc"
|
||
|
|
}[text.slice(42, 44)];
|
||
|
|
if (!network) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const payloadBytes = text.slice(0, 40).match(/../g) || [];
|
||
|
|
const payload = payloadBytes
|
||
|
|
.map((part) => String.fromCharCode(Number.parseInt(part, 16)))
|
||
|
|
.join("")
|
||
|
|
.trimStart();
|
||
|
|
if (!payload || !/^[a-z]+$/i.test(payload)) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${payload.toLowerCase()}.${network}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isRawVanityAddressHex(address) {
|
||
|
|
return Boolean(vanityAddressFromRawAddressHex(address));
|
||
|
|
}
|
||
|
|
|
||
|
|
function canonicalAddressBookAddress(address) {
|
||
|
|
const text = String(address || "").trim().toLowerCase();
|
||
|
|
if (/^[a-f0-9]{40}\.[a-z0-9]+$/.test(text)) {
|
||
|
|
return text;
|
||
|
|
}
|
||
|
|
if (isRawVanityAddressHex(text)) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
const fromRawBytes = shortAddressFromRawAddressHex(text);
|
||
|
|
if (fromRawBytes) {
|
||
|
|
return fromRawBytes;
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function shouldSkipAddressBookAddress(address) {
|
||
|
|
const canonical = canonicalAddressBookAddress(address);
|
||
|
|
return (
|
||
|
|
!canonical ||
|
||
|
|
canonical === canonicalAddressBookAddress(activeWalletAddressShort) ||
|
||
|
|
canonical === "network" ||
|
||
|
|
canonical === "self"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function addressBookSleep(ms) {
|
||
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
|
||
|
|
function addressBookEscape(value) {
|
||
|
|
return String(value ?? "")
|
||
|
|
.replaceAll("&", "&")
|
||
|
|
.replaceAll("<", "<")
|
||
|
|
.replaceAll(">", ">")
|
||
|
|
.replaceAll('"', """)
|
||
|
|
.replaceAll("'", "'");
|
||
|
|
}
|
||
|
|
|
||
|
|
function addressBookInvoke() {
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
throw new Error("Tauri bridge is not ready.");
|
||
|
|
}
|
||
|
|
return invoke;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function lookupAddressBookVanity(address) {
|
||
|
|
const broadcastNode = typeof activeBroadcastNode === "function" ? activeBroadcastNode() : "";
|
||
|
|
if (!walletLoaded || !broadcastNode) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = addressBookInvoke();
|
||
|
|
const vanity = await invoke("lookup_address_vanity", {
|
||
|
|
broadcastNode,
|
||
|
|
address
|
||
|
|
});
|
||
|
|
return String(vanity || "").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function enqueueAddressBookVanityLookup(address) {
|
||
|
|
const canonical = canonicalAddressBookAddress(address);
|
||
|
|
if (!canonical || shouldSkipAddressBookAddress(canonical)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
addressBookVanityLookupQueue.add(canonical);
|
||
|
|
runAddressBookVanityLookupQueue();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runAddressBookVanityLookupQueue() {
|
||
|
|
if (addressBookVanityLookupRunning) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
addressBookVanityLookupRunning = true;
|
||
|
|
try {
|
||
|
|
while (walletLoaded && addressBookVanityLookupQueue.size) {
|
||
|
|
const [address] = addressBookVanityLookupQueue;
|
||
|
|
addressBookVanityLookupQueue.delete(address);
|
||
|
|
|
||
|
|
await loadAddressBookCache();
|
||
|
|
const entry = addressBookCache.entries[address];
|
||
|
|
if (!entry || (entry.vanity && entry.vanity !== ADDRESS_BOOK_UNKNOWN)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const vanity = await lookupAddressBookVanity(address);
|
||
|
|
if (vanity && addressBookCache.entries[address]?.vanity !== vanity) {
|
||
|
|
addressBookCache.entries[address].vanity = vanity;
|
||
|
|
addressBookCache.entries[address].updated_at = currentUnixSeconds();
|
||
|
|
scheduleAddressBookWrite();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
} catch (_) {
|
||
|
|
// Vanity lookup is best-effort; failed lookups should not block address discovery.
|
||
|
|
}
|
||
|
|
|
||
|
|
if (addressBookVanityLookupQueue.size) {
|
||
|
|
await addressBookSleep(ADDRESS_BOOK_VANITY_DELAY_MS);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
addressBookVanityLookupRunning = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadAddressBookCache(force = false) {
|
||
|
|
if (!walletLoaded) {
|
||
|
|
addressBookCache = { entries: {} };
|
||
|
|
addressBookLoaded = false;
|
||
|
|
return addressBookCache;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (addressBookLoaded && !force) {
|
||
|
|
return addressBookCache;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (addressBookLoadingPromise && !force) {
|
||
|
|
return addressBookLoadingPromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
addressBookLoadingPromise = (async () => {
|
||
|
|
try {
|
||
|
|
const invoke = addressBookInvoke();
|
||
|
|
const raw = await invoke("read_address_book");
|
||
|
|
addressBookCache = normalizeAddressBook(JSON.parse(raw || "{}"));
|
||
|
|
} catch (_) {
|
||
|
|
addressBookCache = { entries: {} };
|
||
|
|
} finally {
|
||
|
|
addressBookLoaded = true;
|
||
|
|
addressBookLoadingPromise = null;
|
||
|
|
}
|
||
|
|
renderAddressBookPage();
|
||
|
|
return addressBookCache;
|
||
|
|
})();
|
||
|
|
|
||
|
|
return addressBookLoadingPromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
function scheduleAddressBookWrite() {
|
||
|
|
if (!walletLoaded || !addressBookLoaded) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
clearTimeout(addressBookWriteTimer);
|
||
|
|
addressBookWriteTimer = setTimeout(() => {
|
||
|
|
addressBookWritePromise = addressBookWritePromise
|
||
|
|
.catch(() => {})
|
||
|
|
.then(() => {
|
||
|
|
const invoke = addressBookInvoke();
|
||
|
|
return invoke("write_address_book", {
|
||
|
|
addressBookJson: JSON.stringify(normalizeAddressBook(addressBookCache))
|
||
|
|
});
|
||
|
|
})
|
||
|
|
.catch((error) => console.error("Address book write failed:", error));
|
||
|
|
}, ADDRESS_BOOK_WRITE_DELAY_MS);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function flushAddressBookWrite() {
|
||
|
|
clearTimeout(addressBookWriteTimer);
|
||
|
|
if (!walletLoaded || !addressBookLoaded) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
addressBookWritePromise = addressBookWritePromise
|
||
|
|
.catch(() => {})
|
||
|
|
.then(() => {
|
||
|
|
const invoke = addressBookInvoke();
|
||
|
|
return invoke("write_address_book", {
|
||
|
|
addressBookJson: JSON.stringify(normalizeAddressBook(addressBookCache))
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await addressBookWritePromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function upsertAddressBookEntry(address, options = {}) {
|
||
|
|
const canonical = canonicalAddressBookAddress(address);
|
||
|
|
if (shouldSkipAddressBookAddress(canonical)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
await loadAddressBookCache();
|
||
|
|
|
||
|
|
const now = currentUnixSeconds();
|
||
|
|
const existing = addressBookCache.entries[canonical];
|
||
|
|
const label = String(options.label || "").trim();
|
||
|
|
const labelSource = options.labelSource || "sync";
|
||
|
|
const vanity = String(options.vanity || "").trim();
|
||
|
|
let changed = false;
|
||
|
|
let shouldLookupVanity = false;
|
||
|
|
|
||
|
|
if (!existing) {
|
||
|
|
addressBookCache.entries[canonical] = {
|
||
|
|
label: label || ADDRESS_BOOK_UNKNOWN,
|
||
|
|
label_source: label ? labelSource : "sync",
|
||
|
|
vanity: vanity || ADDRESS_BOOK_UNKNOWN,
|
||
|
|
latest_txid: String(options.latestTxid || ""),
|
||
|
|
created_at: now,
|
||
|
|
updated_at: now
|
||
|
|
};
|
||
|
|
changed = true;
|
||
|
|
shouldLookupVanity = !vanity;
|
||
|
|
} else {
|
||
|
|
const canReplaceLabel =
|
||
|
|
label &&
|
||
|
|
existing.label_source !== "user" &&
|
||
|
|
(!existing.label || existing.label === ADDRESS_BOOK_UNKNOWN || existing.label_source === "sync");
|
||
|
|
|
||
|
|
if (canReplaceLabel && existing.label !== label) {
|
||
|
|
existing.label = label;
|
||
|
|
existing.label_source = labelSource;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.latestTxid && existing.latest_txid !== options.latestTxid) {
|
||
|
|
existing.latest_txid = String(options.latestTxid);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (vanity && existing.vanity !== vanity) {
|
||
|
|
existing.vanity = vanity;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (changed) {
|
||
|
|
existing.updated_at = now;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (changed) {
|
||
|
|
scheduleAddressBookWrite();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
if (shouldLookupVanity) {
|
||
|
|
enqueueAddressBookVanityLookup(canonical);
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function upsertVanityRegistrationAddressBookEntry(txid, transaction) {
|
||
|
|
const vanity = transaction?.unsigned_vanity_address;
|
||
|
|
if (!vanity) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const owner = canonicalAddressBookAddress(vanity.address);
|
||
|
|
const displayVanity = vanityAddressFromRawAddressHex(vanity.vanity_address);
|
||
|
|
if (!owner || !displayVanity) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return upsertAddressBookEntry(owner, {
|
||
|
|
vanity: displayVanity,
|
||
|
|
latestTxid: txid,
|
||
|
|
labelSource: "sync"
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectAddressBookAddressesFromValue(value, found = new Set()) {
|
||
|
|
if (!value || typeof value !== "object") {
|
||
|
|
if (typeof value === "string") {
|
||
|
|
const canonical = canonicalAddressBookAddress(value);
|
||
|
|
if (!shouldSkipAddressBookAddress(canonical)) {
|
||
|
|
found.add(canonical);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Array.isArray(value)) {
|
||
|
|
value.forEach((item) => collectAddressBookAddressesFromValue(item, found));
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.entries(value).forEach(([key, item]) => {
|
||
|
|
const keyText = String(key || "").toLowerCase();
|
||
|
|
if (["sender", "receiver", "creator", "advertiser", "sender1", "sender2", "lender", "borrower", "address"].includes(keyText)) {
|
||
|
|
const canonical = canonicalAddressBookAddress(item);
|
||
|
|
if (!shouldSkipAddressBookAddress(canonical)) {
|
||
|
|
found.add(canonical);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
collectAddressBookAddressesFromValue(item, found);
|
||
|
|
});
|
||
|
|
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function syncAddressBookFromTransactionRecord(txid, record) {
|
||
|
|
const vanityChanged = await upsertVanityRegistrationAddressBookEntry(txid, record?.transaction);
|
||
|
|
const found = collectAddressBookAddressesFromValue(record?.transaction);
|
||
|
|
if (!found.size) {
|
||
|
|
if (vanityChanged) {
|
||
|
|
await flushAddressBookWrite();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const address of found) {
|
||
|
|
await upsertAddressBookEntry(address, { latestTxid: txid, labelSource: "sync" });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function addPaymentUriAddressBookEntry(address, label) {
|
||
|
|
const changed = await upsertAddressBookEntry(address, {
|
||
|
|
label,
|
||
|
|
labelSource: "payment_uri"
|
||
|
|
});
|
||
|
|
if (changed) {
|
||
|
|
await flushAddressBookWrite();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetAddressBookPage() {
|
||
|
|
addressBookCache = { entries: {} };
|
||
|
|
addressBookLoaded = false;
|
||
|
|
addressBookLoadingPromise = null;
|
||
|
|
clearTimeout(addressBookWriteTimer);
|
||
|
|
addressBookVanityRefreshRunning = false;
|
||
|
|
addressBookVanityRefreshDoneForWallet = "";
|
||
|
|
addressBookVanityLookupRunning = false;
|
||
|
|
addressBookVanityLookupQueue.clear();
|
||
|
|
if (addressBookSearch) {
|
||
|
|
addressBookSearch.value = "";
|
||
|
|
}
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortedAddressBookEntries() {
|
||
|
|
const query = String(addressBookSearch?.value || "").trim().toLowerCase();
|
||
|
|
const sort = addressBookSort?.value || "latest";
|
||
|
|
let entries = Object.entries(addressBookCache.entries || {}).map(([address, entry]) => ({
|
||
|
|
address,
|
||
|
|
...entry
|
||
|
|
}));
|
||
|
|
|
||
|
|
if (query) {
|
||
|
|
entries = entries.filter((entry) => {
|
||
|
|
return [entry.address, entry.label, entry.vanity, entry.latest_txid]
|
||
|
|
.some((value) => String(value || "").toLowerCase().includes(query));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
entries.sort((left, right) => {
|
||
|
|
if (sort === "label") {
|
||
|
|
return String(left.label || "").localeCompare(String(right.label || ""));
|
||
|
|
}
|
||
|
|
if (sort === "vanity") {
|
||
|
|
return String(left.vanity || "").localeCompare(String(right.vanity || ""));
|
||
|
|
}
|
||
|
|
return Number(right.updated_at || 0) - Number(left.updated_at || 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderAddressBookPage() {
|
||
|
|
if (!addressBookRows) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entries = sortedAddressBookEntries();
|
||
|
|
const entryParts = entries.map((entry) => {
|
||
|
|
const txid = entry.latest_txid || "";
|
||
|
|
const label = entry.label && entry.label !== ADDRESS_BOOK_UNKNOWN ? entry.label : "";
|
||
|
|
const vanity = entry.vanity && entry.vanity !== ADDRESS_BOOK_UNKNOWN ? entry.vanity : "";
|
||
|
|
const escapedAddress = addressBookEscape(entry.address);
|
||
|
|
const escapedLabel = addressBookEscape(label);
|
||
|
|
const escapedVanity = addressBookEscape(vanity);
|
||
|
|
const escapedTxid = addressBookEscape(shortenAddress(txid));
|
||
|
|
return {
|
||
|
|
txid,
|
||
|
|
vanity,
|
||
|
|
escapedAddress,
|
||
|
|
escapedLabel,
|
||
|
|
escapedVanity,
|
||
|
|
escapedTxid
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
addressBookRows.innerHTML = entries.map((entry) => {
|
||
|
|
const txid = entry.latest_txid || "";
|
||
|
|
const label = entry.label && entry.label !== ADDRESS_BOOK_UNKNOWN ? entry.label : "";
|
||
|
|
const vanity = entry.vanity && entry.vanity !== ADDRESS_BOOK_UNKNOWN ? entry.vanity : "";
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td>
|
||
|
|
<input class="addressbook-label-input" data-address-book-label="${addressBookEscape(entry.address)}" value="${addressBookEscape(label)}" placeholder="${ADDRESS_BOOK_UNKNOWN}">
|
||
|
|
</td>
|
||
|
|
<td>${vanity ? `<button class="addressbook-copy-value" type="button" data-address-book-copy="${addressBookEscape(vanity)}">${addressBookEscape(vanity)}</button>` : ADDRESS_BOOK_UNKNOWN}</td>
|
||
|
|
<td><button class="addressbook-copy-value" type="button" data-address-book-copy="${addressBookEscape(entry.address)}"><code>${addressBookEscape(entry.address)}</code></button></td>
|
||
|
|
<td>${txid ? `<code>${addressBookEscape(shortenAddress(txid))}</code>` : "--"}</td>
|
||
|
|
<td class="addressbook-actions">
|
||
|
|
<button class="icon-button addressbook-delete-button" type="button" data-address-book-delete="${addressBookEscape(entry.address)}" title="Delete address" aria-label="Delete address">x</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}).join("");
|
||
|
|
|
||
|
|
if (addressBookCards) {
|
||
|
|
addressBookCards.innerHTML = entryParts.map((entry) => `
|
||
|
|
<article class="addressbook-card">
|
||
|
|
<div class="addressbook-card-top">
|
||
|
|
<input class="addressbook-label-input" data-address-book-label="${entry.escapedAddress}" value="${entry.escapedLabel}" placeholder="${ADDRESS_BOOK_UNKNOWN}" aria-label="Address label">
|
||
|
|
<button class="icon-button addressbook-delete-button" type="button" data-address-book-delete="${entry.escapedAddress}" title="Delete address" aria-label="Delete address">x</button>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span>Vanity</span>
|
||
|
|
<b>${entry.vanity ? `<button class="addressbook-copy-value" type="button" data-address-book-copy="${entry.escapedVanity}">${entry.escapedVanity}</button>` : ADDRESS_BOOK_UNKNOWN}</b>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span>Address</span>
|
||
|
|
<b><button class="addressbook-copy-value" type="button" data-address-book-copy="${entry.escapedAddress}"><code>${entry.escapedAddress}</code></button></b>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span>Last Transaction</span>
|
||
|
|
<b>${entry.txid ? `<code>${entry.escapedTxid}</code>` : "--"}</b>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
`).join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (addressBookState) {
|
||
|
|
addressBookState.textContent = walletLoaded
|
||
|
|
? `${entries.length} contact${entries.length === 1 ? "" : "s"}`
|
||
|
|
: "Load a wallet to view the address book.";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function prepareAddressBookPage() {
|
||
|
|
await loadAddressBookCache();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function refreshAddressBookVanitiesOnce(force = false) {
|
||
|
|
if (!walletLoaded || addressBookVanityRefreshRunning) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const walletKey = activeWalletAddressShort || activeWalletPath || "";
|
||
|
|
if (!force && addressBookVanityRefreshDoneForWallet === walletKey) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
addressBookVanityRefreshRunning = true;
|
||
|
|
addressBookVanityRefreshDoneForWallet = walletKey;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await loadAddressBookCache();
|
||
|
|
const addresses = Object.keys(addressBookCache.entries || {});
|
||
|
|
for (const address of addresses) {
|
||
|
|
if (!walletLoaded || (activeWalletAddressShort || activeWalletPath || "") !== walletKey) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const invoke = addressBookInvoke();
|
||
|
|
const vanity = await invoke("lookup_address_vanity", {
|
||
|
|
broadcastNode: activeBroadcastNode(),
|
||
|
|
address
|
||
|
|
});
|
||
|
|
const entry = addressBookCache.entries[address];
|
||
|
|
const nextVanity = vanity || ADDRESS_BOOK_UNKNOWN;
|
||
|
|
if (entry && entry.vanity !== nextVanity) {
|
||
|
|
entry.vanity = nextVanity;
|
||
|
|
entry.updated_at = currentUnixSeconds();
|
||
|
|
scheduleAddressBookWrite();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
} catch (_) {
|
||
|
|
// Vanity refresh is best-effort; connection errors should not block the wallet.
|
||
|
|
}
|
||
|
|
await addressBookSleep(ADDRESS_BOOK_VANITY_DELAY_MS);
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
addressBookVanityRefreshRunning = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
addressBookSearch?.addEventListener("input", renderAddressBookPage);
|
||
|
|
addressBookSort?.addEventListener("change", renderAddressBookPage);
|
||
|
|
addressBookRefreshVanity?.addEventListener("click", () => refreshAddressBookVanitiesOnce(true));
|
||
|
|
|
||
|
|
async function handleAddressBookLabelChange(event) {
|
||
|
|
const input = event.target.closest("[data-address-book-label]");
|
||
|
|
if (!input) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const address = canonicalAddressBookAddress(input.dataset.addressBookLabel);
|
||
|
|
if (!address) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await loadAddressBookCache();
|
||
|
|
const entry = addressBookCache.entries[address];
|
||
|
|
if (!entry) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
entry.label = input.value.trim() || ADDRESS_BOOK_UNKNOWN;
|
||
|
|
entry.label_source = "user";
|
||
|
|
entry.updated_at = currentUnixSeconds();
|
||
|
|
scheduleAddressBookWrite();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleAddressBookClick(event) {
|
||
|
|
const copyButton = event.target.closest("[data-address-book-copy]");
|
||
|
|
if (copyButton) {
|
||
|
|
const value = copyButton.dataset.addressBookCopy || "";
|
||
|
|
const copied = typeof writeClipboardText === "function"
|
||
|
|
? await writeClipboardText(value)
|
||
|
|
: false;
|
||
|
|
if (copied) {
|
||
|
|
const previous = copyButton.textContent;
|
||
|
|
copyButton.textContent = "Copied";
|
||
|
|
setTimeout(() => {
|
||
|
|
if (copyButton.isConnected) {
|
||
|
|
copyButton.textContent = previous;
|
||
|
|
}
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const button = event.target.closest("[data-address-book-delete]");
|
||
|
|
if (!button) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const address = canonicalAddressBookAddress(button.dataset.addressBookDelete);
|
||
|
|
if (!address) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await loadAddressBookCache();
|
||
|
|
if (!addressBookCache.entries[address]) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
delete addressBookCache.entries[address];
|
||
|
|
scheduleAddressBookWrite();
|
||
|
|
renderAddressBookPage();
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleAddressBookKeydown(event) {
|
||
|
|
const input = event.target.closest("[data-address-book-label]");
|
||
|
|
if (input && event.key === "Enter") {
|
||
|
|
event.preventDefault();
|
||
|
|
input.blur();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[addressBookRows, addressBookCards].forEach((container) => {
|
||
|
|
container?.addEventListener("change", handleAddressBookLabelChange);
|
||
|
|
container?.addEventListener("click", handleAddressBookClick);
|
||
|
|
container?.addEventListener("keydown", handleAddressBookKeydown);
|
||
|
|
});
|