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 PREFIX = "BNS1";
|
||||||
export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
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 BEACON_DUST_SAT = 600n;
|
||||||
export const CERTIFICATE_DUST_SAT = 1000n; // token outputs need more than plain dust
|
export const CERTIFICATE_DUST_SAT = 1000n; // token outputs need more than plain dust
|
||||||
export const P2PKH_DUST_SAT = 546n;
|
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 LABEL_RE = /^[a-z0-9-]{1,32}$/;
|
||||||
const TLD_RE = /^[a-z0-9-]{1,16}$/;
|
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)
|
// A signed P2PKH unlocking script: push(64-byte schnorr sig + sighash byte)
|
||||||
// then push(33-byte compressed pubkey) = 1+65+1+33 = 100 bytes.
|
// then push(33-byte compressed pubkey) = 1+65+1+33 = 100 bytes.
|
||||||
const P2PKH_UNLOCK_BYTES = 100;
|
const P2PKH_UNLOCK_BYTES = 100;
|
||||||
|
|
@ -363,6 +378,131 @@ export function buildRegistrationTx({
|
||||||
throw new Error("could not converge on a fee — please retry");
|
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
|
* Build a BNS1 UPD transaction: proves ownership by moving the name's
|
||||||
* certificate back to the owner, and republishes the records.
|
* certificate back to the owner, and republishes the records.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export {
|
||||||
quoteRegistration,
|
quoteRegistration,
|
||||||
registerWithBuiltInWallet,
|
registerWithBuiltInWallet,
|
||||||
registerWithExternalWallet,
|
registerWithExternalWallet,
|
||||||
|
registerTldWithBuiltInWallet,
|
||||||
setRecordsWithBuiltInWallet,
|
setRecordsWithBuiltInWallet,
|
||||||
setRecordsWithExternalWallet,
|
setRecordsWithExternalWallet,
|
||||||
findCertificateUtxo,
|
findCertificateUtxo,
|
||||||
|
|
@ -19,13 +20,16 @@ export {
|
||||||
export { REGISTRAR, priceSummary } from "../registrar-config.js";
|
export { REGISTRAR, priceSummary } from "../registrar-config.js";
|
||||||
export {
|
export {
|
||||||
buildRegistrationTx,
|
buildRegistrationTx,
|
||||||
|
buildTldRegistrationTx,
|
||||||
buildUpdateTx,
|
buildUpdateTx,
|
||||||
buildPrepTx,
|
buildPrepTx,
|
||||||
hasGenesisInput,
|
hasGenesisInput,
|
||||||
toWcTransactionObject,
|
toWcTransactionObject,
|
||||||
normalizeName,
|
normalizeName,
|
||||||
|
normalizeTld,
|
||||||
displayName,
|
displayName,
|
||||||
payloadBudget,
|
payloadBudget,
|
||||||
|
TLD_BEACON,
|
||||||
NoGenesisInputError,
|
NoGenesisInputError,
|
||||||
InsufficientFundsError,
|
InsufficientFundsError,
|
||||||
} from "../lib/register-tx.js";
|
} from "../lib/register-tx.js";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue