import type { WalletStatus } from "./types";
import QRCode from "qrcode";
import { core } from "./core";

const byId = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
const screens = Array.from(document.querySelectorAll<HTMLElement>(".screen"));
const notice = byId<HTMLParagraphElement>("notice");
const firefoxToolbarPopup = navigator.userAgent.includes("Firefox");

let pendingPassword = "";
let currentPassword = "";
let selectedImageBase64 = "";
let backupImageBase64 = "";
let walletStatus: WalletStatus | undefined;
let pendingBackupAction: "image" | "private" | "print" | undefined;
let tokenFilter: "held" | "all" = "held";
let nftPage = 1;
let nftLoadId = 0;
const NFT_PAGE_SIZE = 8;
let walletBalances:
  | {
      balances: Array<{
        asset: string;
        nft_series: number;
        balance: string;
      }>;
      tokens: Array<{
        token: string;
        origin_txid: string;
      }>;
      nfts: Array<{
        name: string;
        series: number;
        origin_txid: string;
        ownership_type: number;
        supply: string;
      }>;
      tokenCatalogError?: string;
      nftCatalogError?: string;
    }
  | undefined;

type ActivityRecord = {
  txid: string;
  block_height: number;
  transaction_hex: string;
  miner_earnings?: Array<{
    type: string;
    asset: string;
    nft_series: number;
    amount_atomic: string;
  }>;
};

type OwnedNft = {
  name: string;
  series: number;
  origin_txid: string;
  ownership_type: number;
  supply: string;
  balance: string;
  details?: NftDetails;
  loading?: boolean;
  error?: string;
};

type NftDetails = {
  nft_name: string;
  series: number;
  asset_name: string;
  genesis_txid: string;
  creator: string;
  item_ipfs: string;
  metadata_uri: string;
  current_holder?: string | null;
  history_count: number;
  history: Array<{
    txid: string;
    block: number;
    transaction_type: number;
    action: string;
    from?: string | null;
    to?: string | null;
    received_asset?: string;
    received_series?: number;
    received_value?: string;
  }>;
  metadata: Record<string, unknown>;
  image_data_url: string;
  image_available: boolean;
  media_error?: string;
};

let ownedNfts: OwnedNft[] = [];
let selectedNft: OwnedNft | undefined;
let sendNft: OwnedNft | undefined;

type DecodedTransaction = {
  txtype: number;
  transaction_name: string;
  fields: Record<string, unknown>;
  stored_hash?: string | null;
  signature?: string | null;
  signature1?: string | null;
  signature2?: string | null;
};

let selectedTransaction: ActivityRecord | undefined;
let transactionReturnScreen: "activity" | "nft" = "activity";
let pendingTransfer:
  | {
      transaction: Record<string, unknown>;
      receiverInput: string;
      asset: string;
      amount: string;
      fee: string;
      total: string;
    }
  | undefined;
let lastSentTxid = "";
let pendingProviderApprovalId = "";
const CONFIRMED_AFTER_BLOCKS = 20;

const TRANSACTION_NAMES: Record<number, string> = {
  1: "Mining Reward",
  2: "Transfer",
  3: "Token Creation",
  4: "NFT/RWA Creation",
  5: "Marketing",
  6: "Swap",
  7: "Loan",
  8: "Loan Payment",
  9: "Collateral Claim",
  10: "Asset Burn",
  11: "Token Issuance",
  12: "Vanity Address",
  100: "Storage Key",
  101: "Data Storage Bool",
  102: "Data Storage U8",
  103: "Data Storage U16",
  104: "Data Storage U32",
  105: "Data Storage U64",
  106: "Data Storage U128",
  107: "Data Storage String",
  108: "Data Storage I8",
  109: "Data Storage I16",
  110: "Data Storage I32",
  111: "Data Storage I64",
  112: "Data Storage I128",
  113: "Delete Stored Data",
  200: "Proposal",
  201: "Proposal Vote",
  202: "Activation Vote"
};

async function send(type: string, values: Record<string, unknown> = {}) {
  const response = await chrome.runtime.sendMessage({ type, ...values });
  if (!response?.ok) throw new Error(response?.error ?? "Wallet request failed.");
  return response.result;
}

function showScreen(id: string, clearNotice = true): void {
  screens.forEach((screen) => {
    screen.hidden = screen.id !== id;
  });
  if (clearNotice) notice.textContent = "";
}

function showStatus(status: WalletStatus): void {
  walletStatus = status;
  const networkName = status.networkName;
  const networkSymbol = status.networkSymbol;
  document.querySelectorAll<HTMLElement>("[data-network-name]").forEach((element) => {
    element.textContent = networkName;
  });
  document.querySelectorAll<HTMLElement>("[data-network-symbol]").forEach((element) => {
    element.textContent = networkSymbol;
  });
  byId("base-asset-name").textContent = `Contractless ${networkName}`;
  byId("wallet-address").textContent = status.address ?? "";
  byId("wallet-address-short").textContent = status.address
    ? `${status.address.slice(0, 6)}...${status.address.slice(-6)}`
    : "Account";
  byId<HTMLInputElement>("settings-testnet-api-url").value = status.testnetApiUrl;
  byId<HTMLInputElement>("settings-mainnet-api-url").value = status.mainnetApiUrl;
  document.querySelectorAll<HTMLButtonElement>("[data-select-network]").forEach((button) => {
    const active = button.dataset.selectNetwork === status.selectedNetwork;
    button.classList.toggle("active", active);
    button.setAttribute("aria-pressed", String(active));
  });
  byId<HTMLSelectElement>("settings-lock-timeout").value = String(status.autoLockMinutes);
  renderKnownAddresses(status);
  if (!status.exists) {
    showScreen("create-screen");
  } else if (!status.unlocked) {
    showScreen("locked-screen");
  } else {
    void showPendingProviderApproval().then((shown) => {
      if (!shown) {
        showScreen("dashboard-screen");
        void loadWalletBalances();
      }
    });
  }
}

function approvalTitle(method: string, display: Record<string, unknown>): string {
  if (method === "contractless_connect") return "Connect application";
  if (method === "contractless_signMessage") return "Sign text message";
  if (
    method === "contractless_sendTransaction"
    || method === "contractless_signDualTransaction"
  ) {
    return `Sign ${String(display.transaction_type ?? "transaction")}`;
  }
  return "Review request";
}

function appendApprovalValue(
  container: HTMLElement,
  label: string,
  value: unknown
): void {
  const term = document.createElement("dt");
  term.textContent = label.replaceAll("_", " ");
  const description = document.createElement("dd");
  description.textContent = typeof value === "object"
    ? JSON.stringify(value, null, 2)
    : String(value);
  container.append(term, description);
}

function swapApprovalAmount(
  fields: Record<string, unknown>,
  valueField: "value1" | "value2" | "tip1" | "tip2",
  tickerField: "ticker1" | "ticker2",
  seriesField: "nft_series1" | "nft_series2"
): string {
  const amount = formatAtomicDisplay(fields[valueField]);
  const ticker = String(fields[tickerField] ?? "").trim().toUpperCase();
  const series = Number(fields[seriesField] ?? 0);
  return `${amount} ${ticker}${series > 0 ? ` #${series}` : ""}`;
}

function appendSwapApprovalCosts(
  container: HTMLElement,
  fields: Record<string, unknown>,
  signerSlot: 1 | 2
): void {
  const otherSlot = signerSlot === 1 ? 2 : 1;
  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  appendApprovalValue(
    container,
    "Your miner tip",
    swapApprovalAmount(
      fields,
      `tip${signerSlot}`,
      `ticker${signerSlot}`,
      `nft_series${signerSlot}`
    )
  );
  appendApprovalValue(
    container,
    "Your transaction fee",
    `${formatAtomicDisplay(fields[`txfee${signerSlot}`])} ${baseSymbol}`
  );
  appendApprovalValue(
    container,
    "Other party miner tip",
    swapApprovalAmount(
      fields,
      `tip${otherSlot}`,
      `ticker${otherSlot}`,
      `nft_series${otherSlot}`
    )
  );
  appendApprovalValue(
    container,
    "Other party transaction fee",
    `${formatAtomicDisplay(fields[`txfee${otherSlot}`])} ${baseSymbol}`
  );
}

function appendSwapApprovalSummary(
  container: HTMLElement,
  fields: Record<string, unknown>
): void {
  const activeAddress = walletStatus?.address?.trim().toLowerCase() ?? "";
  const sender1 = String(fields.sender1 ?? "").trim().toLowerCase();
  const sender2 = String(fields.sender2 ?? "").trim().toLowerCase();
  const signerSlot = activeAddress === sender1 ? 1 : activeAddress === sender2 ? 2 : 0;

  if (signerSlot === 1) {
    appendApprovalValue(
      container,
      "Sending",
      swapApprovalAmount(fields, "value1", "ticker1", "nft_series1")
    );
    appendApprovalValue(
      container,
      "Receiving",
      swapApprovalAmount(fields, "value2", "ticker2", "nft_series2")
    );
    appendSwapApprovalCosts(container, fields, 1);
    return;
  }

  if (signerSlot === 2) {
    appendApprovalValue(
      container,
      "Sending",
      swapApprovalAmount(fields, "value2", "ticker2", "nft_series2")
    );
    appendApprovalValue(
      container,
      "Receiving",
      swapApprovalAmount(fields, "value1", "ticker1", "nft_series1")
    );
    appendSwapApprovalCosts(container, fields, 2);
    return;
  }

  appendApprovalValue(
    container,
    "First party sending",
    swapApprovalAmount(fields, "value1", "ticker1", "nft_series1")
  );
  appendApprovalValue(
    container,
    "Second party sending",
    swapApprovalAmount(fields, "value2", "ticker2", "nft_series2")
  );
}

function setApprovalProcessing(action: "approve" | "reject" | null): void {
  const approve = byId<HTMLButtonElement>("approve-provider-approval");
  const reject = byId<HTMLButtonElement>("reject-provider-approval");
  const processing = action !== null;
  approve.disabled = processing;
  reject.disabled = processing;
  approve.classList.toggle("is-processing", action === "approve");
  reject.classList.toggle("is-processing", action === "reject");
  approve.textContent = action === "approve" ? "Processing..." : "Approve";
  reject.textContent = action === "reject" ? "Rejecting..." : "Reject";
}

async function showPendingProviderApproval(): Promise<boolean> {
  const pending = await send("pending-provider-approval") as {
    locked?: boolean;
    position?: number;
    total?: number;
    request?: {
      id: string;
      method: string;
      origin: string;
      risk: "safe" | "low" | "high";
      display?: Record<string, unknown>;
    };
  } | null;
  if (!pending?.request) return false;

  const request = pending.request;
  setApprovalProcessing(null);
  pendingProviderApprovalId = request.id;
  const queueLabel = (pending.total ?? 0) > 1
    ? ` · Request ${pending.position ?? 1} of ${pending.total}`
    : "";
  byId("provider-approval-origin").textContent = `${request.origin}${queueLabel}`;
  const risk = byId("provider-approval-risk");
  risk.className = `risk-label risk-${request.risk}`;
  risk.textContent = request.risk === "safe"
    ? "Safe"
    : request.risk === "low"
      ? "Low risk"
      : "High risk";
  const display = request.display ?? {};
  const transactionRequest = request.method === "contractless_sendTransaction"
    || request.method === "contractless_signDualTransaction";
  byId("provider-approval-title").textContent = approvalTitle(request.method, display);
  byId("provider-approval-intro").textContent = transactionRequest
    ? "Review every transaction field before signing."
    : "Review this request before approving it.";
  const details = byId("provider-approval-details");
  details.replaceChildren();
  Object.entries(display).forEach(([label, value]) => {
    if (label === "transaction_fields" && value && typeof value === "object") {
      const fields = value as Record<string, unknown>;
      const txtype = Number(fields.txtype ?? 0);
      const swapAmountFields = new Set([
        "ticker1",
        "nft_series1",
        "value1",
        "ticker2",
        "nft_series2",
        "value2",
        "tip1",
        "tip2",
        "txfee1",
        "txfee2"
      ]);
      if (txtype === 6) appendSwapApprovalSummary(details, fields);
      Object.entries(fields).forEach(([field, fieldValue]) => {
        if (txtype === 6 && swapAmountFields.has(field)) return;
        appendApprovalValue(
          details,
          field === "txtype" ? "transaction_type_id" : field,
          transactionFieldDisplay(txtype, field, fieldValue)
        );
      });
      return;
    }
    appendApprovalValue(details, label, value);
  });
  showScreen("provider-approval-screen");
  return true;
}

async function showNextProviderApproval(
  remaining: number,
  waitForFollowup: boolean
): Promise<void> {
  if (remaining > 0 && await showPendingProviderApproval()) return;

  if (waitForFollowup) {
    const expiresAt = Date.now() + 1_500;
    while (Date.now() < expiresAt) {
      if (await showPendingProviderApproval()) return;
      await new Promise((resolve) => setTimeout(resolve, 75));
    }
  }

  window.close();
}

function decimalHasValue(value: string): boolean {
  return value.replace(/[.0]/g, "").length > 0;
}

function renderTokenBalances(): void {
  const list = byId("token-list");
  list.replaceChildren();
  if (!walletBalances) {
    const empty = document.createElement("div");
    empty.className = "empty-assets";
    const title = document.createElement("strong");
    title.textContent = "Loading token balances...";
    const description = document.createElement("span");
    description.textContent = "Balances are retrieved from the selected Contractless API.";
    empty.append(title, description);
    list.append(empty);
    return;
  }

  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  const tokenNames = new Set(
    walletBalances.tokens.map((token) => token.token.trim().toUpperCase())
  );
  const nftKeys = new Set(
    walletBalances.nfts.map(
      (nft) => `${nft.name.trim().toUpperCase()}|${Number(nft.series || 0)}`
    )
  );
  const fungibleBalances = walletBalances.balances.filter((balance) => {
    const asset = balance.asset.trim().toUpperCase();
    const key = `${asset}|${Number(balance.nft_series || 0)}`;
    return (
      balance.nft_series === 0
      && asset !== "CLTC"
      && asset !== "CLC"
      && tokenNames.has(asset)
      && !nftKeys.has(key)
    );
  });
  const balancesByAsset = new Map(
    fungibleBalances.map((balance) => [balance.asset.trim().toUpperCase(), balance])
  );

  const displayedTokenNames = tokenFilter === "all"
    ? walletBalances.tokens.map((token) => token.token)
    : fungibleBalances
        .filter((balance) => decimalHasValue(balance.balance))
        .map((balance) => balance.asset);
  const uniqueNames = [...new Set(displayedTokenNames.map((name) => name.trim().toUpperCase()))]
    .filter((name) => name !== baseSymbol && !nftKeys.has(`${name}|0`))
    .sort((left, right) => left.localeCompare(right));

  if (uniqueNames.length === 0) {
    const empty = document.createElement("div");
    empty.className = "empty-assets";
    const title = document.createElement("strong");
    title.textContent = tokenFilter === "held"
      ? "No token balances found"
      : "No tokens found";
    const description = document.createElement("span");
    description.textContent = tokenFilter === "held"
      ? "Switch to All tokens to view the complete network token list."
      : walletBalances.tokenCatalogError
        ? `Token catalog unavailable: ${walletBalances.tokenCatalogError}`
        : "The selected API did not return any fungible tokens.";
    empty.append(title, description);
    list.append(empty);
    return;
  }

  uniqueNames.forEach((name) => {
    const balance = balancesByAsset.get(name);
    const row = document.createElement("div");
    row.className = "asset-row";

    const symbol = document.createElement("div");
    symbol.className = "asset-symbol token-symbol";
    symbol.textContent = name.slice(0, 3);

    const identity = document.createElement("div");
    identity.className = "asset-name";
    const title = document.createElement("strong");
    title.textContent = name;
    const subtitle = document.createElement("span");
    subtitle.textContent = "Contractless token";
    identity.append(title, subtitle);

    const value = document.createElement("div");
    value.className = "asset-value";
    const amount = document.createElement("strong");
    amount.textContent = balance?.balance ?? "0.00000000";
    const ticker = document.createElement("span");
    ticker.textContent = name;
    value.append(amount, ticker);

    row.append(symbol, identity, value);
    list.append(row);
  });
}

async function loadWalletBalances(): Promise<void> {
  if (!walletStatus?.unlocked) return;
  walletBalances = undefined;
  byId("base-balance").textContent = "--";
  byId("base-asset-balance").textContent = "--";
  renderTokenBalances();
  try {
    if (!walletStatus.apiUrl) {
      throw new Error(`No ${walletStatus.networkName} API is configured.`);
    }
    const result = await send("wallet-balances") as {
      balances: Array<{ asset: string; nft_series: number; balance: string }>;
      tokens: Array<{ token: string; origin_txid: string }>;
      nfts: Array<{
        name: string;
        series: number;
        origin_txid: string;
        ownership_type: number;
        supply: string;
      }>;
      tokenCatalogError?: string;
      nftCatalogError?: string;
    };
    walletBalances = {
      balances: Array.isArray(result.balances) ? result.balances : [],
      tokens: Array.isArray(result.tokens) ? result.tokens : [],
      nfts: Array.isArray(result.nfts) ? result.nfts : [],
      tokenCatalogError: result.tokenCatalogError,
      nftCatalogError: result.nftCatalogError
    };
    const baseSymbol = walletStatus.networkSymbol;
    const base = walletBalances.balances.find(
      (balance) =>
        balance.nft_series === 0 && balance.asset.toUpperCase() === baseSymbol
    );
    const baseBalance = base?.balance ?? "0.00000000";
    byId("base-balance").textContent = baseBalance;
    byId("base-asset-balance").textContent = baseBalance;
    renderTokenBalances();
  } catch (error) {
    const list = byId("token-list");
    list.replaceChildren();
    const empty = document.createElement("div");
    empty.className = "empty-assets balance-error";
    const title = document.createElement("strong");
    title.textContent = "Unable to load balances";
    const description = document.createElement("span");
    description.textContent = errorText(error);
    empty.append(title, description);
    list.append(empty);
    notice.textContent = errorText(error);
  }
}

function nftKey(name: string, series: number): string {
  return `${name.trim().toUpperCase()}|${series}`;
}

function positiveDecimal(value: string): boolean {
  try {
    return decimalToAtomic(value, "Balance") > 0n;
  } catch {
    return false;
  }
}

function nftOwnership(nft: OwnedNft): string {
  if (nft.ownership_type !== 1) return "Complete ownership";
  try {
    const balance = decimalToAtomic(nft.balance, "Balance");
    const supply = decimalToAtomic(nft.supply, "Supply");
    if (supply <= 0n) return "0% ownership";
    const scaled = balance * 100_000_000n / supply;
    const whole = scaled / 1_000_000n;
    const fraction = (scaled % 1_000_000n)
      .toString()
      .padStart(6, "0")
      .replace(/0+$/, "");
    return `${whole}${fraction ? `.${fraction}` : ""}% ownership`;
  } catch {
    return "Fractional ownership";
  }
}

