feat(argus/registrar): TLD-registration TREG builder (per-TLD certificates)
New public API in register-tx.js:
- TLD_BEACON constant (per Decentralized.DNS/DESIGN-tld-registry.md §3);
keep in step with Argus/src/config.js:TLD_BEACON and the indexer's
BNS_TLD_BEACON_SCRIPTHASH — all three must agree, or clients disagree
about which TLDs exist.
- normalizeTld(input) — validates a bare TLD label (1–16 chars,
[a-z0-9-]), matches bns.js's normalizeTld.
- buildTldRegistrationTx({...}) — mints a per-TLD certificate onto the
TLD beacon. Same shape as buildRegistrationTx (genesis-input rule,
dust accounting, change-at-vout-0) but the payload op is "TREG", the
commitment is the TLD label, and dust flows to TLD_BEACON instead of
the name beacon.
register-entry.js re-exports registerTldWithBuiltInWallet so the
in-browser registrar can drive TLD mints alongside name mints.
This commit is contained in:
parent
408f965629
commit
d07fece6c9
2 changed files with 144 additions and 0 deletions
|
|
@ -42,6 +42,11 @@ import {
|
|||
|
||||
export const PREFIX = "BNS1";
|
||||
export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
||||
// TLD-registry beacon (Decentralized.DNS/DESIGN-tld-registry.md §3). Keep in
|
||||
// step with Argus/src/config.js:TLD_BEACON and the indexer's default
|
||||
// BNS_TLD_BEACON_SCRIPTHASH — the three must agree, or clients disagree
|
||||
// about which TLD certificates exist.
|
||||
export const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
|
||||
export const BEACON_DUST_SAT = 600n;
|
||||
export const CERTIFICATE_DUST_SAT = 1000n; // token outputs need more than plain dust
|
||||
export const P2PKH_DUST_SAT = 546n;
|
||||
|
|
@ -51,6 +56,16 @@ export const MAX_PAYLOAD_BYTES = 200;
|
|||
const LABEL_RE = /^[a-z0-9-]{1,32}$/;
|
||||
const TLD_RE = /^[a-z0-9-]{1,16}$/;
|
||||
|
||||
// Validate a TLD label — same rule as bns.js's normalizeTld. Accepts the
|
||||
// bare label (no dots), 1–16 chars, [a-z0-9-]. Lowercases first.
|
||||
export function normalizeTld(input) {
|
||||
const tld = String(input).toLowerCase().trim().replace(/^\.+/, "").replace(/\.+$/, "");
|
||||
if (!TLD_RE.test(tld)) {
|
||||
throw new Error(`invalid TLD "${input}" (allowed: [a-z0-9-]{1,16})`);
|
||||
}
|
||||
return tld;
|
||||
}
|
||||
|
||||
// A signed P2PKH unlocking script: push(64-byte schnorr sig + sighash byte)
|
||||
// then push(33-byte compressed pubkey) = 1+65+1+33 = 100 bytes.
|
||||
const P2PKH_UNLOCK_BYTES = 100;
|
||||
|
|
@ -363,6 +378,131 @@ export function buildRegistrationTx({
|
|||
throw new Error("could not converge on a fee — please retry");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a TLD-registry TREG transaction — mint a per-TLD certificate onto the
|
||||
* TLD beacon. Same shape as buildRegistrationTx (genesis-input rule, dust
|
||||
* accounting, change-at-vout-0) but the payload's op is "TREG", the commitment
|
||||
* is the TLD label, and dust flows to TLD_BEACON instead of the name beacon.
|
||||
* See Decentralized.DNS/DESIGN-tld-registry.md §3.
|
||||
*/
|
||||
export function buildTldRegistrationTx({
|
||||
tld,
|
||||
ownerAddress,
|
||||
ownerTokenAddress,
|
||||
changeAddress,
|
||||
records = {},
|
||||
utxos,
|
||||
feeAddress = null,
|
||||
feeSats = 0n,
|
||||
beaconAddress = TLD_BEACON,
|
||||
feeRate = 1,
|
||||
addressToLockingBytecode,
|
||||
}) {
|
||||
if (typeof addressToLockingBytecode !== "function") {
|
||||
throw new Error("addressToLockingBytecode must be injected (see chain.js)");
|
||||
}
|
||||
const rawTld = normalizeTld(tld);
|
||||
const payload = encodePayload("TREG", rawTld, records);
|
||||
const serviceFee = BigInt(feeSats ?? 0n);
|
||||
if (serviceFee > 0n && !feeAddress) throw new Error("feeSats set but feeAddress missing");
|
||||
|
||||
const changeLock = addressToLockingBytecode(changeAddress ?? ownerAddress);
|
||||
const certLock = addressToLockingBytecode(ownerTokenAddress ?? ownerAddress);
|
||||
const beaconLock = addressToLockingBytecode(beaconAddress);
|
||||
const feeLock = serviceFee > 0n ? addressToLockingBytecode(feeAddress) : null;
|
||||
|
||||
const fixedOut = CERTIFICATE_DUST_SAT + BEACON_DUST_SAT + serviceFee;
|
||||
|
||||
let selection = selectInputs(utxos, fixedOut + 300n);
|
||||
let chainFee = 0n;
|
||||
let change = 0n;
|
||||
let withChange = true;
|
||||
|
||||
for (let round = 0; round < 6; round++) {
|
||||
const { inputs, totalSats } = selection;
|
||||
const genesisUtxo = inputs[0];
|
||||
const category = hexToBin(genesisUtxo.txid);
|
||||
|
||||
const build = (includeChange, changeSats) => {
|
||||
const outputs = [];
|
||||
if (includeChange) outputs.push({ lockingBytecode: changeLock, valueSatoshis: changeSats });
|
||||
outputs.push({
|
||||
lockingBytecode: certLock,
|
||||
valueSatoshis: CERTIFICATE_DUST_SAT,
|
||||
token: {
|
||||
amount: 0n,
|
||||
category,
|
||||
nft: { capability: "none", commitment: utf8ToBin(rawTld) },
|
||||
},
|
||||
});
|
||||
outputs.push({ lockingBytecode: opReturnScript(payload), valueSatoshis: 0n });
|
||||
outputs.push({ lockingBytecode: beaconLock, valueSatoshis: BEACON_DUST_SAT });
|
||||
if (feeLock) outputs.push({ lockingBytecode: feeLock, valueSatoshis: serviceFee });
|
||||
return { version: 2, locktime: 0, inputs: inputs.map(toInput), outputs };
|
||||
};
|
||||
|
||||
const probe = build(true, 1_000_000n);
|
||||
const size = measure(probe);
|
||||
chainFee = BigInt(Math.ceil(size * feeRate));
|
||||
const need = fixedOut + chainFee;
|
||||
|
||||
if (totalSats < need) {
|
||||
const grown = selectInputs(utxos, need + 200n);
|
||||
if (grown.inputs.length > inputs.length) { selection = grown; continue; }
|
||||
throw new InsufficientFundsError(
|
||||
`not enough funds: need ${need} sat, have ${totalSats} sat`,
|
||||
{ needSats: need, haveSats: totalSats },
|
||||
);
|
||||
}
|
||||
|
||||
change = totalSats - need;
|
||||
withChange = change >= P2PKH_DUST_SAT;
|
||||
if (!withChange) {
|
||||
const noChange = build(false, 0n);
|
||||
chainFee = totalSats - fixedOut;
|
||||
const minFee = BigInt(Math.ceil(measure(noChange) * feeRate));
|
||||
if (chainFee < minFee) {
|
||||
const grown = selectInputs(utxos, fixedOut + minFee + P2PKH_DUST_SAT + 200n);
|
||||
if (grown.inputs.length > inputs.length) { selection = grown; continue; }
|
||||
throw new InsufficientFundsError(
|
||||
`not enough funds: need ${fixedOut + minFee} sat, have ${totalSats} sat`,
|
||||
{ needSats: fixedOut + minFee, haveSats: totalSats },
|
||||
);
|
||||
}
|
||||
change = 0n;
|
||||
}
|
||||
|
||||
const transaction = build(withChange, change);
|
||||
const outputMap = {};
|
||||
let i = 0;
|
||||
if (withChange) outputMap.change = i++;
|
||||
outputMap.certificate = i++;
|
||||
outputMap.opReturn = i++;
|
||||
outputMap.beacon = i++;
|
||||
if (feeLock) outputMap.serviceFee = i++;
|
||||
|
||||
return {
|
||||
tld: rawTld,
|
||||
category: binToHex(category),
|
||||
transaction,
|
||||
sourceOutputs: inputs.map(toSourceOutput),
|
||||
inputUtxos: inputs,
|
||||
outputMap,
|
||||
payload,
|
||||
costs: {
|
||||
certificateDustSats: CERTIFICATE_DUST_SAT,
|
||||
beaconDustSats: BEACON_DUST_SAT,
|
||||
serviceFeeSats: serviceFee,
|
||||
chainFeeSats: chainFee,
|
||||
changeSats: change,
|
||||
totalInputSats: totalSats,
|
||||
netCostSats: serviceFee + chainFee + BEACON_DUST_SAT,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error("could not converge on a fee — please retry");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a BNS1 UPD transaction: proves ownership by moving the name's
|
||||
* certificate back to the owner, and republishes the records.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export {
|
|||
quoteRegistration,
|
||||
registerWithBuiltInWallet,
|
||||
registerWithExternalWallet,
|
||||
registerTldWithBuiltInWallet,
|
||||
setRecordsWithBuiltInWallet,
|
||||
setRecordsWithExternalWallet,
|
||||
findCertificateUtxo,
|
||||
|
|
@ -19,13 +20,16 @@ export {
|
|||
export { REGISTRAR, priceSummary } from "../registrar-config.js";
|
||||
export {
|
||||
buildRegistrationTx,
|
||||
buildTldRegistrationTx,
|
||||
buildUpdateTx,
|
||||
buildPrepTx,
|
||||
hasGenesisInput,
|
||||
toWcTransactionObject,
|
||||
normalizeName,
|
||||
normalizeTld,
|
||||
displayName,
|
||||
payloadBudget,
|
||||
TLD_BEACON,
|
||||
NoGenesisInputError,
|
||||
InsufficientFundsError,
|
||||
} from "../lib/register-tx.js";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue