global: private xpub support part 2

This commit is contained in:
nym21
2026-06-17 11:25:42 +02:00
parent 0c7861071d
commit 43df9e098c
68 changed files with 1836 additions and 1653 deletions
+81
View File
@@ -0,0 +1,81 @@
import { createElement } from "../dom.js";
import { createField } from "../form/index.js";
import { redaction } from "../redaction/index.js";
/**
* @typedef {Object} AddWalletFormSubmit
* @property {HTMLInputElement} name
* @property {HTMLInputElement} source
* @property {HTMLButtonElement} submit
* @property {HTMLElement} status
* @property {HTMLFormElement} form
*/
/**
* @typedef {Object} AddWalletFormOptions
* @property {() => void} onCancel
* @property {(submit: AddWalletFormSubmit) => void | Promise<void>} onSubmit
*/
function createSourceInput() {
const input = document.createElement("input");
input.name = "source";
input.type = redaction.isHidden() ? "password" : "text";
input.setAttribute("data-wallets-private-input", "");
input.autocomplete = "off";
input.placeholder = "xpub or descriptor...";
input.required = true;
input.spellcheck = false;
return input;
}
/**
* @param {AddWalletFormOptions} options
*/
export function createAddForm(options) {
const form = createElement("form", "wallets__dialog-form");
const title = document.createElement("h2");
const name = document.createElement("input");
const source = createSourceInput();
const actions = createElement("div", "wallets__dialog-actions");
const cancel = document.createElement("button");
const submit = document.createElement("button");
const status = createElement("p", "wallets__status");
const fields = [
createField("name", name),
createField("xpub or descriptor", source),
];
title.append("Watch wallet");
name.name = "name";
name.autocomplete = "off";
name.placeholder = "Wallet name";
name.required = true;
cancel.type = "button";
cancel.append("Cancel");
submit.type = "submit";
submit.append("Add");
status.setAttribute("role", "status");
actions.append(cancel, submit);
form.append(
title,
...fields,
actions,
status,
);
cancel.addEventListener("click", options.onCancel);
form.addEventListener("submit", (event) => {
event.preventDefault();
void options.onSubmit({
name,
source,
submit,
status,
form,
});
});
return form;
}
+49
View File
@@ -0,0 +1,49 @@
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<AddressScript>}
*/
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;
}
+23
View File
@@ -0,0 +1,23 @@
import { isOutputDescriptor } from "../derive/index.js";
const EXTENDED_PUBLIC_KEY_PATTERN =
/\b(?:xpub|ypub|zpub|tpub|upub|vpub)[1-9A-HJ-NP-Za-km-z]{20,}\b/;
/**
* @param {string} text
*/
export function readWalletSourceText(text) {
const value = text.trim();
if (isOutputDescriptor(value)) {
return value;
}
const match = value.match(EXTENDED_PUBLIC_KEY_PATTERN);
if (match) {
return match[0];
}
throw new Error("Expected an xpub or descriptor");
}
+12
View File
@@ -0,0 +1,12 @@
main.wallets {
.wallets__dialog-form {
display: grid;
gap: 0.75rem;
}
.wallets__dialog-actions {
display: flex;
gap: 0.5rem;
justify-content: end;
}
}