function nftMetadata(nft: OwnedNft): Record<string, unknown> {
  return nft.details?.metadata && typeof nft.details.metadata === "object"
    ? nft.details.metadata
    : {};
}

function nftDisplayName(nft: OwnedNft): string {
  const metadataName = nftMetadata(nft).name;
  if (typeof metadataName === "string" && metadataName.trim()) {
    return metadataName.trim();
  }
  return nft.series > 0 ? `${nft.name} #${nft.series}` : nft.name;
}

function nftKind(nft: OwnedNft): "NFT" | "RWA" {
  const metadata = nftMetadata(nft);
  const contractless = metadata.contractless;
  if (
    contractless
    && typeof contractless === "object"
    && String((contractless as Record<string, unknown>).metadata_type ?? "")
      .toLowerCase() === "rwa"
  ) {
    return "RWA";
  }
  return "NFT";
}

function nftAssetLabel(nft: OwnedNft): string {
  return nft.series > 0 ? `${nft.name}_${nft.series}` : nft.name;
}

function shortenNftValue(value: string, start = 7, end = 7): string {
  return value.length > start + end + 3
    ? `${value.slice(0, start)}...${value.slice(-end)}`
    : value;
}

function createNftMedia(nft: OwnedNft, className: string): HTMLElement {
  const media = document.createElement("div");
  media.className = className;
  if (nft.details?.image_data_url) {
    const image = document.createElement("img");
    image.src = nft.details.image_data_url;
    image.alt = nftDisplayName(nft);
    media.append(image);
    return media;
  }
  const fallback = document.createElement("span");
  fallback.textContent = nft.loading ? "..." : nft.name.slice(0, 2).toUpperCase();
  media.append(fallback);
  return media;
}

function visibleNfts(): OwnedNft[] {
  const start = (nftPage - 1) * NFT_PAGE_SIZE;
  return ownedNfts.slice(start, start + NFT_PAGE_SIZE);
}

function renderNftGallery(): void {
  const list = byId("nfts-list");
  list.replaceChildren();
  const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE));
  nftPage = Math.min(Math.max(1, nftPage), totalPages);
  byId("nfts-count").textContent = ownedNfts.length === 1
    ? "1 owned asset"
    : `${ownedNfts.length} owned assets`;
  byId("nfts-page").textContent = String(nftPage);
  byId("nfts-pages").textContent = String(totalPages);
  byId("nfts-pagination").hidden = totalPages <= 1;
  byId<HTMLButtonElement>("nfts-previous").disabled = nftPage <= 1;
  byId<HTMLButtonElement>("nfts-next").disabled = nftPage >= totalPages;

  if (!ownedNfts.length) {
    const empty = document.createElement("div");
    empty.className = "nfts-empty";
    const title = document.createElement("strong");
    title.textContent = "No NFTs or RWAs found";
    const copy = document.createElement("span");
    copy.textContent = walletBalances?.nftCatalogError
      ? `NFT catalog unavailable: ${walletBalances.nftCatalogError}`
      : "Assets owned by this wallet will appear here.";
    empty.append(title, copy);
    list.append(empty);
    return;
  }

  visibleNfts().forEach((nft) => {
    const card = document.createElement("button");
    card.type = "button";
    card.className = "nft-card";
    card.append(createNftMedia(nft, "nft-card-media"));

    const identity = document.createElement("div");
    identity.className = "nft-card-identity";
    const kind = document.createElement("span");
    kind.textContent = nftKind(nft);
    const name = document.createElement("strong");
    name.textContent = nftDisplayName(nft);
    const asset = document.createElement("small");
    asset.textContent = nftAssetLabel(nft);
    const ownership = document.createElement("b");
    ownership.textContent = nftOwnership(nft);
    identity.append(kind, name, asset, ownership);
    if (nft.error) {
      const error = document.createElement("em");
      error.textContent = "Media unavailable";
      identity.append(error);
    }
    card.append(identity);
    card.addEventListener("click", () => openNftDetails(nft));
    list.append(card);
  });
}

function appendNftDetailField(
  container: HTMLElement,
  label: string,
  value: string
): void {
  const row = document.createElement("div");
  const term = document.createElement("dt");
  term.textContent = label;
  const description = document.createElement("dd");
  description.textContent = value || "--";
  row.append(term, description);
  container.append(row);
}

function renderNftDetails(nft: OwnedNft): void {
  selectedNft = nft;
  const details = nft.details;
  const metadata = nftMetadata(nft);
  const kind = nftKind(nft);
  const name = nftDisplayName(nft);
  byId("nft-detail-title").textContent = name;
  byId("nft-detail-kind").textContent = `${kind} details`;
  byId("nft-detail-badge").textContent = kind;
  byId("nft-detail-name").textContent = name;
  byId("nft-detail-ownership").textContent = nftOwnership(nft);

  const image = byId<HTMLImageElement>("nft-detail-image");
  image.src = details?.image_data_url ?? "";
  image.alt = name;
  image.hidden = !details?.image_data_url;

  const description = metadata.description ?? metadata.summary;
  byId("nft-detail-description").textContent =
    typeof description === "string" && description.trim()
      ? description.trim()
      : details
        ? "No description was included in the NFT metadata."
        : "Loading NFT metadata and provenance...";

  const attributes = byId("nft-detail-attributes");
  attributes.replaceChildren();
  const metadataAttributes = Array.isArray(metadata.attributes)
    ? metadata.attributes.slice(0, 12)
    : [];
  metadataAttributes.forEach((attribute) => {
    if (!attribute || typeof attribute !== "object") return;
    const values = attribute as Record<string, unknown>;
    const item = document.createElement("div");
    const label = document.createElement("span");
    label.textContent = String(values.trait_type ?? values.type ?? "Attribute");
    const value = document.createElement("strong");
    value.textContent = String(values.value ?? "");
    item.append(label, value);
    attributes.append(item);
  });
  attributes.hidden = attributes.childElementCount === 0;

  const fields = byId("nft-detail-fields");
  fields.replaceChildren();
  appendNftDetailField(fields, "Asset", nftAssetLabel(nft));
  appendNftDetailField(fields, "Creator", details?.creator ?? "--");
  appendNftDetailField(fields, "IPFS CID", details?.item_ipfs ?? "--");
  appendNftDetailField(fields, "Origin transaction", details?.genesis_txid ?? nft.origin_txid);

  const history = byId("nft-detail-history");
  history.replaceChildren();
  if (!details) {
    const loading = document.createElement("p");
    loading.textContent = "Loading provenance...";
    history.append(loading);
  } else if (!details.history.length) {
    const empty = document.createElement("p");
    empty.textContent = "No provenance records were returned.";
    history.append(empty);
  } else {
    details.history.slice().reverse().forEach((entry) => {
      const row = document.createElement("button");
      row.type = "button";
      row.className = "nft-history-row";
      row.title = `View full transaction ${entry.txid}`;
      const action = document.createElement("strong");
      action.textContent = entry.action;
      const block = document.createElement("span");
      block.textContent = `Block ${Number(entry.block).toLocaleString()}`;
      const transaction = document.createElement("small");
      transaction.textContent = shortenNftValue(entry.txid);
      row.append(action, block, transaction);
      row.addEventListener("click", () => {
        void openTransaction(
          {
            txid: entry.txid,
            block_height: Number(entry.block),
            transaction_hex: ""
          },
          "nft"
        );
      });
      history.append(row);
    });
  }
}

function openNftDetails(nft: OwnedNft): void {
  renderNftDetails(nft);
  showScreen("nft-detail-screen");
}

async function hydrateVisibleNfts(loadId: number): Promise<void> {
  const queue = visibleNfts().filter((nft) => !nft.details && !nft.loading);
  let cursor = 0;
  const worker = async (): Promise<void> => {
    while (cursor < queue.length && loadId === nftLoadId) {
      const nft = queue[cursor++];
      nft.loading = true;
      renderNftGallery();
      try {
        nft.details = await send("wallet-nft-details", {
          name: nft.name,
          series: nft.series
        }) as NftDetails;
        nft.error = nft.details.media_error;
      } catch (error) {
        nft.error = errorText(error);
      } finally {
        nft.loading = false;
      }
      if (loadId !== nftLoadId) return;
      renderNftGallery();
      if (selectedNft === nft) renderNftDetails(nft);
    }
  };
  await Promise.all([worker(), worker()]);
}

async function loadOwnedNfts(force = false): Promise<void> {
  const loadId = ++nftLoadId;
  byId("nfts-count").textContent = "Loading ownership...";
  if (!walletBalances || force) {
    await loadWalletBalances();
  }
  if (loadId !== nftLoadId || !walletBalances) return;

  const previous = force
    ? new Map<string, OwnedNft>()
    : new Map(ownedNfts.map((nft) => [nftKey(nft.name, nft.series), nft]));
  ownedNfts = walletBalances.nfts
    .map((catalog): OwnedNft | null => {
      const balance = walletBalances?.balances.find(
        (entry) =>
          nftKey(entry.asset, Number(entry.nft_series || 0))
          === nftKey(catalog.name, Number(catalog.series || 0))
      );
      if (!balance || !positiveDecimal(balance.balance)) return null;
      const existing = previous.get(nftKey(catalog.name, catalog.series));
      return {
        name: catalog.name.trim(),
        series: Number(catalog.series || 0),
        origin_txid: catalog.origin_txid,
        ownership_type: Number(catalog.ownership_type || 0),
        supply: catalog.supply,
        balance: balance.balance,
        details: existing?.details,
        error: existing?.error
      };
    })
    .filter((nft): nft is OwnedNft => nft !== null)
    .sort((left, right) =>
      left.name.localeCompare(right.name) || left.series - right.series
    );
  const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE));
  nftPage = Math.min(nftPage, totalPages);
  renderNftGallery();
  await hydrateVisibleNfts(loadId);
}

function renderKnownAddresses(status: WalletStatus): void {
  const container = byId("known-addresses");
  container.replaceChildren();

  status.wallets.forEach((wallet) => {
    const row = document.createElement("div");
    row.className = `known-address${wallet.active ? " active-address" : ""}`;

    const select = document.createElement("button");
    select.type = "button";
    select.className = "address-select";
    select.dataset.walletId = wallet.id;

    const avatar = document.createElement("span");
    avatar.className = "address-avatar";
    const logo = document.createElement("img");
    logo.src = chrome.runtime.getURL("contractless-logo.png");
    logo.alt = "";
    avatar.append(logo);

    const details = document.createElement("span");
    details.className = "address-details";
    const label = document.createElement("strong");
    label.textContent = wallet.label;
    const address = document.createElement("small");
    address.textContent = wallet.address;
    details.append(label, address);

    const selected = document.createElement("span");
    selected.className = "selected-mark";
    selected.textContent = wallet.active ? "✓" : "";
    select.append(avatar, details, selected);

    select.addEventListener("click", () => {
      if (wallet.active) {
        setAccountDrawer(false);
        return;
      }
      void action(async () => {
        setAccountDrawer(false);
        currentPassword = "";
        backupImageBase64 = "";
        showStatus(await send("switch-wallet", { walletId: wallet.id }) as WalletStatus);
      });
    });

    const rename = document.createElement("button");
    rename.type = "button";
    rename.className = "rename-address";
    rename.title = "Rename wallet";
    rename.setAttribute("aria-label", `Rename ${wallet.label}`);
    rename.textContent = "✎";
    rename.addEventListener("click", () => {
      const nextLabel = window.prompt("Wallet label", wallet.label);
      if (nextLabel === null || nextLabel.trim() === wallet.label) return;
      void action(async () => {
        const nextStatus = await send("rename-wallet", {
          walletId: wallet.id,
          label: nextLabel
        }) as WalletStatus;
        showStatus(nextStatus);
        setAccountDrawer(true);
      });
    });

    row.append(select, rename);
    container.append(row);
  });
}

function setAccountDrawer(open: boolean): void {
  const drawer = byId("account-drawer");
  const backdrop = byId("drawer-backdrop");
  const trigger = byId("account-menu-button");
  drawer.classList.toggle("open", open);
  drawer.setAttribute("aria-hidden", String(!open));
  trigger.setAttribute("aria-expanded", String(open));
  backdrop.hidden = !open;
}

function clearRevealedPrivateKey(): void {
  byId<HTMLTextAreaElement>("revealed-private-key").value = "";
  byId("private-key-result").hidden = true;
}

function setBackupKeyModal(open: boolean): void {
  byId("backup-key-modal").hidden = !open;
  byId("backup-key-backdrop").hidden = !open;
  if (open) {
    const input = byId<HTMLInputElement>("backup-key-input");
    input.value = "";
    input.focus();
  } else {
    byId<HTMLInputElement>("backup-key-input").value = "";
    pendingBackupAction = undefined;
  }
}

async function openReceiveScreen(): Promise<void> {
  const result = await send("wallet-receive-address");
  const address = String(result.receiveAddress ?? "");
  if (!address) throw new Error("No active wallet address is available.");
  byId("receive-address-label").textContent = result.vanity
    ? "Vanity address"
    : "Wallet address";
  byId("receive-address-value").textContent = address;
  const canvas = byId<HTMLCanvasElement>("receive-qr");
  await QRCode.toCanvas(canvas, address, {
    width: 220,
    margin: 1,
    errorCorrectionLevel: "M",
    color: {
      dark: "#071016",
      light: "#ffffff"
    }
  });
  showScreen("receive-screen");
}

function decimalToAtomic(value: string, label: string): bigint {
  const normalized = value.trim();
  if (!/^(?:0|[1-9]\d*)(?:\.\d{1,8})?$/.test(normalized)) {
    throw new Error(`${label} must be a positive number with no more than 8 decimal places.`);
  }
  const [whole, fraction = ""] = normalized.split(".");
  return BigInt(whole) * 100_000_000n + BigInt(fraction.padEnd(8, "0"));
}

function atomicToDecimal(value: bigint): string {
  const whole = value / 100_000_000n;
  const fraction = (value % 100_000_000n).toString().padStart(8, "0");
  return `${whole}.${fraction}`;
}

function selectedSendBalance(): string {
  if (sendNft) return sendNft.balance;
  const asset = byId<HTMLSelectElement>("send-asset").value;
  const balance = walletBalances?.balances.find(
    (entry) => entry.nft_series === 0 && entry.asset.trim().toUpperCase() === asset
  );
  return balance?.balance ?? "0.00000000";
}

function minimumSendFeeAtomic(): bigint {
  const asset = byId<HTMLSelectElement>("send-asset").value;
  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  if (asset !== baseSymbol) return 100_000_000n;
  const amount = byId<HTMLInputElement>("send-amount").value.trim();
  if (!amount) return 0n;
  try {
    const atomic = decimalToAtomic(amount, "Amount");
    return (atomic + 99n) / 100n;
  } catch {
    return 0n;
  }
}

function updateSendForm(): void {
  const asset = sendNft?.name ?? byId<HTMLSelectElement>("send-asset").value;
  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  const assetLabel = sendNft ? nftAssetLabel(sendNft) : asset;
  byId("send-amount-symbol").textContent = assetLabel;
  byId("send-available").textContent =
    `Available: ${selectedSendBalance()} ${assetLabel}`;
  const minimumFee = minimumSendFeeAtomic();
  byId<HTMLInputElement>("send-fee").value = atomicToDecimal(minimumFee);
  byId("send-fee-note").textContent = !sendNft && asset === baseSymbol
    ? "Minimum fee: 1% of the transfer amount."
    : `Minimum fee: 1 ${baseSymbol} for asset transfers.`;
}

function populateSendAssets(): void {
  const select = byId<HTMLSelectElement>("send-asset");
  select.replaceChildren();
  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  const assets = new Set<string>([baseSymbol]);
  walletBalances?.tokens.forEach((token) => {
    const symbol = token.token.trim().toUpperCase();
    const balance = walletBalances?.balances.find(
      (entry) =>
        entry.nft_series === 0 &&
        entry.asset.trim().toUpperCase() === symbol &&
        decimalToAtomic(entry.balance, "Balance") > 0n
    );
    if (balance) assets.add(symbol);
  });
  assets.forEach((asset) => {
    const option = document.createElement("option");
    option.value = asset;
    option.textContent = asset;
    select.append(option);
  });
  updateSendForm();
}

function openSendScreen(): void {
  if (!walletBalances) throw new Error("Wallet balances are still loading.");
  sendNft = undefined;
  pendingTransfer = undefined;
  byId<HTMLFormElement>("send-form").reset();
  byId<HTMLSelectElement>("send-asset").disabled = false;
  byId<HTMLInputElement>("send-amount").disabled = false;
  populateSendAssets();
  showScreen("send-screen");
}

function openNftSendScreen(nft: OwnedNft): void {
  if (!walletBalances) throw new Error("Wallet balances are still loading.");
  sendNft = nft;
  pendingTransfer = undefined;
  byId<HTMLFormElement>("send-form").reset();

  const select = byId<HTMLSelectElement>("send-asset");
  select.replaceChildren();
  const option = document.createElement("option");
  option.value = nft.name;
  option.textContent = nftAssetLabel(nft);
  select.append(option);
  select.disabled = true;

  const amount = byId<HTMLInputElement>("send-amount");
  if (nft.ownership_type === 0) {
    amount.value = "1.00000000";
    amount.disabled = true;
  } else {
    amount.value = "";
    amount.disabled = false;
  }

  updateSendForm();
  showScreen("send-screen");
}

