feat(registrar): registerTldWithExternalWallet + wire into portal

Adds the external-wallet variant of registerTldWithBuiltInWallet so
WizardConnect users can mint TLDs from the sirius.x portal without
first importing a seed. Same transaction shape (TREG payload, TLD-
beacon output, service-fee output) — the wallet approves the mint
(and, if needed, a one-time prep signature to satisfy the vout-0
non-token genesis-input rule) on the user's own device.

Bundle: re-exported from src/web/register-entry.js and rebuilt
(bns-register.js: 33.3 kB, cache-buster bumped to ?v=20260907tldext
on portal.html / register.html / admin/index.html).

Portal: TLD-register submit now dispatches on wallet.source, calling
registerTldWithExternalWallet with wallet.session when the user is
signed in via WC. Removed the WC-disable guard and updated the card
copy to say TLD mints also go through the wallet's signing prompt.

Uses the same landOnChain helper as registerWithExternalWallet, so
wallet-broadcasts (walletBroadcasts:true sessions) and duplicate/
mempool responses are handled identically.
This commit is contained in:
Local Dev 2026-09-07 01:39:16 +02:00
parent d07fece6c9
commit db71b9e832
2 changed files with 105 additions and 0 deletions

View file

@ -293,6 +293,110 @@ export async function registerTldWithBuiltInWallet(client, {
};
}
/**
* Register a TLD with an EXTERNAL wallet (WizardConnect).
*
* Same transaction shape as registerTldWithBuiltInWallet — same TREG payload,
* same TLD-beacon output, same service-fee output — only the signing transport
* differs. Genesis-input requirement is handled by prompting the connected
* wallet for a one-time prep signature if it can't front a vout-0 non-token
* coin already, mirroring registerWithExternalWallet's pattern.
*
* Does NOT accept `ownerAddress` because a TLD certificate is minted to
* whoever signs the mint — using the first address the connected wallet
* exposes keeps the same "you own what you signed for" invariant as the
* name-registration external path.
*
* @param {object} o.session a WalletSession (see connect/)
*/
export async function registerTldWithExternalWallet(client, {
session,
tld,
records = {},
serviceFee = { address: null, sats: 0n },
feeRate = REGISTRAR.feeRate,
tldListUrl = null,
onProgress = () => {},
}) {
const rawTld = normalizeTld(tld);
onProgress("checking-availability");
if (tldListUrl) {
try {
const r = await fetch(tldListUrl);
if (r.ok) {
const j = await r.json();
const taken = Array.isArray(j.tlds) && j.tlds.some((t) => (t.tld || t) === rawTld);
if (taken) throw new Error(`".${rawTld}" is already registered as a TLD`);
}
} catch (e) {
if (/already registered/.test(e.message)) throw e;
// network / API blip — proceed; on-chain "first TREG wins" handles it.
}
}
onProgress("reading-wallet");
const addresses = await session.getAddresses();
if (!addresses.length) throw new Error("the wallet exposed no addresses");
const owner = addresses[0];
onProgress("loading-coins");
let utxos = await getUtxosForAddresses(client, addresses);
if (utxos.filter((u) => !u.token).reduce((s, u) => s + u.satoshis, 0n) === 0n) {
throw new Error("the connected wallet has no spendable coins");
}
let prepTxid = null;
if (!hasGenesisInput(utxos)) {
onProgress("preparing-wallet");
const prep = buildPrepTx({ ownerAddress: owner, utxos, feeRate, addressToLockingBytecode });
const prepSigned = await session.signTransaction(prep, {
userPrompt: "One-time setup so your wallet can create a TLD certificate",
});
prepTxid = await landOnChain(client, session, prepSigned, onProgress);
onProgress("prepared", { txid: prepTxid });
utxos = [
{
txid: prepTxid, vout: 0,
satoshis: prep.transaction.outputs[0].valueSatoshis,
address: owner,
lockingBytecode: addressToLockingBytecode(owner),
},
...utxos.filter((u) => !u.token && !prep.inputUtxos.some((i) => i.txid === u.txid && i.vout === u.vout)),
];
}
onProgress("building");
const built = buildTldRegistrationTx({
tld: rawTld,
ownerAddress: owner,
ownerTokenAddress: toTokenAddress(owner),
changeAddress: owner,
records,
utxos,
feeAddress: serviceFee.address || null,
feeSats: BigInt(serviceFee.sats ?? 0n),
feeRate,
addressToLockingBytecode,
});
onProgress("awaiting-approval");
const signed = await session.signTransaction(built, {
userPrompt: `Register .${rawTld} — the TLD certificate is minted to your own address`,
});
const txid = await landOnChain(client, session, signed, onProgress);
onProgress("registered", { txid, category: built.category });
return {
tld: built.tld,
category: built.category,
txid,
prepTxid,
ownerAddress: owner,
certificateAddress: toTokenAddress(owner),
costs: built.costs,
};
}
/**
* Mint a name certificate on a buyer's behalf, from the registrar's own coins.
*

View file

@ -12,6 +12,7 @@ export {
registerWithBuiltInWallet,
registerWithExternalWallet,
registerTldWithBuiltInWallet,
registerTldWithExternalWallet,
setRecordsWithBuiltInWallet,
setRecordsWithExternalWallet,
findCertificateUtxo,