diff --git a/website_next/index.html b/website_next/index.html
index d6612eff8..8c8aef6c2 100644
--- a/website_next/index.html
+++ b/website_next/index.html
@@ -123,7 +123,7 @@
-
+
diff --git a/website_next/wallets/add/index.js b/website_next/wallets/add/index.js
index 8c6955e6b..52611fbab 100644
--- a/website_next/wallets/add/index.js
+++ b/website_next/wallets/add/index.js
@@ -1,4 +1,3 @@
-import { createElement } from "../dom.js";
import { createField } from "../form/index.js";
import { redaction } from "../redaction/index.js";
@@ -35,14 +34,14 @@ function createSourceInput() {
* @param {AddWalletFormOptions} options
*/
export function createAddForm(options) {
- const form = createElement("form", "wallets__dialog-form");
+ const form = document.createElement("form");
const title = document.createElement("h2");
const name = document.createElement("input");
const source = createSourceInput();
- const actions = createElement("div", "wallets__dialog-actions");
+ const actions = document.createElement("div");
const cancel = document.createElement("button");
const submit = document.createElement("button");
- const status = createElement("p", "wallets__status");
+ const status = document.createElement("p");
const fields = [
createField("name", name),
createField("xpub or descriptor", source),
diff --git a/website_next/wallets/add/inference.js b/website_next/wallets/add/inference.js
deleted file mode 100644
index 44f7f7eda..000000000
--- a/website_next/wallets/add/inference.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { fetchWalletAddresses } from "../lookup/index.js";
-import {
- generateAddressesFromKey,
- isOutputDescriptor,
-} from "../derive/index.js";
-import { parseOutputDescriptor } from "../derive/descriptor.js";
-import { addressScripts } from "../derive/script.js";
-
-const RECEIVE_PATH = /** @type {const} */ ([0]);
-
-/**
- * @typedef {import("../derive/address.js").AddressScript} AddressScript
- * @typedef {import("../scan/branch.js").AddressClient} AddressClient
- * @typedef {import("../lookup/index.js").WalletAddress} WalletAddress
- */
-
-/**
- * @param {WalletAddress} address
- */
-function hasHistory(address) {
- return address.received > 0 || address.sent > 0 || address.txCount > 0;
-}
-
-/**
- * @param {AddressClient} client
- * @param {string} source
- * @returns {Promise}
- */
-export async function inferAddressScript(client, source) {
- if (isOutputDescriptor(source)) {
- return parseOutputDescriptor(source).script;
- }
-
- for (const { id } of addressScripts) {
- const generated = await generateAddressesFromKey(source, {
- start: 0,
- count: 1,
- script: id,
- path: RECEIVE_PATH,
- });
- const [address] = await fetchWalletAddresses(client, generated);
-
- if (address && hasHistory(address)) {
- return id;
- }
- }
-
- return addressScripts[0].id;
-}
diff --git a/website_next/wallets/add/style.css b/website_next/wallets/add/style.css
index 5a4434284..4afa740fd 100644
--- a/website_next/wallets/add/style.css
+++ b/website_next/wallets/add/style.css
@@ -1,12 +1,20 @@
main.wallets {
- .wallets__dialog-form {
- display: grid;
- gap: 0.75rem;
- }
+ .wallets__dialog {
+ > form {
+ display: grid;
+ gap: 0.75rem;
- .wallets__dialog-actions {
- display: flex;
- gap: 0.5rem;
- justify-content: end;
+ > div {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: end;
+ }
+
+ button[type="submit"] {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+ }
+ }
}
}
diff --git a/website_next/wallets/amount/index.js b/website_next/wallets/amount/index.js
new file mode 100644
index 000000000..9193b9c76
--- /dev/null
+++ b/website_next/wallets/amount/index.js
@@ -0,0 +1,192 @@
+import { redaction } from "../redaction/index.js";
+
+const SATS_PER_BTC = 100_000_000;
+const FRACTION_DIGITS = 8;
+const FIXED_PRIVATE_TEXT = "*****";
+
+/**
+ * @typedef {Object} BtcAmountOptions
+ * @property {boolean} [signed]
+ */
+
+/**
+ * @typedef {Object} BtcPart
+ * @property {string} text
+ * @property {boolean} muted
+ */
+
+/**
+ * @param {BtcPart[]} parts
+ * @param {string} text
+ * @param {boolean} muted
+ */
+function pushPart(parts, text, muted) {
+ const last = parts[parts.length - 1];
+
+ if (last && last.muted === muted) {
+ last.text += text;
+ return;
+ }
+
+ parts.push({ text, muted });
+}
+
+/**
+ * @param {number} value
+ */
+function formatInteger(value) {
+ return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
+}
+
+/**
+ * @param {number} sats
+ */
+function splitBtc(sats) {
+ const absolute = Math.abs(sats);
+
+ return {
+ whole: Math.floor(absolute / SATS_PER_BTC),
+ fraction: String(absolute % SATS_PER_BTC).padStart(FRACTION_DIGITS, "0"),
+ };
+}
+
+/**
+ * @param {string} fraction
+ * @param {(index: number) => boolean} isMuted
+ * @param {(index: number) => boolean} isSpaceMuted
+ */
+function getFractionParts(fraction, isMuted, isSpaceMuted) {
+ const parts = /** @type {BtcPart[]} */ ([]);
+
+ for (let index = 0; index < fraction.length; index += 1) {
+ pushPart(parts, fraction[index], isMuted(index));
+
+ if (index === 1 || index === 4) {
+ pushPart(parts, " ", isSpaceMuted(index));
+ }
+ }
+
+ return parts;
+}
+
+/**
+ * @param {number} sats
+ * @param {BtcAmountOptions} [options]
+ */
+function getBtcParts(sats, options = {}) {
+ const parts = /** @type {BtcPart[]} */ ([]);
+ const { whole, fraction } = splitBtc(sats);
+ const firstFractionDigit = fraction.search(/[1-9]/);
+ const lastFractionDigit = Math.max(...[...fraction].map((digit, index) => {
+ return digit === "0" ? -1 : index;
+ }));
+
+ if (options.signed && sats > 0) pushPart(parts, "+", false);
+ if (sats < 0) pushPart(parts, "-", false);
+
+ pushPart(parts, "₿", true);
+
+ if (whole === 0) {
+ const mutedUntil = firstFractionDigit === -1
+ ? FRACTION_DIGITS
+ : firstFractionDigit;
+
+ pushPart(parts, "0.", true);
+ for (const part of getFractionParts(
+ fraction,
+ (index) => index < mutedUntil,
+ (index) => index < mutedUntil,
+ )) {
+ pushPart(parts, part.text, part.muted);
+ }
+
+ return parts;
+ }
+
+ pushPart(parts, formatInteger(whole), false);
+
+ if (lastFractionDigit === -1) {
+ pushPart(parts, ".", true);
+ for (const part of getFractionParts(fraction, () => true, () => true)) {
+ pushPart(parts, part.text, part.muted);
+ }
+
+ return parts;
+ }
+
+ pushPart(parts, ".", false);
+ for (const part of getFractionParts(
+ fraction,
+ (index) => index > lastFractionDigit,
+ (index) => index >= lastFractionDigit,
+ )) {
+ pushPart(parts, part.text, part.muted);
+ }
+
+ return parts;
+}
+
+/**
+ * @param {number} sats
+ * @param {BtcAmountOptions} [options]
+ */
+export function formatBtc(sats, options = {}) {
+ return getBtcParts(sats, options).map((part) => part.text).join("");
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {number} sats
+ * @param {BtcAmountOptions} [options]
+ */
+function renderBtcAmount(element, sats, options = {}) {
+ if (redaction.isHidden()) {
+ element.textContent = FIXED_PRIVATE_TEXT;
+ return;
+ }
+
+ element.replaceChildren(...getBtcParts(sats, options).map((part) => {
+ const span = document.createElement("span");
+
+ if (part.muted) {
+ span.setAttribute("data-wallets-btc-muted", "");
+ }
+ span.append(part.text);
+
+ return span;
+ }));
+}
+
+/**
+ * @template {keyof HTMLElementTagNameMap} Tag
+ * @param {Tag} tag
+ * @param {number} sats
+ * @param {BtcAmountOptions} [options]
+ */
+export function createBtcAmount(tag, sats, options = {}) {
+ const element = document.createElement(tag);
+
+ element.setAttribute("data-wallets-btc-amount", String(sats));
+ element.setAttribute(
+ "data-wallets-btc-signed",
+ options.signed ? "true" : "false",
+ );
+ renderBtcAmount(element, sats, options);
+
+ return element;
+}
+
+/**
+ * @param {HTMLElement} root
+ */
+export function syncBtcAmounts(root) {
+ const amounts = root.querySelectorAll("[data-wallets-btc-amount]");
+
+ for (const amount of amounts) {
+ const element = /** @type {HTMLElement} */ (amount);
+ const sats = Number(element.getAttribute("data-wallets-btc-amount"));
+ const signed = element.getAttribute("data-wallets-btc-signed") === "true";
+
+ renderBtcAmount(element, sats, { signed });
+ }
+}
diff --git a/website_next/wallets/dialog/style.css b/website_next/wallets/dialog/style.css
index 0320ef0f1..6b62202ad 100644
--- a/website_next/wallets/dialog/style.css
+++ b/website_next/wallets/dialog/style.css
@@ -10,10 +10,9 @@ main.wallets {
h2 {
margin: 0;
}
- }
- .wallets__dialog::backdrop {
- background: color-mix(in oklch, var(--black) 72%, transparent);
+ &::backdrop {
+ background: color-mix(in oklch, var(--black) 72%, transparent);
+ }
}
-
}
diff --git a/website_next/wallets/empty/style.css b/website_next/wallets/empty/style.css
index 46cc8b080..ab1fd0b7b 100644
--- a/website_next/wallets/empty/style.css
+++ b/website_next/wallets/empty/style.css
@@ -6,9 +6,14 @@ main.wallets {
min-height: 16rem;
text-align: center;
- h2,
p {
margin: 0;
}
+
+ > button {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+ }
}
}
diff --git a/website_next/wallets/form/index.js b/website_next/wallets/form/index.js
index a8959c44c..de352783c 100644
--- a/website_next/wallets/form/index.js
+++ b/website_next/wallets/form/index.js
@@ -1,12 +1,10 @@
-import { createElement } from "../dom.js";
-
/**
* @param {string} label
* @param {HTMLInputElement | HTMLSelectElement} control
*/
export function createField(label, control) {
- const element = createElement("label", "wallets__field");
- const text = createElement("span", "wallets__label");
+ const element = document.createElement("label");
+ const text = document.createElement("span");
text.append(label);
element.append(text, control);
diff --git a/website_next/wallets/form/style.css b/website_next/wallets/form/style.css
index 87a628b87..06e2e3892 100644
--- a/website_next/wallets/form/style.css
+++ b/website_next/wallets/form/style.css
@@ -1,15 +1,14 @@
main.wallets {
- .wallets__field {
+ label {
display: grid;
gap: 0.375rem;
min-width: 0;
- }
- .wallets__label {
- color: var(--gray);
- font-size: var(--font-size-xs);
- line-height: var(--line-height-xs);
- text-transform: uppercase;
+ > span {
+ color: var(--gray);
+ font-size: var(--font-size-xs);
+ line-height: var(--line-height-xs);
+ text-transform: uppercase;
+ }
}
-
}
diff --git a/website_next/wallets/format.js b/website_next/wallets/format.js
index 5b5d25830..2f062797e 100644
--- a/website_next/wallets/format.js
+++ b/website_next/wallets/format.js
@@ -5,15 +5,6 @@ export function formatNumber(value) {
return new Intl.NumberFormat("en-US").format(value);
}
-/**
- * @param {number} sats
- */
-export function formatBtc(sats) {
- return `${(sats / 100_000_000).toLocaleString("en-US", {
- maximumFractionDigits: 8,
- })} BTC`;
-}
-
/**
* @param {number} dollars
*/
diff --git a/website_next/wallets/index.js b/website_next/wallets/index.js
index aa55553c6..dae28b35a 100644
--- a/website_next/wallets/index.js
+++ b/website_next/wallets/index.js
@@ -10,20 +10,19 @@ import { createAddForm } from "./add/index.js";
import { createLayout } from "./layout/index.js";
import { createLock } from "./lock/index.js";
import { redaction } from "./redaction/index.js";
-import { inferAddressScript } from "./add/inference.js";
import { readWalletSourceText } from "./add/source.js";
import { scanStatus } from "./wallet/status.js";
import { createSelector } from "./selector/index.js";
-import { renderSettings } from "./wallet/settings/index.js";
import { createSetup } from "./setup/index.js";
import {
createWalletPanel,
renderWalletPanel,
} from "./wallet/index.js";
import { createVault } from "./vault/index.js";
+import { generateAddressesFromWalletSource } from "./derive/index.js";
+import { syncBtcAmounts } from "./amount/index.js";
/**
- * @typedef {import("./derive/address.js").AddressScript} AddressScript
* @typedef {import("./scan/index.js").WalletScan} WalletScan
* @typedef {import("./vault/index.js").StoredWallet} StoredWallet
* @typedef {import("./vault/index.js").WalletRuntime} WalletRuntime
@@ -83,6 +82,7 @@ export function createWalletsPage() {
privacyButton.addEventListener("click", () => {
redaction.toggle(main, privacyButton, createGroupedAddress);
+ syncBtcAmounts(main);
});
lockButton.addEventListener("click", () => {
@@ -120,11 +120,6 @@ export function createWalletsPage() {
const panel = createWalletPanel();
content.replaceChildren(...panel.nodes);
- renderSettings(panel.settings, wallet, {
- onScriptChange(script, select, status) {
- return updateScript(wallet, script, select, status);
- },
- });
if (runtime.scan) {
renderWalletData(runtime.scan, panel);
@@ -135,7 +130,6 @@ export function createWalletsPage() {
scanStatus.setPending(panel.status);
void runtime.load({
client: brk,
- script: wallet.script,
onProgress(progress) {
scanStatus.setProgress(panel.status, progress);
},
@@ -209,34 +203,6 @@ export function createWalletsPage() {
});
}
- /**
- * @param {StoredWallet} wallet
- * @param {AddressScript} script
- * @param {HTMLSelectElement} select
- * @param {HTMLElement} status
- */
- async function updateScript(
- wallet,
- script,
- select,
- status,
- ) {
- if (script === wallet.script) return;
-
- select.disabled = true;
- setStatus(status, "Saving");
-
- try {
- await vault.updateWalletScript(wallet, script);
- render();
- } catch (error) {
- select.value = wallet.script;
- setStatus(status, getErrorMessage(error));
- } finally {
- select.disabled = false;
- }
- }
-
function renderContent() {
const needsSetup = vault.needsSetup();
const locked = vault.isLocked();
@@ -296,18 +262,18 @@ export function createWalletsPage() {
form,
}) {
await withBusy(submit, "Add", "Adding", async () => {
- setStatus(status, "Checking address type");
+ setStatus(status, "Checking wallet");
try {
const value = readWalletSourceText(source.value);
- const script = await inferAddressScript(brk, value);
+
+ await generateAddressesFromWalletSource(value, { count: 1 });
setStatus(status, "Saving");
await vault.addWallet({
name: name.value,
source: value,
- script,
});
form.reset();
diff --git a/website_next/wallets/layout/index.js b/website_next/wallets/layout/index.js
index 4c928cbcc..350ee0144 100644
--- a/website_next/wallets/layout/index.js
+++ b/website_next/wallets/layout/index.js
@@ -18,14 +18,14 @@ import { createElement } from "../dom.js";
*/
export function createLayout() {
const main = createElement("main", "wallets");
- const header = createElement("header", "wallets__header");
- const actions = createElement("div", "wallets__actions");
+ const header = document.createElement("header");
+ const actions = document.createElement("div");
const addButton = document.createElement("button");
const privacyButton = document.createElement("button");
const lockButton = document.createElement("button");
const selector = createElement("section", "wallets__selector");
- const walletList = createElement("div", "wallets__wallet-list");
- const content = createElement("section", "wallets__content");
+ const walletList = document.createElement("div");
+ const content = document.createElement("section");
const addDialog = createElement("dialog", "wallets__dialog");
addButton.type = "button";
diff --git a/website_next/wallets/layout/style.css b/website_next/wallets/layout/style.css
index b25d3895d..0dd63ff3f 100644
--- a/website_next/wallets/layout/style.css
+++ b/website_next/wallets/layout/style.css
@@ -1,32 +1,34 @@
main.wallets {
- .wallets__header {
+ > header {
display: flex;
gap: 1rem;
align-items: center;
justify-content: end;
+
+ @media (max-width: 34rem) {
+ justify-content: start;
+ }
+
+ > div {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: end;
+
+ @media (max-width: 34rem) {
+ justify-content: start;
+ }
+
+ > button {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+ }
+ }
}
- .wallets__actions {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- justify-content: end;
- }
-
- .wallets__content {
+ > section[aria-live] {
display: grid;
gap: 1.5rem;
}
}
-
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__header {
- justify-content: start;
- }
-
- .wallets__actions {
- justify-content: start;
- }
- }
-}
diff --git a/website_next/wallets/lock/index.js b/website_next/wallets/lock/index.js
index 1bdb69701..5a4311101 100644
--- a/website_next/wallets/lock/index.js
+++ b/website_next/wallets/lock/index.js
@@ -6,26 +6,80 @@ import { createElement } from "../dom.js";
* @property {() => void} onReset
*/
+const RESET_HOLD_MS = 2_000;
+
+/**
+ * @param {HTMLButtonElement} button
+ * @param {() => void} onReset
+ */
+function bindResetHold(button, onReset) {
+ /** @type {number | undefined} */
+ let timer;
+
+ function cancel() {
+ if (timer === undefined) return;
+
+ clearTimeout(timer);
+ timer = undefined;
+ button.removeAttribute("data-wallets-holding");
+ }
+
+ function start() {
+ if (timer !== undefined) return;
+
+ button.setAttribute("data-wallets-holding", "");
+ timer = window.setTimeout(() => {
+ timer = undefined;
+ button.removeAttribute("data-wallets-holding");
+ onReset();
+ }, RESET_HOLD_MS);
+ }
+
+ button.addEventListener("pointerdown", (event) => {
+ if (event.button !== 0) return;
+
+ button.setPointerCapture(event.pointerId);
+ start();
+ });
+ button.addEventListener("pointerup", cancel);
+ button.addEventListener("pointercancel", cancel);
+ button.addEventListener("lostpointercapture", cancel);
+ button.addEventListener("keydown", (event) => {
+ if (event.repeat || (event.key !== " " && event.key !== "Enter")) return;
+
+ event.preventDefault();
+ start();
+ });
+ button.addEventListener("keyup", (event) => {
+ if (event.key === " " || event.key === "Enter") {
+ cancel();
+ }
+ });
+ button.addEventListener("blur", cancel);
+}
+
/**
* @param {LockOptions} options
*/
export function createLock(options) {
const section = createElement("section", "wallets__unlock");
- const form = createElement("form", "wallets__unlock-form");
+ const title = document.createElement("h1");
+ const form = document.createElement("form");
const password = document.createElement("input");
const button = document.createElement("button");
const reset = document.createElement("button");
- const status = createElement("p", "wallets__status");
+ const status = document.createElement("p");
+ title.append("Unlock vault");
password.name = "password";
password.type = "password";
password.autocomplete = "current-password";
+ password.autofocus = true;
password.placeholder = "Password";
password.required = true;
button.type = "submit";
button.append("Unlock");
reset.type = "button";
- reset.className = "wallets__reset";
reset.append("Reset vault");
status.setAttribute("role", "status");
form.append(password, button);
@@ -33,8 +87,11 @@ export function createLock(options) {
event.preventDefault();
void options.onUnlock(password.value, button, status);
});
- reset.addEventListener("click", options.onReset);
- section.append(form, reset, status);
+ bindResetHold(reset, options.onReset);
+ section.append(title, form, reset, status);
+ queueMicrotask(() => {
+ password.focus({ preventScroll: true });
+ });
return section;
}
diff --git a/website_next/wallets/lock/style.css b/website_next/wallets/lock/style.css
index 2d680f03c..84b61ac2e 100644
--- a/website_next/wallets/lock/style.css
+++ b/website_next/wallets/lock/style.css
@@ -6,31 +6,53 @@ main.wallets {
min-height: 16rem;
text-align: center;
- h2,
- p {
+ > h1 {
margin: 0;
+ font-size: 3rem;
+ font-weight: 400;
+ line-height: 1;
}
- }
- .wallets__unlock-form {
- display: grid;
- grid-template-columns: minmax(12rem, 18rem) auto;
- gap: 0.75rem;
- align-items: end;
- justify-content: center;
- }
+ > form {
+ display: grid;
+ grid-template-columns: minmax(12rem, 18rem) auto;
+ gap: 0.75rem;
+ align-items: end;
+ justify-content: center;
- .wallets__reset {
- justify-self: center;
- color: var(--gray);
- background: transparent;
- }
-}
+ @media (max-width: 34rem) {
+ grid-template-columns: 1fr;
+ }
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__unlock-form {
- grid-template-columns: 1fr;
+ > button {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+ }
+ }
+
+ > button {
+ position: relative;
+ isolation: isolate;
+ justify-self: center;
+ overflow: hidden;
+ color: var(--gray);
+ background: transparent;
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: color-mix(in oklch, var(--red) 34%, transparent);
+ transform: scaleX(0);
+ transform-origin: left;
+ }
+
+ &[data-wallets-holding]::before {
+ transform: scaleX(1);
+ transition: transform 2s linear;
+ }
}
}
}
diff --git a/website_next/wallets/scan/index.js b/website_next/wallets/scan/index.js
index 0eda95900..1a239ff94 100644
--- a/website_next/wallets/scan/index.js
+++ b/website_next/wallets/scan/index.js
@@ -1,9 +1,13 @@
import { scanBranches } from "./branches.js";
+import { isOutputDescriptor } from "../derive/index.js";
+import { parseOutputDescriptor } from "../derive/descriptor.js";
+import { addressScripts } from "../derive/script.js";
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {Awaited>["addresses"][number]} WalletAddress
+ * @typedef {Awaited>} ScriptScan
*/
/**
@@ -27,32 +31,116 @@ import { scanBranches } from "./branches.js";
* @property {number} unusedInRow
*/
+/**
+ * @typedef {Object} ScanScript
+ * @property {AddressScript} id
+ * @property {string} label
+ */
+
+const descriptorScripts = /** @type {const} */ ({
+ v0_p2wsh_sortedmulti: "P2WSH",
+});
+
+/**
+ * @param {string} source
+ * @returns {readonly ScanScript[]}
+ */
+function getSourceScripts(source) {
+ if (isOutputDescriptor(source)) {
+ const script = parseOutputDescriptor(source).script;
+
+ return [{
+ id: script,
+ label: descriptorScripts[script],
+ }];
+ }
+
+ return addressScripts;
+}
+
+/**
+ * @param {WalletAddress} a
+ * @param {WalletAddress} b
+ */
+function compareWalletAddresses(a, b) {
+ return (
+ (b.typeIndex ?? -1) - (a.typeIndex ?? -1) ||
+ a.script.localeCompare(b.script) ||
+ a.branchLabel.localeCompare(b.branchLabel) ||
+ a.index - b.index
+ );
+}
+
+/**
+ * @param {ScriptScan} scan
+ */
+function getLatestSeenIndex(scan) {
+ return scan.addresses.reduce((latest, address) => {
+ return Math.max(latest, address.typeIndex ?? -1);
+ }, -1);
+}
+
+/**
+ * @param {readonly ScriptScan[]} scans
+ */
+function selectReceiveAddress(scans) {
+ let receiveAddress = scans.find((scan) => {
+ return scan.receiveAddress;
+ })?.receiveAddress;
+ let selectedSeenIndex = -1;
+
+ for (const scan of scans) {
+ const seenIndex = getLatestSeenIndex(scan);
+ const hasActivity = scan.addresses.length > 0;
+
+ if (
+ hasActivity &&
+ scan.receiveAddress &&
+ seenIndex >= selectedSeenIndex
+ ) {
+ receiveAddress = scan.receiveAddress;
+ selectedSeenIndex = seenIndex;
+ }
+ }
+
+ return receiveAddress;
+}
+
/**
* @param {Object} options
* @param {WalletScanClient} options.client
* @param {string} options.source
- * @param {AddressScript} options.script
* @param {(progress: WalletScanProgress) => void} [options.onProgress]
* @returns {Promise}
*/
export async function scanWalletAddresses({
client,
source,
- script,
onProgress,
}) {
- const scan = await scanBranches(client, source, {
- script,
- onProgress,
- });
- const addresses = /** @type {WalletAddress[]} */ (scan.addresses);
+ const scans = /** @type {ScriptScan[]} */ ([]);
+
+ for (const script of getSourceScripts(source)) {
+ scans.push(await scanBranches(client, source, {
+ script: script.id,
+ onProgress(progress) {
+ onProgress?.({
+ ...progress,
+ branchLabel: `${script.label} ${progress.branchLabel}`,
+ });
+ },
+ }));
+ }
+
+ const addresses = scans.flatMap((scan) => scan.addresses)
+ .sort(compareWalletAddresses);
const btcUsdPrice = /** @type {number} */ (
await client.getLivePrice({ cache: false })
);
return {
addresses,
- receiveAddress: scan.receiveAddress,
+ receiveAddress: selectReceiveAddress(scans),
btcUsdPrice,
};
}
diff --git a/website_next/wallets/selector/index.js b/website_next/wallets/selector/index.js
index b04a4e705..9460f4a20 100644
--- a/website_next/wallets/selector/index.js
+++ b/website_next/wallets/selector/index.js
@@ -23,7 +23,6 @@ function renderButtons(walletList, wallets, options) {
const selected = wallet.id === options.getSelectedId();
button.type = "button";
- button.className = "wallets__wallet-button";
button.setAttribute("aria-pressed", selected ? "true" : "false");
button.setAttribute("data-wallet-id", wallet.id);
button.append(wallet.name);
@@ -40,7 +39,7 @@ function renderButtons(walletList, wallets, options) {
*/
export function createSelector(walletList, options) {
function selectSnappedWallet() {
- const buttons = [...walletList.querySelectorAll(".wallets__wallet-button")];
+ const buttons = [...walletList.querySelectorAll("button")];
if (buttons.length === 0) return;
diff --git a/website_next/wallets/selector/style.css b/website_next/wallets/selector/style.css
index 69b79b7de..88505fdaa 100644
--- a/website_next/wallets/selector/style.css
+++ b/website_next/wallets/selector/style.css
@@ -1,56 +1,43 @@
main.wallets {
.wallets__selector {
min-width: 0;
- }
- .wallets__wallet-list {
- display: flex;
- gap: 1rem;
- min-width: 0;
- overflow-x: auto;
- padding-bottom: 0.25rem;
- overscroll-behavior-inline: contain;
- scroll-padding-inline: var(--page-x);
- scroll-snap-type: x proximity;
- }
+ > div {
+ display: flex;
+ gap: 1rem;
+ min-width: 0;
+ overflow-x: auto;
+ padding-bottom: 0.25rem;
+ overscroll-behavior-inline: contain;
+ scroll-padding-inline: var(--page-x);
+ scroll-snap-type: x proximity;
- .wallets__wallet-button {
- flex: 0 0 auto;
- scroll-snap-align: center;
- border: 0;
- padding: 0;
- color: var(--white);
- background: transparent;
- font-family: var(--font-serif);
- font-size: 4rem;
- font-weight: 400;
- line-height: 1;
- opacity: 0.48;
- cursor: pointer;
- }
+ > button {
+ flex: 0 0 auto;
+ height: auto;
+ scroll-snap-align: center;
+ border: 0;
+ padding: 0;
+ color: var(--white);
+ background: transparent;
+ font-size: 4rem;
+ font-weight: 400;
+ line-height: 1;
+ opacity: 0.48;
+ cursor: pointer;
- .wallets__wallet-button[aria-pressed="true"] {
- opacity: 1;
- }
+ @media (max-width: 56rem) {
+ font-size: 3rem;
+ }
- .wallets__wallet-button:focus-visible {
- outline: 2px solid var(--orange);
- outline-offset: 2px;
- }
-}
+ @media (max-width: 34rem) {
+ font-size: 2.5rem;
+ }
-@media (max-width: 56rem) {
- main.wallets {
- .wallets__wallet-button {
- font-size: 3rem;
- }
- }
-}
-
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__wallet-button {
- font-size: 2.5rem;
+ &[aria-pressed="true"] {
+ opacity: 1;
+ }
+ }
}
}
}
diff --git a/website_next/wallets/setup/index.js b/website_next/wallets/setup/index.js
index b762c0bd8..73e7db466 100644
--- a/website_next/wallets/setup/index.js
+++ b/website_next/wallets/setup/index.js
@@ -22,11 +22,11 @@ function createDescriptionText(text) {
export function createSetup(options) {
const section = createElement("section", "wallets__setup");
const title = document.createElement("h1");
- const description = createElement("div", "wallets__setup-description");
- const form = createElement("form", "wallets__setup-form");
+ const description = document.createElement("div");
+ const form = document.createElement("form");
const password = document.createElement("input");
const button = document.createElement("button");
- const status = createElement("p", "wallets__status");
+ const status = document.createElement("p");
title.append("Wallets");
description.append(
diff --git a/website_next/wallets/setup/style.css b/website_next/wallets/setup/style.css
index aea280acf..2460f77e2 100644
--- a/website_next/wallets/setup/style.css
+++ b/website_next/wallets/setup/style.css
@@ -6,58 +6,53 @@ main.wallets {
max-width: 36rem;
min-height: 16rem;
margin-inline: auto;
- text-align: center;
h1 {
margin: 0;
- font-family: var(--font-serif);
+ text-align: center;
font-size: 5rem;
font-weight: 400;
line-height: 0.9;
+
+ @media (max-width: 56rem) {
+ font-size: 4rem;
+ }
+
+ @media (max-width: 34rem) {
+ font-size: 3rem;
+ }
}
p {
margin: 0;
}
- }
- .wallets__setup-description {
- display: grid;
- gap: 0.625rem;
- color: var(--gray);
- font-size: var(--font-size-md);
- line-height: var(--line-height-md);
- }
+ > div {
+ display: grid;
+ gap: 0.75rem;
+ color: var(--gray);
+ font-size: var(--font-size-sm);
+ line-height: var(--line-height-sm);
+ text-align: left;
+ }
- .wallets__setup-form {
- display: grid;
- grid-template-columns: minmax(12rem, 18rem) auto;
- gap: 0.75rem;
- align-items: end;
- justify-content: center;
- }
-}
+ > form {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.75rem;
+ align-items: end;
+ justify-content: center;
+ width: 100%;
-@media (max-width: 56rem) {
- main.wallets {
- .wallets__setup {
- h1 {
- font-size: 4rem;
+ @media (max-width: 34rem) {
+ grid-template-columns: 1fr;
+ }
+
+ > button {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
}
}
}
}
-
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__setup {
- h1 {
- font-size: 3rem;
- }
- }
-
- .wallets__setup-form {
- grid-template-columns: 1fr;
- }
- }
-}
diff --git a/website_next/wallets/style.css b/website_next/wallets/style.css
index 287f95ca8..2220c9852 100644
--- a/website_next/wallets/style.css
+++ b/website_next/wallets/style.css
@@ -16,7 +16,7 @@ main.wallets {
align-content: center;
}
- .wallets__status {
+ [role="status"] {
min-height: var(--line-height-sm);
margin: 0;
color: var(--gray);
@@ -24,8 +24,11 @@ main.wallets {
line-height: var(--line-height-sm);
}
- :is(input, select),
- button:not(.wallets__wallet-button) {
+ [data-wallets-btc-muted] {
+ color: color-mix(in oklch, currentColor 45%, transparent);
+ }
+
+ :is(input, select, button) {
min-width: 0;
height: var(--control-height);
border: 1px solid color-mix(in oklch, var(--gray) 45%, transparent);
@@ -37,11 +40,11 @@ main.wallets {
line-height: 1;
}
- button:not(.wallets__wallet-button) {
+ button {
cursor: pointer;
}
- :is(input, select, button:not(.wallets__wallet-button)):focus-visible {
+ :is(input, select, button):focus-visible {
outline: 2px solid var(--orange);
outline-offset: 2px;
}
@@ -50,21 +53,7 @@ main.wallets {
color: color-mix(in oklch, var(--gray) 70%, transparent);
}
- :is(
- .wallets__actions,
- .wallets__empty,
- .wallets__setup-form,
- .wallets__unlock-form
- ) button,
- .wallets__dialog-form button[type="submit"],
- .wallets__receive-button,
- .wallets__receive-actions button:first-child {
- border-color: var(--orange);
- color: var(--black);
- background: var(--orange);
- }
-
- button:not(.wallets__wallet-button):disabled {
+ button:disabled {
border-color: var(--gray);
color: var(--black);
background: var(--gray);
diff --git a/website_next/wallets/vault/index.js b/website_next/wallets/vault/index.js
index dbe1f9a87..80984ec97 100644
--- a/website_next/wallets/vault/index.js
+++ b/website_next/wallets/vault/index.js
@@ -4,7 +4,6 @@ import { createRuntime } from "./runtime.js";
/**
* @typedef {import("./storage.js").StoredWallet} StoredWallet
* @typedef {import("./storage.js").AddWalletInput} AddWalletInput
- * @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {ReturnType} WalletRuntime
*/
@@ -116,19 +115,6 @@ export function createVault() {
runtimes.set(added.wallet.id, createRuntime(added.wallet.source));
}
- /**
- * @param {StoredWallet} wallet
- * @param {AddressScript} script
- */
- async function updateWalletScript(wallet, script) {
- wallets = await vaultStorage.updateWalletScript(wallets, {
- walletId: wallet.id,
- script,
- }, password);
- runtimes.set(wallet.id, createRuntime(wallet.source));
- syncSelected();
- }
-
return {
get wallets() {
return wallets;
@@ -153,6 +139,5 @@ export function createVault() {
setup,
unlock,
addWallet,
- updateWalletScript,
};
}
diff --git a/website_next/wallets/vault/runtime.js b/website_next/wallets/vault/runtime.js
index 2c0c96274..cfec880c0 100644
--- a/website_next/wallets/vault/runtime.js
+++ b/website_next/wallets/vault/runtime.js
@@ -4,11 +4,9 @@ import { scanWalletAddresses } from "../scan/index.js";
* @typedef {import("../scan/index.js").WalletScan} WalletScan
* @typedef {import("../scan/index.js").WalletScanClient} WalletScanClient
* @typedef {import("../scan/index.js").WalletScanProgress} WalletScanProgress
- * @typedef {import("../derive/address.js").AddressScript} AddressScript
*
* @typedef {Object} LoadOptions
* @property {WalletScanClient} client
- * @property {AddressScript} script
* @property {(progress: WalletScanProgress) => void} [onProgress]
*/
@@ -31,7 +29,6 @@ export function createRuntime(source) {
pending = scanWalletAddresses({
client: options.client,
source,
- script: options.script,
onProgress: options.onProgress,
}).then((nextScan) => {
scan = nextScan;
diff --git a/website_next/wallets/vault/storage.js b/website_next/wallets/vault/storage.js
index 44a3cdebe..5f7e5b7dd 100644
--- a/website_next/wallets/vault/storage.js
+++ b/website_next/wallets/vault/storage.js
@@ -4,14 +4,12 @@ const STORAGE_KEY = "bitview.wallets.v3";
/**
* @typedef {import("./encryption.js").EncryptedSecret} EncryptedSecret
- * @typedef {import("../derive/address.js").AddressScript} AddressScript
*/
/**
* @typedef {Object} StoredWallet
* @property {string} id
* @property {string} name
- * @property {AddressScript} script
* @property {string} source
* @property {number} createdAt
* @property {number} updatedAt
@@ -20,16 +18,9 @@ const STORAGE_KEY = "bitview.wallets.v3";
/**
* @typedef {Object} AddWalletInput
* @property {string} name
- * @property {AddressScript} script
* @property {string} source
*/
-/**
- * @typedef {Object} UpdateWalletScriptInput
- * @property {string} walletId
- * @property {AddressScript} script
- */
-
/**
* @typedef {Object} WalletVault
* @property {StoredWallet[]} wallets
@@ -120,7 +111,6 @@ async function addWallet(wallets, input, pagePassword) {
const wallet = {
id: createWalletId(),
name: input.name.trim(),
- script: input.script,
source: input.source.trim(),
createdAt: time,
updatedAt: time,
@@ -135,33 +125,10 @@ async function addWallet(wallets, input, pagePassword) {
};
}
-/**
- * @param {StoredWallet[]} wallets
- * @param {UpdateWalletScriptInput} input
- * @param {string} pagePassword
- */
-async function updateWalletScript(wallets, input, pagePassword) {
- const time = now();
- const nextWallets = wallets.map((wallet) => {
- return wallet.id === input.walletId
- ? {
- ...wallet,
- script: input.script,
- updatedAt: time,
- }
- : wallet;
- });
-
- await writeWallets(nextWallets, pagePassword);
-
- return nextWallets;
-}
-
export const vaultStorage = /** @type {const} */ ({
has,
reset,
setup,
load,
addWallet,
- updateWalletScript,
});
diff --git a/website_next/wallets/wallet/actions/style.css b/website_next/wallets/wallet/actions/style.css
new file mode 100644
index 000000000..b1a36da48
--- /dev/null
+++ b/website_next/wallets/wallet/actions/style.css
@@ -0,0 +1,26 @@
+main.wallets {
+ .wallets__wallet-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ align-items: end;
+ justify-content: end;
+
+ @media (max-width: 34rem) {
+ justify-content: start;
+ }
+
+ > button {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+
+ &:disabled {
+ border-color: color-mix(in oklch, var(--gray) 35%, transparent);
+ color: var(--gray);
+ background: transparent;
+ cursor: default;
+ }
+ }
+ }
+}
diff --git a/website_next/wallets/wallet/address/index.js b/website_next/wallets/wallet/address/index.js
index 6943869f2..375bfc52a 100644
--- a/website_next/wallets/wallet/address/index.js
+++ b/website_next/wallets/wallet/address/index.js
@@ -14,14 +14,14 @@ export function createGroupedAddress(text) {
const groups = text.match(/.{1,4}/g) ?? [];
for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
- const group = createElement("span", "wallets__address-group");
+ const group = document.createElement("span");
for (const character of groups[groupIndex]) {
- const span = createElement(
- "span",
- Number.isNaN(Number(character))
- ? "wallets__address-letter"
- : "wallets__address-number",
+ const span = document.createElement("span");
+
+ span.setAttribute(
+ "data-wallets-address-character",
+ Number.isNaN(Number(character)) ? "letter" : "number",
);
span.append(character);
@@ -55,7 +55,7 @@ function createPrivateAddress(address) {
* @param {WalletAddress} row
*/
function createAddressBadge(row) {
- const badge = createElement("span", "wallets__address-badge");
+ const badge = document.createElement("span");
const label = row.branchLabel?.toLowerCase() ?? "address";
badge.setAttribute("data-wallets-address-branch", label);
@@ -69,7 +69,7 @@ function createAddressBadge(row) {
*/
export function createAddressCellContent(row) {
const element = createElement("div", "wallets__address-cell");
- const anonSet = createElement("span", "wallets__address-meta");
+ const anonSet = document.createElement("span");
anonSet.append(`anon set: ${formatNumber(row.historyBucketSize)}`);
element.append(
diff --git a/website_next/wallets/wallet/address/style.css b/website_next/wallets/wallet/address/style.css
index 5bc1b58d1..fa336fadf 100644
--- a/website_next/wallets/wallet/address/style.css
+++ b/website_next/wallets/wallet/address/style.css
@@ -2,24 +2,24 @@ main.wallets {
.wallets__address-cell {
display: grid;
gap: 0.25rem;
- }
- .wallets__address-meta {
- color: var(--gray);
- font-size: var(--font-size-xs);
- line-height: var(--line-height-xs);
- }
+ > span:first-child {
+ display: inline-flex;
+ align-items: center;
+ justify-self: start;
+ min-height: 1rem;
+ border: 1px solid color-mix(in oklch, var(--gray) 28%, transparent);
+ border-radius: 0.25rem;
+ padding: 0 0.25rem;
+ color: color-mix(in oklch, var(--white) 76%, var(--gray));
+ line-height: 1;
+ }
- .wallets__address-badge {
- display: inline-flex;
- align-items: center;
- justify-self: start;
- min-height: 1rem;
- border: 1px solid color-mix(in oklch, var(--gray) 28%, transparent);
- border-radius: 0.25rem;
- padding: 0 0.25rem;
- color: color-mix(in oklch, var(--white) 76%, var(--gray));
- line-height: 1;
+ > span:last-child {
+ color: var(--gray);
+ font-size: var(--font-size-xs);
+ line-height: var(--line-height-xs);
+ }
}
.wallets__address {
@@ -27,17 +27,17 @@ main.wallets {
flex-wrap: wrap;
gap: 0 0.375rem;
max-width: 40rem;
- }
- .wallets__address-group {
- white-space: nowrap;
- }
+ > span {
+ white-space: nowrap;
+ }
- .wallets__address-letter {
- color: var(--white);
- }
+ [data-wallets-address-character="letter"] {
+ color: var(--white);
+ }
- .wallets__address-number {
- color: color-mix(in oklch, var(--white) 50%, var(--gray));
+ [data-wallets-address-character="number"] {
+ color: color-mix(in oklch, var(--white) 50%, var(--gray));
+ }
}
}
diff --git a/website_next/wallets/wallet/index.js b/website_next/wallets/wallet/index.js
index 3cf395016..2901a16f0 100644
--- a/website_next/wallets/wallet/index.js
+++ b/website_next/wallets/wallet/index.js
@@ -9,7 +9,7 @@ import { renderTransactions } from "./transactions/index.js";
* @typedef {Parameters[0]} TransactionClient
*
* @typedef {Object} WalletPanel
- * @property {HTMLElement} settings
+ * @property {HTMLElement} actions
* @property {HTMLElement} summary
* @property {HTMLElement} status
* @property {HTMLElement} results
@@ -20,22 +20,22 @@ import { renderTransactions } from "./transactions/index.js";
* @returns {WalletPanel}
*/
export function createWalletPanel() {
- const settings = createElement("section", "wallets__settings");
+ const actions = createElement("section", "wallets__wallet-actions");
const summary = createElement("section", "wallets__summary");
- const status = createElement("p", "wallets__status");
+ const status = document.createElement("p");
const results = createElement("section", "wallets__results");
- settings.setAttribute("aria-label", "Wallet settings");
+ actions.setAttribute("aria-label", "Wallet actions");
status.setAttribute("role", "status");
summary.setAttribute("aria-label", "Wallets summary");
results.setAttribute("aria-label", "Wallets results");
return {
- settings,
+ actions,
summary,
status,
results,
- nodes: [settings, summary, status, results],
+ nodes: [actions, summary, status, results],
};
}
@@ -46,7 +46,7 @@ export function createWalletPanel() {
*/
export function renderWalletPanel(scan, panel, client) {
renderWalletSummary(panel.summary, scan.addresses, scan.btcUsdPrice);
- renderReceiveButton(panel.settings, scan.receiveAddress);
+ renderReceiveButton(panel.actions, scan.receiveAddress);
panel.results.replaceChildren("Loading activity");
void transactionCache.load(client, scan.addresses).then((transactions) => {
if (panel.results.isConnected) {
diff --git a/website_next/wallets/wallet/receive/index.js b/website_next/wallets/wallet/receive/index.js
index e721b2cbe..01ff2ea74 100644
--- a/website_next/wallets/wallet/receive/index.js
+++ b/website_next/wallets/wallet/receive/index.js
@@ -46,7 +46,6 @@ function createReceiveQr(receiveAddress) {
const image = document.createElement("img");
const uri = `bitcoin:${receiveAddress.address}`;
- image.className = "wallets__receive-qr";
image.alt = `QR code for ${receiveAddress.address}`;
image.src = createQrDataUrl(uri);
@@ -57,7 +56,7 @@ function createReceiveQr(receiveAddress) {
* @param {ReceiveAddress} receiveAddress
*/
function createReceiveAddress(receiveAddress) {
- const element = createElement("div", "wallets__receive-address");
+ const element = document.createElement("div");
element.append(createGroupedAddress(receiveAddress.address));
@@ -82,8 +81,8 @@ function openReceiveDialog(receiveAddress) {
"dialog",
"wallets__dialog wallets__receive-dialog",
);
- const content = createElement("div", "wallets__receive-card");
- const actions = createElement("div", "wallets__receive-actions");
+ const content = document.createElement("div");
+ const actions = document.createElement("div");
const copy = document.createElement("button");
const close = document.createElement("button");
@@ -128,7 +127,6 @@ export function renderReceiveButton(element, receiveAddress) {
const button = document.createElement("button");
button.type = "button";
- button.className = "wallets__receive-button";
button.disabled = !receiveAddress;
button.append("Receive");
button.addEventListener("click", () => {
diff --git a/website_next/wallets/wallet/receive/style.css b/website_next/wallets/wallet/receive/style.css
index 8e7542797..1beb16b43 100644
--- a/website_next/wallets/wallet/receive/style.css
+++ b/website_next/wallets/wallet/receive/style.css
@@ -1,46 +1,45 @@
main.wallets {
- .wallets__receive-button:disabled {
- border-color: color-mix(in oklch, var(--gray) 35%, transparent);
- color: var(--gray);
- background: transparent;
- cursor: default;
- }
-
.wallets__receive-dialog {
width: min(100% - 2rem, 32rem);
- }
- .wallets__receive-card {
- display: grid;
- gap: 1rem;
+ > div {
+ display: grid;
+ gap: 1rem;
- h2 {
- margin: 0;
- font-size: var(--font-size-lg);
- font-weight: 400;
- line-height: var(--line-height-lg);
+ h2 {
+ margin: 0;
+ font-size: var(--font-size-lg);
+ font-weight: 400;
+ line-height: var(--line-height-lg);
+ }
+
+ > img {
+ justify-self: center;
+ width: min(100%, 18rem);
+ aspect-ratio: 1;
+ padding: 1rem;
+ background: var(--white);
+ image-rendering: pixelated;
+ }
+
+ > div:first-of-type {
+ color: var(--white);
+ font-size: var(--font-size-sm);
+ line-height: var(--line-height-sm);
+ }
+
+ > div:last-of-type {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: end;
+
+ > button:first-child {
+ border-color: var(--orange);
+ color: var(--black);
+ background: var(--orange);
+ }
+ }
}
}
-
- .wallets__receive-qr {
- justify-self: center;
- width: min(100%, 18rem);
- aspect-ratio: 1;
- padding: 1rem;
- background: var(--white);
- image-rendering: pixelated;
- }
-
- .wallets__receive-address {
- color: var(--white);
- font-size: var(--font-size-sm);
- line-height: var(--line-height-sm);
- }
-
- .wallets__receive-actions {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- justify-content: end;
- }
}
diff --git a/website_next/wallets/wallet/settings/index.js b/website_next/wallets/wallet/settings/index.js
deleted file mode 100644
index 6d4ed6fba..000000000
--- a/website_next/wallets/wallet/settings/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- createAddressScriptSelect,
- readAddressScript,
-} from "./script.js";
-import { createElement } from "../../dom.js";
-import { createField } from "../../form/index.js";
-import { isOutputDescriptor } from "../../derive/index.js";
-
-/**
- * @typedef {import("../../derive/address.js").AddressScript} AddressScript
- * @typedef {import("../../vault/index.js").StoredWallet} StoredWallet
- */
-
-/**
- * @typedef {Object} WalletSettingsOptions
- * @property {(script: AddressScript, select: HTMLSelectElement, status: HTMLElement) => void | Promise} onScriptChange
- */
-
-/**
- * @param {HTMLElement} element
- * @param {StoredWallet} wallet
- * @param {WalletSettingsOptions} options
- */
-export function renderSettings(element, wallet, options) {
- if (isOutputDescriptor(wallet.source)) {
- element.replaceChildren();
- return;
- }
-
- const script = createAddressScriptSelect(
- /** @type {AddressScript} */ (wallet.script),
- );
- const status = createElement("p", "wallets__status");
-
- status.setAttribute("role", "status");
- script.addEventListener("change", () => {
- void options.onScriptChange(readAddressScript(script), script, status);
- });
- element.replaceChildren(createField("Address type", script), status);
-}
diff --git a/website_next/wallets/wallet/settings/script.js b/website_next/wallets/wallet/settings/script.js
deleted file mode 100644
index 8dfe7e479..000000000
--- a/website_next/wallets/wallet/settings/script.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { addressScripts } from "../../derive/script.js";
-
-/**
- * @typedef {import("../../derive/address.js").AddressScript} AddressScript
- */
-
-/**
- * @param {AddressScript} [value]
- */
-export function createAddressScriptSelect(value) {
- const select = document.createElement("select");
-
- select.name = "script";
-
- for (const { id, label } of addressScripts) {
- const option = document.createElement("option");
-
- option.value = id;
- option.selected = id === value;
- option.append(label);
- select.append(option);
- }
-
- return select;
-}
-
-/**
- * @param {HTMLSelectElement} select
- * @returns {AddressScript}
- */
-export function readAddressScript(select) {
- return /** @type {AddressScript} */ (select.value);
-}
diff --git a/website_next/wallets/wallet/settings/style.css b/website_next/wallets/wallet/settings/style.css
deleted file mode 100644
index 932b3f99a..000000000
--- a/website_next/wallets/wallet/settings/style.css
+++ /dev/null
@@ -1,21 +0,0 @@
-main.wallets {
- .wallets__settings {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
- align-items: end;
- justify-content: end;
- }
-
- .wallets__settings .wallets__field {
- min-width: min(100%, 14rem);
- }
-}
-
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__settings {
- justify-content: start;
- }
- }
-}
diff --git a/website_next/wallets/wallet/summary/index.js b/website_next/wallets/wallet/summary/index.js
index cd08ad057..31c86cf38 100644
--- a/website_next/wallets/wallet/summary/index.js
+++ b/website_next/wallets/wallet/summary/index.js
@@ -1,5 +1,5 @@
-import { createElement } from "../../dom.js";
-import { formatBtc, formatUsd } from "../../format.js";
+import { createBtcAmount } from "../../amount/index.js";
+import { formatUsd } from "../../format.js";
import { redaction } from "../../redaction/index.js";
/**
@@ -11,8 +11,8 @@ import { redaction } from "../../redaction/index.js";
* @param {number} btcUsdPrice
*/
function createBalanceSummary(balance, btcUsdPrice) {
- const element = createElement("p", "wallets__balance");
- const btc = redaction.createValue("strong", formatBtc(balance), "fixed");
+ const element = document.createElement("p");
+ const btc = createBtcAmount("strong", balance);
const usd = redaction.createValue(
"span",
formatUsd((balance / 100_000_000) * btcUsdPrice),
diff --git a/website_next/wallets/wallet/summary/style.css b/website_next/wallets/wallet/summary/style.css
index df02ce694..978135dd5 100644
--- a/website_next/wallets/wallet/summary/style.css
+++ b/website_next/wallets/wallet/summary/style.css
@@ -1,35 +1,26 @@
main.wallets {
.wallets__summary {
min-height: 5rem;
- }
- .wallets__balance {
- display: grid;
- gap: 0.5rem;
- margin: 0;
+ > p {
+ display: grid;
+ gap: 0.5rem;
+ margin: 0;
- strong {
- min-width: 0;
- overflow-wrap: anywhere;
- color: var(--white);
- font-size: 3rem;
- font-weight: 620;
- line-height: 1;
- }
-
- span {
- color: var(--gray);
- font-size: var(--font-size-lg);
- line-height: var(--line-height-lg);
- }
- }
-}
-
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__balance {
strong {
- font-size: 2.25rem;
+ min-width: 0;
+ overflow-wrap: anywhere;
+ color: var(--white);
+ font-family: var(--font-serif);
+ font-size: 4rem;
+ font-weight: 400;
+ line-height: 1;
+ }
+
+ > span {
+ color: var(--gray);
+ font-size: var(--font-size-lg);
+ line-height: var(--line-height-lg);
}
}
}
diff --git a/website_next/wallets/wallet/transactions/index.js b/website_next/wallets/wallet/transactions/index.js
index b92ea3a58..7a22192f5 100644
--- a/website_next/wallets/wallet/transactions/index.js
+++ b/website_next/wallets/wallet/transactions/index.js
@@ -1,5 +1,5 @@
import { createElement } from "../../dom.js";
-import { formatBtc } from "../../format.js";
+import { createBtcAmount } from "../../amount/index.js";
import { redaction } from "../../redaction/index.js";
import { createAddressCellContent } from "../address/index.js";
@@ -21,107 +21,82 @@ function formatTxid(txid) {
}
/**
- * @param {number} sats
- */
-function formatSignedBtc(sats) {
- if (sats > 0) return `+${formatBtc(sats)}`;
- if (sats < 0) return `-${formatBtc(Math.abs(sats))}`;
-
- return formatBtc(sats);
-}
-
-/**
+ * @param {HTMLElement} element
* @param {WalletTransaction} transaction
*/
-function getTransactionDetail(transaction) {
+function appendTransactionDetail(element, transaction) {
if (transaction.type === "consolidation") {
- return `${transaction.addresses.length} wallet addresses · fee only`;
+ element.append(
+ `${transaction.addresses.length} wallet addresses · fee only`,
+ );
+ return;
}
if (transaction.type === "send") {
- return `to external wallet · fee ${formatBtc(transaction.fee)}`;
+ element.append(
+ "to external wallet · fee ",
+ createBtcAmount("span", transaction.fee),
+ );
+ return;
}
- return transaction.status;
+ element.append(transaction.status);
}
/**
* @param {WalletTransaction} transaction
*/
function createTransactionDetails(transaction) {
- const dialog = createElement("dialog", "wallets__dialog wallets__tx-dialog");
- const content = createElement("div", "wallets__tx-details");
- const title = document.createElement("h2");
+ const content = document.createElement("div");
const txid = document.createElement("code");
const meta = document.createElement("p");
- const list = createElement("div", "wallets__tx-addresses");
- const close = document.createElement("button");
+ const list = document.createElement("div");
- title.append(typeLabels[transaction.type]);
redaction.setTitle(txid, transaction.txid);
redaction.setValue(txid, transaction.txid);
meta.append(
transaction.status,
" · ",
- redaction.createValue("span", formatSignedBtc(transaction.amount), "fixed"),
+ createBtcAmount("span", transaction.amount, { signed: true }),
" · fee ",
- redaction.createValue("span", formatBtc(transaction.fee), "fixed"),
+ createBtcAmount("span", transaction.fee),
);
for (const address of transaction.addresses) {
list.append(createAddressCellContent(address.walletAddress));
}
- close.type = "button";
- close.append("Close");
- content.append(title, txid, meta, list, close);
- dialog.append(content);
- close.addEventListener("click", () => {
- dialog.close();
- });
- dialog.addEventListener("close", () => {
- dialog.remove();
- });
- dialog.addEventListener("click", (event) => {
- if (event.target === dialog) {
- dialog.close();
- }
- });
+ content.append(txid, meta, list);
- return dialog;
+ return content;
}
/**
* @param {WalletTransaction} transaction
*/
function createTransactionRow(transaction) {
- const row = createElement("li", "wallets__tx");
- const main = createElement("div", "wallets__tx-main");
+ const row = document.createElement("li");
+ const main = document.createElement("div");
const label = document.createElement("strong");
- const amount = redaction.createValue(
+ const amount = createBtcAmount(
"span",
- formatSignedBtc(transaction.amount),
- "fixed",
+ transaction.amount,
+ { signed: true },
);
- const detail = createElement("p", "wallets__tx-detail");
+ const detail = document.createElement("p");
const txid = document.createElement("code");
- const more = document.createElement("button");
+ const details = document.createElement("details");
+ const summary = document.createElement("summary");
label.append(typeLabels[transaction.type]);
amount.dataset.walletsTxAmount =
transaction.amount >= 0 ? "positive" : "negative";
redaction.setTitle(txid, transaction.txid);
redaction.setValue(txid, formatTxid(transaction.txid));
- more.type = "button";
- more.append("View more");
- detail.append(getTransactionDetail(transaction), " · ", txid);
+ summary.append("Details");
+ appendTransactionDetail(detail, transaction);
+ detail.append(" · ", txid);
+ details.append(summary, createTransactionDetails(transaction));
main.append(label, amount);
- row.append(main, detail, more);
- more.addEventListener("click", () => {
- const dialog = createTransactionDetails(transaction);
- const mainElement = document.querySelector("main.wallets") ?? document.body;
-
- mainElement.append(dialog);
- dialog.showModal();
- });
+ row.append(main, detail, details);
return row;
}
@@ -164,11 +139,11 @@ export function renderTransactions(element, transactions) {
}
for (const [date, group] of groups) {
- const section = createElement("section", "wallets__tx-group");
+ const section = document.createElement("section");
const heading = document.createElement("h3");
- const list = createElement("ol", "wallets__tx-list");
+ const list = document.createElement("ol");
- heading.append(date);
+ heading.append(redaction.createValue("span", date, "fixed"));
for (const transaction of group) {
list.append(createTransactionRow(transaction));
}
diff --git a/website_next/wallets/wallet/transactions/style.css b/website_next/wallets/wallet/transactions/style.css
index 09dbb065b..c7a69c87b 100644
--- a/website_next/wallets/wallet/transactions/style.css
+++ b/website_next/wallets/wallet/transactions/style.css
@@ -9,9 +9,7 @@ main.wallets {
display: grid;
gap: 1.25rem;
- h2,
- h3,
- p {
+ :is(h2, h3, p) {
margin: 0;
}
@@ -29,113 +27,132 @@ main.wallets {
line-height: var(--line-height-xs);
text-transform: uppercase;
}
- }
- .wallets__tx-group {
- display: grid;
- gap: 0.5rem;
- }
+ > section {
+ display: grid;
+ gap: 0.5rem;
- .wallets__tx-list {
- display: grid;
- gap: 0.25rem;
- margin: 0;
- padding: 0;
- list-style: none;
- }
+ > ol {
+ display: grid;
+ gap: 0.25rem;
+ margin: 0;
+ padding: 0;
+ list-style: none;
- .wallets__tx {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 0.25rem 1rem;
- align-items: center;
- padding: 0.875rem 0;
- border-bottom: 1px solid color-mix(in oklch, var(--gray) 18%, transparent);
- }
+ > li {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.25rem 1rem;
+ align-items: center;
+ padding: 0.875rem 0;
+ border-bottom: 1px solid color-mix(
+ in oklch,
+ var(--gray) 18%,
+ transparent
+ );
- .wallets__tx-main {
- display: flex;
- gap: 1rem;
- align-items: baseline;
- justify-content: space-between;
- min-width: 0;
+ @media (max-width: 34rem) {
+ grid-template-columns: 1fr;
+ }
- strong {
- color: var(--white);
- font-weight: 500;
- }
+ > div:first-child {
+ display: flex;
+ gap: 1rem;
+ align-items: baseline;
+ justify-content: space-between;
+ min-width: 0;
- span {
- color: var(--white);
- white-space: nowrap;
- }
+ strong {
+ color: var(--white);
+ font-weight: 500;
+ }
- span[data-wallets-tx-amount="positive"] {
- color: var(--green);
- }
+ > span {
+ color: var(--white);
+ white-space: nowrap;
- span[data-wallets-tx-amount="negative"] {
- color: var(--red);
- }
- }
+ &[data-wallets-tx-amount="positive"] {
+ color: var(--green);
+ }
- .wallets__tx-detail {
- grid-column: 1;
- min-width: 0;
- color: var(--gray);
- font-size: var(--font-size-sm);
- line-height: var(--line-height-sm);
+ &[data-wallets-tx-amount="negative"] {
+ color: var(--red);
+ }
+ }
+ }
- code {
- color: inherit;
- font-family: inherit;
- }
- }
+ > p {
+ grid-column: 1;
+ min-width: 0;
+ color: var(--gray);
+ font-size: var(--font-size-sm);
+ line-height: var(--line-height-sm);
- .wallets__tx button {
- grid-column: 2;
- grid-row: 1 / span 2;
- height: 2rem;
- padding-inline: 0.625rem;
- background: transparent;
- }
+ code {
+ color: inherit;
+ font-family: inherit;
+ }
+ }
- .wallets__tx-dialog {
- width: min(100% - 2rem, 42rem);
- }
+ > details {
+ grid-column: 2;
+ grid-row: 1 / span 2;
- .wallets__tx-details {
- display: grid;
- gap: 1rem;
+ @media (max-width: 34rem) {
+ grid-column: 1;
+ grid-row: auto;
+ justify-self: start;
+ }
- h2,
- p {
- margin: 0;
- }
+ &[open] {
+ display: grid;
+ grid-column: 1 / -1;
+ grid-row: auto;
+ gap: 0.75rem;
+ }
- code {
- overflow-wrap: anywhere;
- color: var(--white);
- font-family: inherit;
- }
- }
+ > summary {
+ display: grid;
+ place-items: center;
+ height: 2rem;
+ border: 1px solid color-mix(
+ in oklch,
+ var(--gray) 45%,
+ transparent
+ );
+ border-radius: 0.375rem;
+ padding-inline: 0.625rem;
+ color: var(--white);
+ background: transparent;
+ line-height: 1;
+ list-style: none;
+ cursor: pointer;
- .wallets__tx-addresses {
- display: grid;
- gap: 0.75rem;
- }
-}
+ &::marker,
+ &::-webkit-details-marker {
+ display: none;
+ content: "";
+ }
+ }
-@media (max-width: 34rem) {
- main.wallets {
- .wallets__tx {
- grid-template-columns: 1fr;
- }
+ > div {
+ display: grid;
+ gap: 1rem;
- .wallets__tx button {
- grid-column: 1;
- grid-row: auto;
- justify-self: start;
+ code {
+ overflow-wrap: anywhere;
+ color: var(--white);
+ font-family: inherit;
+ }
+
+ > div {
+ display: grid;
+ gap: 0.75rem;
+ }
+ }
+ }
+ }
+ }
}
}
}