async function prepareTransfer(): Promise<void> {
  if (!walletStatus?.address) throw new Error("No active wallet is available.");
  const asset = sendNft?.name ?? byId<HTMLSelectElement>("send-asset").value;
  const assetLabel = sendNft ? nftAssetLabel(sendNft) : asset;
  const receiver = requireValue(
    byId<HTMLInputElement>("send-recipient").value,
    "Enter a receiving address."
  );
  const amount = requireValue(
    byId<HTMLInputElement>("send-amount").value,
    "Enter an amount to send."
  );
  const fee = requireValue(
    byId<HTMLInputElement>("send-fee").value,
    "Enter a transaction fee."
  );
  const amountAtomic = decimalToAtomic(amount, "Amount");
  const feeAtomic = decimalToAtomic(fee, "Transaction fee");
  if (amountAtomic === 0n) throw new Error("The transfer amount must be greater than zero.");
  if (sendNft?.ownership_type === 0 && amountAtomic !== 100_000_000n) {
    throw new Error("An indivisible NFT must be transferred as one complete asset.");
  }
  if (sendNft?.ownership_type === 1 && amountAtomic > 100_000_000n) {
    throw new Error("A fractional NFT/RWA transfer cannot exceed one complete asset.");
  }
  const minimumFee = minimumSendFeeAtomic();
  if (feeAtomic < minimumFee) {
    throw new Error(`The minimum transaction fee is ${atomicToDecimal(minimumFee)}.`);
  }
  const assetBalance = decimalToAtomic(selectedSendBalance(), "Available balance");
  if (amountAtomic > assetBalance) throw new Error(`Insufficient ${asset} balance.`);

  const baseSymbol = walletStatus.networkSymbol;
  const baseBalanceText = walletBalances?.balances.find(
    (entry) =>
      entry.nft_series === 0 && entry.asset.trim().toUpperCase() === baseSymbol
  )?.balance ?? "0.00000000";
  const baseBalance = decimalToAtomic(baseBalanceText, "Base balance");
  const baseRequired = asset === baseSymbol ? amountAtomic + feeAtomic : feeAtomic;
  if (baseRequired > baseBalance) throw new Error(`Insufficient ${baseSymbol} balance including the fee.`);

  const result = await send("wallet-prepare-transfer", {
    receiver,
    asset,
    nftSeries: sendNft?.series ?? 0,
    valueAtomic: amountAtomic.toString(),
    feeAtomic: feeAtomic.toString()
  }) as {
    transaction: Record<string, unknown>;
    review: Record<string, unknown>;
    receiverInput: string;
  };
  const total = asset === baseSymbol
    ? `${atomicToDecimal(amountAtomic + feeAtomic)} ${baseSymbol}`
    : `${atomicToDecimal(amountAtomic)} ${assetLabel} + ${atomicToDecimal(feeAtomic)} ${baseSymbol}`;
  pendingTransfer = {
    transaction: result.transaction,
    receiverInput: result.receiverInput,
    asset,
    amount: atomicToDecimal(amountAtomic),
    fee: atomicToDecimal(feeAtomic),
    total
  };
  byId("review-amount").textContent = pendingTransfer.amount;
  byId("review-asset").textContent = assetLabel;
  byId("review-sender").textContent = walletStatus.address;
  byId("review-recipient").textContent = result.receiverInput;
  byId("review-resolved-recipient").textContent = String(result.review.receiver ?? "");
  byId("review-fee").textContent = pendingTransfer.fee;
  byId("review-total").textContent = total;
  showScreen("send-review-screen");
}

function transactionTimestamp(transactionHex: string): Date | undefined {
  if (!/^[0-9a-f]+$/i.test(transactionHex) || transactionHex.length < 10) return undefined;
  const bytes = transactionHex.slice(2, 10).match(/../g);
  if (!bytes || bytes.length !== 4) return undefined;
  const seconds = bytes.reduce(
    (value, byte, index) => value + (Number.parseInt(byte, 16) * (2 ** (index * 8))),
    0
  );
  const date = new Date(seconds * 1_000);
  return Number.isNaN(date.getTime()) ? undefined : date;
}

function fieldLabel(name: string): string {
  return name
    .split("_")
    .map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : part)
    .join(" ");
}

function formatAtomicDisplay(value: unknown): string {
  try {
    const decimal = atomicToDecimal(BigInt(String(value ?? "0")));
    const [whole, fraction] = decimal.split(".");
    const trimmed = fraction.replace(/0+$/, "");
    return trimmed ? `${whole}.${trimmed}` : `${whole}.0`;
  } catch {
    return String(value ?? "");
  }
}

function isAtomicTransactionField(txtype: number, name: string): boolean {
  if (name.startsWith("txfee")) return true;
  const fieldsByType: Record<number, Set<string>> = {
    1: new Set(["value"]),
    2: new Set(["value"]),
    3: new Set(["number"]),
    6: new Set(["value1", "value2", "tip1", "tip2"]),
    7: new Set([
      "loan_amount",
      "collateral_amount",
      "payment_amount",
      "max_late_value"
    ]),
    8: new Set(["payback_amount", "tip"]),
    10: new Set(["value"]),
    11: new Set(["number"])
  };
  return fieldsByType[txtype]?.has(name) ?? false;
}

function transactionFieldDisplay(txtype: number, name: string, value: unknown): string {
  if (typeof value === "boolean") return value ? "True" : "False";
  if (isAtomicTransactionField(txtype, name)) return formatAtomicDisplay(value);
  if (name === "time" || name === "timestamp" || name === "offer_expiration") {
    const timestamp = Number(value);
    if (Number.isFinite(timestamp)) {
      return `${new Date(timestamp * 1_000).toLocaleString()} (${timestamp})`;
    }
  }
  if (name === "payment_period") {
    const periods: Record<string, string> = { d: "Daily", w: "Weekly", m: "Monthly" };
    return periods[String(value).trim().toLowerCase()] ?? String(value ?? "");
  }
  if (txtype === 3 && name === "hard_limit") {
    return Number(value) === 1 ? "Yes" : "No";
  }
  if (txtype === 4 && name === "series") {
    return Number(value) === 1 ? "Series" : "Single NFT/RWA";
  }
  if (txtype === 4 && name === "ownership_type") {
    return Number(value) === 1 ? "Fractional ownership" : "Complete ownership";
  }
  if ((txtype === 201 || txtype === 202) && name === "vote") {
    return Number(value) === 1 ? "Approve" : "Reject";
  }
  if ((name === "previous" || name.endsWith("_hash")) && /^0+$/.test(String(value))) {
    return "None";
  }
  if (txtype === 5 && (name === "impression_value" || name === "click_value")) {
    return (Number(value) / 100).toFixed(2);
  }
  return typeof value === "string" ? value.trim() : String(value ?? "");
}

function appendTransactionField(
  container: HTMLElement,
  name: string,
  value: unknown,
  technical = false,
  txtype = 0
): void {
  const row = document.createElement("div");
  row.className = `transaction-field${technical ? " technical-field" : ""}`;
  const label = document.createElement("span");
  label.textContent = fieldLabel(name);
  const contents = document.createElement("strong");
  contents.textContent = technical
    ? String(value ?? "")
    : transactionFieldDisplay(txtype, name, value);
  row.append(label, contents);
  container.append(row);
}

function transactionConfirmations(blockHeight: number, currentHeight: number): number {
  if (!blockHeight || !currentHeight || currentHeight < blockHeight) return 0;
  return currentHeight - blockHeight;
}

function confirmationLabel(blockHeight: number, currentHeight: number): string {
  const confirmations = transactionConfirmations(blockHeight, currentHeight);
  if (!blockHeight) return `Pending · 0/${CONFIRMED_AFTER_BLOCKS} confirmations`;
  if (confirmations >= CONFIRMED_AFTER_BLOCKS) {
    return `${confirmations} confirmations`;
  }
  return `${confirmations}/${CONFIRMED_AFTER_BLOCKS} confirmations`;
}

async function openTransaction(
  record: ActivityRecord,
  returnScreen: "activity" | "nft" = "activity"
): Promise<void> {
  selectedTransaction = record;
  transactionReturnScreen = returnScreen;
  const back = byId<HTMLButtonElement>("close-transaction");
  back.title = returnScreen === "nft"
    ? "Back to NFT/RWA details"
    : "Back to activity";
  back.setAttribute(
    "aria-label",
    returnScreen === "nft" ? "Back to NFT/RWA details" : "Back to activity"
  );
  showScreen("transaction-screen");
  byId("transaction-title").textContent = "Loading transaction";
  byId("transaction-header-status").textContent = "";
  byId("transaction-full-txid").textContent = record.txid;
  byId("transaction-overview").replaceChildren();
  byId("transaction-fields").replaceChildren();
  byId("transaction-technical-fields").replaceChildren();
  byId("transaction-miner-earnings").replaceChildren();
  byId("miner-earnings-section").hidden = true;

  try {
    const lookup = await send("wallet-transaction", { txid: record.txid }) as {
      txid: string;
      block_height: number;
      current_height: number;
      transaction_hex: string;
    };
    const wasm = await core(walletStatus?.selectedNetwork ?? "testnet");
    const decoded = JSON.parse(
      wasm.decode_transaction(lookup.transaction_hex)
    ) as DecodedTransaction;
    const confirmations = transactionConfirmations(
      lookup.block_height,
      lookup.current_height
    );
    const pending = confirmations < CONFIRMED_AFTER_BLOCKS;
    const timestamp = transactionTimestamp(lookup.transaction_hex);

    byId("transaction-title").textContent = decoded.transaction_name;
    byId("transaction-header-status").textContent = pending ? "Pending" : "Confirmed";
    byId("transaction-header-status").className = pending
      ? "transaction-pending"
      : "transaction-confirmed";
    byId("transaction-full-txid").textContent = lookup.txid;

    const overview = byId("transaction-overview");
    appendTransactionField(overview, "status", pending ? "Pending" : "Confirmed");
    appendTransactionField(
      overview,
      "block_height",
      lookup.block_height ? lookup.block_height.toLocaleString() : "Mempool"
    );
    appendTransactionField(
      overview,
      "confirmations",
      confirmationLabel(lookup.block_height, lookup.current_height)
    );
    if (timestamp) appendTransactionField(overview, "date", timestamp.toLocaleString());
    appendTransactionField(overview, "transaction_type", decoded.txtype);

    const fields = byId("transaction-fields");
    Object.entries(decoded.fields).forEach(([name, value]) => {
      appendTransactionField(fields, name, value, false, decoded.txtype);
    });

    const earnings = record.miner_earnings ?? [];
    if (earnings.length > 0) {
      const earningsContainer = byId("transaction-miner-earnings");
      earnings.forEach((earning, index) => {
        appendTransactionField(earningsContainer, `earning_${index + 1}_type`, earning.type);
        appendTransactionField(earningsContainer, `earning_${index + 1}_asset`, earning.asset);
        appendTransactionField(
          earningsContainer,
          `earning_${index + 1}_nft_series`,
          earning.nft_series
        );
        appendTransactionField(
          earningsContainer,
          `earning_${index + 1}_amount`,
          `${formatAtomicDisplay(earning.amount_atomic)} ${earning.asset.trim()}`
        );
      });
      byId("miner-earnings-section").hidden = false;
    }

    const technical = byId("transaction-technical-fields");
    if (decoded.stored_hash) {
      appendTransactionField(technical, "stored_hash", decoded.stored_hash, true);
    }
    if (decoded.signature) {
      appendTransactionField(technical, "signature", decoded.signature, true);
    }
    if (decoded.signature1) {
      appendTransactionField(technical, "signature_1", decoded.signature1, true);
    }
    if (decoded.signature2) {
      appendTransactionField(technical, "signature_2", decoded.signature2, true);
    }
    appendTransactionField(technical, "raw_transaction_bytes", lookup.transaction_hex, true);
  } catch (error) {
    byId("transaction-title").textContent = "Transaction unavailable";
    const fields = byId("transaction-fields");
    fields.replaceChildren();
    appendTransactionField(fields, "error", errorText(error));
  }
}

type ActivitySummary = {
  title: string;
  subtitle: string;
  amount: string;
  secondary?: string;
  direction: "incoming" | "outgoing" | "neutral";
};

function shortActivityAddress(value: unknown): string {
  const address = String(value ?? "").trim();
  return address.length > 18
    ? `${address.slice(0, 8)}...${address.slice(-7)}`
    : address;
}

function activitySummary(decoded: DecodedTransaction, address: string): ActivitySummary {
  const fields = decoded.fields;
  const active = address.trim().toLowerCase();
  const baseSymbol = walletStatus?.networkSymbol ?? "CLTC";
  const asset = (name: string) => String(fields[name] ?? "").trim();
  const amount = (name: string, symbol: string, sign = "") =>
    `${sign}${formatAtomicDisplay(fields[name])} ${symbol}`;
  const fee = () => `-${formatAtomicDisplay(fields.txfee ?? 0)} ${baseSymbol}`;

  switch (decoded.txtype) {
    case 1:
      return {
        title: "Mining reward",
        subtitle: "From network",
        amount: amount("value", baseSymbol, "+"),
        direction: "incoming"
      };
    case 2: {
      const symbol = asset("coin");
      const sent = String(fields.sender ?? "").trim().toLowerCase() === active;
      return {
        title: sent ? `Sent ${symbol}` : `Received ${symbol}`,
        subtitle: sent
          ? `To: ${shortActivityAddress(fields.receiver)}`
          : `From: ${shortActivityAddress(fields.sender)}`,
        amount: amount("value", symbol, sent ? "-" : "+"),
        direction: sent ? "outgoing" : "incoming"
      };
    }
    case 3: {
      const symbol = asset("ticker");
      return {
        title: `Created ${symbol}`,
        subtitle: "Token creation",
        amount: amount("number", symbol, "+"),
        secondary: fee(),
        direction: "incoming"
      };
    }
    case 4: {
      const count = String(fields.count ?? "1");
      return {
        title: `Created ${asset("nft_name")}`,
        subtitle: Number(fields.series ?? 0) === 0 ? "NFT/RWA" : "NFT/RWA collection",
        amount: `+${count} ${Number(count) === 1 ? "item" : "items"}`,
        secondary: fee(),
        direction: "incoming"
      };
    }
    case 6: {
      const first = String(fields.sender1 ?? "").trim().toLowerCase() === active;
      const sentAsset = asset(first ? "ticker1" : "ticker2");
      const receivedAsset = asset(first ? "ticker2" : "ticker1");
      return {
        title: "Completed swap",
        subtitle: `With: ${shortActivityAddress(fields[first ? "sender2" : "sender1"])}`,
        amount: amount(first ? "value1" : "value2", sentAsset, "-"),
        secondary: amount(first ? "value2" : "value1", receivedAsset, "+"),
        direction: "neutral"
      };
    }
    case 7: {
      const lender = String(fields.lender ?? "").trim().toLowerCase() === active;
      return {
        title: lender ? "Loan funded" : "Loan received",
        subtitle: lender
          ? `To: ${shortActivityAddress(fields.borrower)}`
          : `From: ${shortActivityAddress(fields.lender)}`,
        amount: amount("loan_amount", asset("loan_coin"), lender ? "-" : "+"),
        direction: lender ? "outgoing" : "incoming"
      };
    }
    case 8:
      return {
        title: "Loan payment",
        subtitle: `Contract: ${shortActivityAddress(fields.contract_hash)}`,
        amount: `-${formatAtomicDisplay(fields.payback_amount)}`,
        direction: "outgoing"
      };
    case 10:
      return {
        title: `Burned ${asset("coin")}`,
        subtitle: "Removed from supply",
        amount: amount("value", asset("coin"), "-"),
        direction: "outgoing"
      };
    case 11: {
      const symbol = asset("ticker");
      return {
        title: `Issued ${symbol}`,
        subtitle: "Additional token supply",
        amount: amount("number", symbol, "+"),
        secondary: fee(),
        direction: "incoming"
      };
    }
    case 12:
      return {
        title: "Created vanity address",
        subtitle: String(fields.vanity_address ?? "").trim(),
        amount: fee(),
        direction: "outgoing"
      };
    default:
      return {
        title: decoded.transaction_name,
        subtitle: "Contractless transaction",
        amount: fee(),
        direction: "outgoing"
      };
  }
}

async function renderActivity(
  records: ActivityRecord[],
  currentHeight: number
): Promise<void> {
  const list = byId("activity-list");
  list.replaceChildren();
  byId("activity-count").textContent = `${records.length} shown`;

  if (records.length === 0) {
    const empty = document.createElement("div");
    empty.className = "activity-empty";
    const title = document.createElement("strong");
    title.textContent = "No wallet activity found";
    const description = document.createElement("span");
    description.textContent = "New transactions will appear here after they reach the selected API.";
    empty.append(title, description);
    list.append(empty);
    return;
  }

  const wasm = await core(walletStatus?.selectedNetwork ?? "testnet");
  records.forEach((record) => {
    let decoded: DecodedTransaction;
    try {
      decoded = JSON.parse(wasm.decode_transaction(record.transaction_hex)) as DecodedTransaction;
    } catch {
      const txtype = Number.parseInt(record.transaction_hex.slice(0, 2), 16);
      decoded = {
        txtype,
        transaction_name: TRANSACTION_NAMES[txtype] ?? `Transaction Type ${txtype}`,
        fields: {}
      };
    }
    const summary = activitySummary(decoded, walletStatus?.address ?? "");
    const date = transactionTimestamp(record.transaction_hex);
    const confirmations = transactionConfirmations(record.block_height, currentHeight);
    const pending = confirmations < CONFIRMED_AFTER_BLOCKS;
    const row = document.createElement("button");
    row.type = "button";
    row.className = "activity-row";

    const marker = document.createElement("span");
    marker.className = `activity-marker ${summary.direction}`;
    marker.textContent = pending
      ? "..."
      : summary.direction === "incoming"
        ? "\u2191"
        : summary.direction === "outgoing"
          ? "\u2193"
          : "\u2194";

    const details = document.createElement("div");
    details.className = "activity-details";
    const name = document.createElement("strong");
    name.textContent = summary.title;
    const counterpart = document.createElement("span");
    counterpart.textContent = pending ? `Pending · ${summary.subtitle}` : summary.subtitle;
    const timestamp = document.createElement("small");
    timestamp.textContent = date
      ? date.toLocaleString()
      : "Transaction time unavailable";
    details.append(name, counterpart, timestamp);

    const status = document.createElement("div");
    status.className = "activity-status";
    const transactionAmount = document.createElement("strong");
    transactionAmount.className = summary.direction;
    transactionAmount.textContent = summary.amount;
    status.append(transactionAmount);
    if (summary.secondary) {
      const secondary = document.createElement("span");
      secondary.textContent = summary.secondary;
      status.append(secondary);
    }
    const confirmation = document.createElement("span");
    confirmation.className = pending ? "pending" : "confirmed";
    confirmation.textContent = confirmationLabel(record.block_height, currentHeight);
    status.append(confirmation);

    row.append(marker, details, status);
    row.addEventListener("click", () => {
      void openTransaction(record);
    });
    list.append(row);
  });
}

async function loadActivity(): Promise<void> {
  if (!walletStatus?.unlocked) return;
  byId("activity-count").textContent = "Loading";
  const list = byId("activity-list");
  list.replaceChildren();
  const loading = document.createElement("div");
  loading.className = "activity-empty";
  loading.textContent = "Loading recent activity...";
  list.append(loading);

  try {
    const result = await send("wallet-activity") as {
      current_height?: number;
      transactions?: ActivityRecord[];
    };
    await renderActivity(
      Array.isArray(result.transactions) ? result.transactions : [],
      Number(result.current_height ?? 0)
    );
  } catch (error) {
    list.replaceChildren();
    const failed = document.createElement("div");
    failed.className = "activity-empty activity-error";
    failed.textContent = errorText(error);
    list.append(failed);
    byId("activity-count").textContent = "Unavailable";
  }
}

function errorText(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error).replace(/^JsValue\((.*)\)$/, "$1");
}

async function action(run: () => Promise<void>, loading = false): Promise<void> {
  notice.textContent = "";
  if (loading) showScreen("loading");
  try {
    await run();
  } catch (error) {
    notice.textContent = errorText(error);
    throw error;
  }
}

function requireValue(value: string, message: string): string {
  if (!value.trim()) throw new Error(message);
  return value;
}

function bytesToBase64(bytes: Uint8Array): string {
  let binary = "";
  const chunkSize = 0x8000;
  for (let offset = 0; offset < bytes.length; offset += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
  }
  return btoa(binary);
}

function downloadBase64Png(contents: string, filename: string): void {
  const binary = atob(contents);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }
  const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();
  setTimeout(() => URL.revokeObjectURL(url), 1_000);
}

async function openBackupScreen(status: WalletStatus, password: string): Promise<void> {
  walletStatus = status;
  currentPassword = password;
  backupImageBase64 = String(await send("export-private-key-image", {
    password,
    imagePassword: password
  }));
  byId<HTMLImageElement>("wallet-image-preview").src =
    `data:image/png;base64,${backupImageBase64}`;
  const confirmation = byId<HTMLInputElement>("backup-confirmed");
  confirmation.disabled = true;
  confirmation.checked = false;
  byId<HTMLButtonElement>("open-wallet").disabled = true;
  showScreen("backup-screen");
}

document.querySelectorAll<HTMLButtonElement>(".navigation").forEach((button) => {
  button.addEventListener("click", () => {
    const destination = String(button.dataset.screen);
    if (destination === "image-screen" && firefoxToolbarPopup) {
      void chrome.tabs.create({
        url: chrome.runtime.getURL("import-wallet.html"),
        active: true
      });
      return;
    }
    showScreen(destination);
  });
});

byId("account-menu-button").addEventListener("click", () => setAccountDrawer(true));
byId("close-account-drawer").addEventListener("click", () => setAccountDrawer(false));
byId("drawer-backdrop").addEventListener("click", () => setAccountDrawer(false));

function openAddWalletFlow(screen: "create-screen" | "image-screen"): void {
  setAccountDrawer(false);
  byId("cancel-wallet-add").hidden = false;
  showScreen(screen);
}

byId("menu-create-wallet").addEventListener("click", () => {
  openAddWalletFlow("create-screen");
});

byId("menu-import-wallet").addEventListener("click", () => {
  setAccountDrawer(false);
  if (firefoxToolbarPopup) {
    void chrome.tabs.create({
      url: chrome.runtime.getURL("import-wallet.html"),
      active: true
    });
  } else {
    openAddWalletFlow("image-screen");
  }
});

byId("cancel-wallet-add").addEventListener("click", () => {
  byId("cancel-wallet-add").hidden = true;
  if (walletStatus) showStatus(walletStatus);
});

byId("menu-settings").addEventListener("click", () => {
  setAccountDrawer(false);
  clearRevealedPrivateKey();
  if (walletStatus) {
    byId<HTMLInputElement>("settings-testnet-api-url").value = walletStatus.testnetApiUrl;
    byId<HTMLInputElement>("settings-mainnet-api-url").value = walletStatus.mainnetApiUrl;
    byId<HTMLSelectElement>("settings-lock-timeout").value =
      String(walletStatus.autoLockMinutes);
  }
  showScreen("settings-screen");
});

byId("close-settings").addEventListener("click", () => {
  clearRevealedPrivateKey();
  if (walletStatus) showStatus(walletStatus);
});

byId("drawer-lock-wallet").addEventListener("click", () => {
  void action(async () => {
    setAccountDrawer(false);
    currentPassword = "";
    backupImageBase64 = "";
    showStatus(await send("lock") as WalletStatus);
  });
});

byId("open-receive").addEventListener("click", () => {
  void action(openReceiveScreen);
});

byId("open-send").addEventListener("click", () => {
  void action(async () => openSendScreen());
});

byId("close-send").addEventListener("click", () => {
  pendingTransfer = undefined;
  if (sendNft) {
    renderNftDetails(sendNft);
    showScreen("nft-detail-screen");
  } else if (walletStatus) {
    showStatus(walletStatus);
  }
});

byId("send-asset").addEventListener("change", updateSendForm);
byId("send-amount").addEventListener("input", updateSendForm);

byId("send-form").addEventListener("submit", (event) => {
  event.preventDefault();
  void action(prepareTransfer);
});

byId("send-form").addEventListener("keydown", (event) => {
  if (!(event instanceof KeyboardEvent) || event.key !== "Enter") return;
  if (!(event.target instanceof HTMLInputElement)) return;
  event.preventDefault();
  byId<HTMLFormElement>("send-form").requestSubmit();
});

byId("edit-send").addEventListener("click", () => {
  showScreen("send-screen");
});

byId("sign-broadcast-transfer").addEventListener("click", () => {
  void action(async () => {
    if (!pendingTransfer) throw new Error("The reviewed transfer is unavailable.");
    const result = await send("wallet-broadcast-transfer", {
      transaction: pendingTransfer.transaction
    }) as { txid: string };
    lastSentTxid = result.txid;
    byId("send-result-txid").textContent = lastSentTxid;
    pendingTransfer = undefined;
    showScreen("send-success-screen");
  });
});

byId("copy-send-txid").addEventListener("click", () => {
  if (!lastSentTxid) return;
  void navigator.clipboard.writeText(lastSentTxid)
    .then(() => {
      notice.textContent = "Transaction ID copied.";
    })
    .catch((error) => {
      notice.textContent = errorText(error);
    });
});

byId("finish-send").addEventListener("click", () => {
  lastSentTxid = "";
  const nftTransfer = Boolean(sendNft);
  sendNft = undefined;
  if (walletStatus) {
    void loadWalletBalances();
    if (nftTransfer) {
      selectedNft = undefined;
      showScreen("nfts-screen");
      void loadOwnedNfts(true);
    } else {
      showStatus(walletStatus);
    }
  }
});

byId("open-activity").addEventListener("click", () => {
  showScreen("activity-screen");
  void loadActivity();
});

byId("open-nfts").addEventListener("click", () => {
  nftPage = 1;
  selectedNft = undefined;
  showScreen("nfts-screen");
  void loadOwnedNfts(true);
});

byId("close-nfts").addEventListener("click", () => {
  nftLoadId += 1;
  if (walletStatus) showStatus(walletStatus);
});

byId("refresh-nfts").addEventListener("click", () => {
  void loadOwnedNfts(true);
});

byId("close-nft-detail").addEventListener("click", () => {
  selectedNft = undefined;
  showScreen("nfts-screen");
});

byId("transfer-nft").addEventListener("click", () => {
  void action(async () => {
    if (!selectedNft) throw new Error("No NFT or RWA is selected.");
    openNftSendScreen(selectedNft);
  });
});

byId("nfts-previous").addEventListener("click", () => {
  if (nftPage <= 1) return;
  nftPage -= 1;
  const loadId = ++nftLoadId;
  renderNftGallery();
  void hydrateVisibleNfts(loadId);
});

byId("nfts-next").addEventListener("click", () => {
  const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE));
  if (nftPage >= totalPages) return;
  nftPage += 1;
  const loadId = ++nftLoadId;
  renderNftGallery();
  void hydrateVisibleNfts(loadId);
});

byId("close-activity").addEventListener("click", () => {
  if (walletStatus) showStatus(walletStatus);
});

byId("close-transaction").addEventListener("click", () => {
  if (transactionReturnScreen === "nft" && selectedNft) {
    renderNftDetails(selectedNft);
    showScreen("nft-detail-screen");
  } else {
    showScreen("activity-screen");
  }
});

byId("copy-transaction-txid").addEventListener("click", () => {
  const txid = selectedTransaction?.txid;
  if (!txid) return;
  void navigator.clipboard.writeText(txid)
    .then(() => {
      notice.textContent = "Transaction ID copied.";
    })
    .catch((error) => {
      notice.textContent = errorText(error);
    });
});

byId("refresh-activity").addEventListener("click", () => {
  void loadActivity();
});

byId("refresh-balances").addEventListener("click", () => {
  void loadWalletBalances();
});

document.querySelectorAll<HTMLButtonElement>("[data-token-filter]").forEach((button) => {
  button.addEventListener("click", () => {
    tokenFilter = button.dataset.tokenFilter === "all" ? "all" : "held";
    document.querySelectorAll<HTMLButtonElement>("[data-token-filter]").forEach((candidate) => {
      candidate.classList.toggle("active", candidate === button);
    });
    renderTokenBalances();
  });
});

byId("close-receive").addEventListener("click", () => {
  if (walletStatus) showStatus(walletStatus);
});

byId("copy-receive-address").addEventListener("click", () => {
  const address = byId("receive-address-value").textContent?.trim();
  if (!address) {
    notice.textContent = "No active wallet address is available.";
    return;
  }
  void navigator.clipboard.writeText(address)
    .then(() => {
      notice.textContent = "Wallet address copied.";
    })
    .catch((error) => {
      notice.textContent = errorText(error);
    });
});

byId("save-wallet-settings").addEventListener("click", () => {
  void action(async () => {
    const testnetApiUrl = byId<HTMLInputElement>("settings-testnet-api-url").value.replace(/\/+$/, "");
    const mainnetApiUrl = byId<HTMLInputElement>("settings-mainnet-api-url").value.replace(/\/+$/, "");
    if (!testnetApiUrl) throw new Error("A testnet API URL is required.");
    for (const apiUrl of [testnetApiUrl, mainnetApiUrl].filter(Boolean)) {
      const parsed = new URL(apiUrl);
      if (parsed.protocol !== "https:"
        && parsed.hostname !== "localhost"
        && parsed.hostname !== "127.0.0.1") {
        throw new Error("Contractless API URLs must use HTTPS.");
      }
      const originPermission = `${parsed.origin}/*`;
      const alreadyGranted = await chrome.permissions.contains({ origins: [originPermission] });
      if (!alreadyGranted) {
        const granted = await chrome.permissions.request({ origins: [originPermission] });
        if (!granted) throw new Error("Permission to contact this API endpoint was denied.");
      }
    }

    walletStatus = await send("save-settings", {
      testnetApiUrl,
      mainnetApiUrl,
      autoLockMinutes: byId<HTMLSelectElement>("settings-lock-timeout").value
    }) as WalletStatus;
    notice.textContent = "Settings saved.";
  });
});

document.querySelectorAll<HTMLButtonElement>("[data-select-network]").forEach((button) => {
  button.addEventListener("click", () => {
    const network = button.dataset.selectNetwork;
    if (!network || network === walletStatus?.selectedNetwork) return;
    void action(async () => {
      setAccountDrawer(false);
      showStatus(await send("switch-network", { network }) as WalletStatus);
    });
  });
});

document.querySelectorAll<HTMLButtonElement>("[data-backup-action]").forEach((button) => {
  button.addEventListener("click", () => {
    pendingBackupAction = button.dataset.backupAction as "image" | "private" | "print";
    setBackupKeyModal(true);
  });
});

byId("close-backup-key").addEventListener("click", () => setBackupKeyModal(false));
byId("backup-key-backdrop").addEventListener("click", () => setBackupKeyModal(false));

byId("confirm-backup-key").addEventListener("click", () => {
  const actionName = pendingBackupAction;
  const password = byId<HTMLInputElement>("backup-key-input").value;
  if (!actionName) return;
  if (!password) {
    notice.textContent = "Enter the active wallet encryption key.";
    return;
  }

  void action(async () => {
    if (actionName === "image") {
      const image = String(await send("export-private-key-image", {
        password,
        imagePassword: password
      }));
      const address = walletStatus?.address?.split(".")[0] ?? "contractless";
      downloadBase64Png(image, `${address}.wallet.png`);
      setBackupKeyModal(false);
      notice.textContent = "Wallet image downloaded.";
      return;
    }

    const backup = await send("reveal-private-key", { password }) as {
      label: string;
      address: string;
      privateKey: string;
    };
    setBackupKeyModal(false);

    if (actionName === "private") {
      byId<HTMLTextAreaElement>("revealed-private-key").value = backup.privateKey;
      byId("private-key-result").hidden = false;
      byId("private-key-result").scrollIntoView({ block: "nearest" });
      return;
    }

    const printId = crypto.randomUUID();
    await chrome.storage.session.set({
      [`print-backup:${printId}`]: {
        label: backup.label,
        address: backup.address,
        privateKey: backup.privateKey,
        encryptionKey: password
      }
    });
    await chrome.windows.create({
      url: chrome.runtime.getURL(`print.html?id=${encodeURIComponent(printId)}`),
      type: "popup",
      width: 820,
      height: 760
    });
  });
});

byId("clear-private-key").addEventListener("click", clearRevealedPrivateKey);
byId("copy-private-key").addEventListener("click", () => {
  const privateKey = byId<HTMLTextAreaElement>("revealed-private-key").value;
  if (!privateKey) return;
  void navigator.clipboard.writeText(privateKey)
    .then(() => {
      notice.textContent = "Private key copied.";
    })
    .catch((error) => {
      notice.textContent = errorText(error);
    });
});

document.querySelectorAll<HTMLButtonElement>(".reveal").forEach((button) => {
  button.addEventListener("click", () => {
    const input = byId<HTMLInputElement>(String(button.dataset.input));
    const showing = input.type === "text";
    input.type = showing ? "password" : "text";
    button.textContent = showing ? "Show" : "Hide";
  });
});

byId("continue-create").addEventListener("click", () => {
  try {
    pendingPassword = requireValue(
      byId<HTMLInputElement>("new-password").value,
      "Enter an encryption key."
    );
    byId<HTMLInputElement>("verify-password").value = "";
    showScreen("verify-screen");
  } catch (error) {
    notice.textContent = errorText(error);
  }
});

byId("create-wallet").addEventListener("click", () => {
  const verification = byId<HTMLInputElement>("verify-password").value;
  if (verification !== pendingPassword) {
    notice.textContent = "The encryption keys do not match.";
    return;
  }
  void action(async () => {
    const password = pendingPassword;
    const status = await send("create-wallet", { password }) as WalletStatus;
    pendingPassword = "";
    byId<HTMLInputElement>("new-password").value = "";
    byId<HTMLInputElement>("verify-password").value = "";
    await openBackupScreen(status, password);
  }, true).catch(() => showScreen("verify-screen", false));
});

const walletImage = byId<HTMLInputElement>("wallet-image");
walletImage.addEventListener("change", () => {
  const file = walletImage.files?.[0];
  selectedImageBase64 = "";
  byId("image-file-name").textContent = file?.name ?? "Choose wallet image";
  if (!file) return;
  void file.arrayBuffer()
    .then((buffer) => {
      selectedImageBase64 = bytesToBase64(new Uint8Array(buffer));
    })
    .catch((error) => {
      notice.textContent = errorText(error);
    });
});

byId("import-image-form").addEventListener("submit", (event) => {
  event.preventDefault();
  const password = byId<HTMLInputElement>("image-password").value;
  try {
    requireValue(password, "Enter the wallet image encryption key.");
    requireValue(selectedImageBase64, "Choose a wallet image.");
  } catch (error) {
    notice.textContent = errorText(error);
    return;
  }

  void action(async () => {
    const status = await send("import-wallet-image", {
      imageBase64: selectedImageBase64,
      imagePassword: password,
      password
    }) as WalletStatus;
    currentPassword = password;
    selectedImageBase64 = "";
    walletImage.value = "";
    byId<HTMLInputElement>("image-password").value = "";
    showStatus(status);
  }, true).catch(() => showScreen("image-screen", false));
});

byId("import-private").addEventListener("click", () => {
  const privateKey = byId<HTMLTextAreaElement>("private-key").value.trim();
  const password = byId<HTMLInputElement>("private-password").value;
  try {
    requireValue(privateKey, "Paste the private key.");
    requireValue(password, "Enter a new encryption key.");
  } catch (error) {
    notice.textContent = errorText(error);
    return;
  }

  void action(async () => {
    const status = await send("import-private-key", { privateKey, password }) as WalletStatus;
    byId<HTMLTextAreaElement>("private-key").value = "";
    byId<HTMLInputElement>("private-password").value = "";
    await openBackupScreen(status, password);
  }, true).catch(() => showScreen("private-screen", false));
});

byId("save-image").addEventListener("click", () => {
  void action(async () => {
    if (!backupImageBase64) throw new Error("The wallet image is not ready.");
    const address = walletStatus?.address?.split(".")[0] ?? "contractless";
    downloadBase64Png(backupImageBase64, `${address}.wallet.png`);
    const confirmation = byId<HTMLInputElement>("backup-confirmed");
    confirmation.disabled = false;
    confirmation.checked = false;
    byId<HTMLButtonElement>("open-wallet").disabled = true;
  });
});

byId<HTMLInputElement>("backup-confirmed").addEventListener("change", (event) => {
  byId<HTMLButtonElement>("open-wallet").disabled =
    !(event.currentTarget as HTMLInputElement).checked;
});

byId("open-wallet").addEventListener("click", () => {
  currentPassword = "";
  backupImageBase64 = "";
  byId<HTMLImageElement>("wallet-image-preview").removeAttribute("src");
  if (walletStatus) showStatus(walletStatus);
});

byId("unlock-form").addEventListener("submit", (event) => {
  event.preventDefault();
  const password = byId<HTMLInputElement>("unlock-password").value;
  try {
    requireValue(password, "Enter your encryption key.");
  } catch (error) {
    notice.textContent = errorText(error);
    return;
  }
  void action(async () => {
    const status = await send("unlock", { password }) as WalletStatus;
    currentPassword = password;
    byId<HTMLInputElement>("unlock-password").value = "";
    showStatus(status);
  }, true).catch(() => showScreen("locked-screen", false));
});

byId("approve-provider-approval").addEventListener("click", () => {
  if (!pendingProviderApprovalId) return;
  setApprovalProcessing("approve");
  void action(async () => {
    const result = await send("resolve-provider-approval", {
      id: pendingProviderApprovalId,
      approved: true
    }) as { remaining?: number };
    pendingProviderApprovalId = "";
    await showNextProviderApproval(result.remaining ?? 0, true);
  }).catch(() => setApprovalProcessing(null));
});

byId("reject-provider-approval").addEventListener("click", () => {
  if (!pendingProviderApprovalId) return;
  setApprovalProcessing("reject");
  void action(async () => {
    const result = await send("resolve-provider-approval", {
      id: pendingProviderApprovalId,
      approved: false
    }) as { remaining?: number };
    pendingProviderApprovalId = "";
    await showNextProviderApproval(result.remaining ?? 0, false);
  }).catch(() => setApprovalProcessing(null));
});

send("status")
  .then((status) => showStatus(status as WalletStatus))
  .catch((error) => {
    showScreen("create-screen");
    notice.textContent = errorText(error);
  });
