BNS: a transaction with more than one protocol message carries none
Resolvers took the last message in a transaction. Wherever one party fixes a message and another assembles the transaction - the delivery escrow pins the registration at output 1 - a later output could replace it and every resolver would read that one. Counting instead of choosing removes the question: one message, or the transaction says nothing. Same rule in all three resolvers. The Android parser now recognises the same set of operations as the JavaScript one, since what counts as a message decides which transactions are void. No transaction on chipnet carried more than one. Three older suites built registrations that never created their certificate's category; their fixtures now do, as the sole-certificate rule requires.
This commit is contained in:
parent
e0eca1190e
commit
b4fc73f0fa
8 changed files with 107 additions and 35 deletions
|
|
@ -62,6 +62,19 @@ export function parseOpReturn(scriptHex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One message per transaction: more than one parseable BNS payload means none.
|
||||||
|
// KEEP IN LOCKSTEP with resolver-web.js onlyPayload (the reasoning is there).
|
||||||
|
export function onlyPayload(vouts) {
|
||||||
|
let found = null;
|
||||||
|
for (const v of vouts || []) {
|
||||||
|
const p = parseOpReturn(v?.scriptPubKey?.hex);
|
||||||
|
if (!p) continue;
|
||||||
|
if (found) return null;
|
||||||
|
found = p;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
// The set of payload opcodes this codec recognises. New opcodes go here AND in
|
// The set of payload opcodes this codec recognises. New opcodes go here AND in
|
||||||
// resolver-web.js — divergence is a consensus split.
|
// resolver-web.js — divergence is a consensus split.
|
||||||
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD", "RPT", "BLK", "UBK"]);
|
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD", "RPT", "BLK", "UBK"]);
|
||||||
|
|
@ -126,8 +139,7 @@ async function buildTldMap(provider) {
|
||||||
const tlds = new Map();
|
const tlds = new Map();
|
||||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
||||||
const tx = txs.get(tx_hash);
|
const tx = txs.get(tx_hash);
|
||||||
let payload = null;
|
const payload = onlyPayload(tx.vout);
|
||||||
for (const vout of tx.vout) payload = parseOpReturn(vout.scriptPubKey?.hex) ?? payload;
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
if (typeof payload.n !== "string" || !/^[a-z0-9-]{1,16}$/.test(payload.n)) continue;
|
if (typeof payload.n !== "string" || !/^[a-z0-9-]{1,16}$/.test(payload.n)) continue;
|
||||||
const tld = payload.n;
|
const tld = payload.n;
|
||||||
|
|
@ -248,10 +260,7 @@ export async function buildIndex(provider, { enforceTld = false, cosignGate = tr
|
||||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
||||||
const tx = txs.get(tx_hash);
|
const tx = txs.get(tx_hash);
|
||||||
|
|
||||||
let payload = null;
|
const payload = onlyPayload(tx.vout);
|
||||||
for (const vout of tx.vout) {
|
|
||||||
payload = parseOpReturn(vout.scriptPubKey?.hex) ?? payload;
|
|
||||||
}
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
|
|
||||||
const name = payload.n;
|
const name = payload.n;
|
||||||
|
|
|
||||||
2
src/lib/resolver-web.d.ts
vendored
2
src/lib/resolver-web.d.ts
vendored
|
|
@ -58,6 +58,8 @@ export interface BnsNetwork {
|
||||||
bchnRpcPort: number;
|
bchnRpcPort: number;
|
||||||
blocklists: { name: string; beacon: string; scripthash?: string }[];
|
blocklists: { name: string; beacon: string; scripthash?: string }[];
|
||||||
}
|
}
|
||||||
|
/** The transaction's single BNS payload; null when it has none or more than one. */
|
||||||
|
export function onlyPayload(vouts: any[]): any | null;
|
||||||
/** REG/TREG: is `mint` the only token of its category there is or can ever be? */
|
/** REG/TREG: is `mint` the only token of its category there is or can ever be? */
|
||||||
export function soleCertificate(tx: any, mint: any): boolean;
|
export function soleCertificate(tx: any, mint: any): boolean;
|
||||||
/** REL: does `tx` burn `entry`'s certificate, spending it from a beacon transaction in `txs`? */
|
/** REL: does `tx` burn `entry`'s certificate, spending it from a beacon transaction in `txs`? */
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,28 @@ function parseOpReturnHex(hex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ONE MESSAGE PER TRANSACTION. Every reducer asks this for a transaction's
|
||||||
|
// payload. If more than one output parses as a BNS message, the transaction
|
||||||
|
// carries none.
|
||||||
|
//
|
||||||
|
// The earlier rule was "the last one wins", which let a later output silently
|
||||||
|
// replace an earlier one. That matters wherever one party fixes a message and
|
||||||
|
// another builds the rest of the transaction — the delivery escrow pins the
|
||||||
|
// registration at output 1; a deliverer could add a second message after it
|
||||||
|
// and every resolver would read that one instead. Making such a transaction
|
||||||
|
// void removes the question of which message counts. KEEP IN LOCKSTEP with
|
||||||
|
// bns.js onlyPayload and the mobile Bns.java.
|
||||||
|
export function onlyPayload(vouts) {
|
||||||
|
let found = null;
|
||||||
|
for (const v of vouts || []) {
|
||||||
|
const p = parseOpReturnHex(v && v.scriptPubKey ? v.scriptPubKey.hex : null);
|
||||||
|
if (!p) continue;
|
||||||
|
if (found) return null;
|
||||||
|
found = p;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
// The set of payload opcodes this codec recognises. New opcodes go here AND in
|
// The set of payload opcodes this codec recognises. New opcodes go here AND in
|
||||||
// bns.js — divergence is a consensus split.
|
// bns.js — divergence is a consensus split.
|
||||||
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD", "RPT", "BLK", "UBK"]);
|
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD", "RPT", "BLK", "UBK"]);
|
||||||
|
|
@ -446,8 +468,7 @@ export async function fetchTldMap(el, network = NETWORK) {
|
||||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
||||||
const tx = txs.get(tx_hash);
|
const tx = txs.get(tx_hash);
|
||||||
const vouts = tx.vout || [];
|
const vouts = tx.vout || [];
|
||||||
let payload = null;
|
const payload = onlyPayload(vouts);
|
||||||
for (const v of vouts) payload = parseOpReturnHex(v.scriptPubKey?.hex) ?? payload;
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
if (typeof payload.n !== "string" || !TLD_RE.test(payload.n)) continue;
|
if (typeof payload.n !== "string" || !TLD_RE.test(payload.n)) continue;
|
||||||
const tld = payload.n;
|
const tld = payload.n;
|
||||||
|
|
@ -614,8 +635,7 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw
|
||||||
const tx = txs.get(tx_hash);
|
const tx = txs.get(tx_hash);
|
||||||
const vouts = tx.vout || [];
|
const vouts = tx.vout || [];
|
||||||
|
|
||||||
let payload = null;
|
const payload = onlyPayload(vouts);
|
||||||
for (const v of vouts) payload = parseOpReturnHex(v.scriptPubKey?.hex) ?? payload;
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
const rawName = payload.n;
|
const rawName = payload.n;
|
||||||
if (!RAWNAME_RE.test(rawName)) continue;
|
if (!RAWNAME_RE.test(rawName)) continue;
|
||||||
|
|
@ -708,8 +728,7 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap =
|
||||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txMap.has(h.tx_hash)), txMap)) {
|
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txMap.has(h.tx_hash)), txMap)) {
|
||||||
const tx = txMap.get(tx_hash);
|
const tx = txMap.get(tx_hash);
|
||||||
const vouts = tx.vout || [];
|
const vouts = tx.vout || [];
|
||||||
let payload = null;
|
const payload = onlyPayload(vouts);
|
||||||
for (const v of vouts) payload = parseOpReturnHex(v.scriptPubKey?.hex) ?? payload;
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
const rawName = payload.n;
|
const rawName = payload.n;
|
||||||
if (!RAWNAME_RE.test(rawName)) continue;
|
if (!RAWNAME_RE.test(rawName)) continue;
|
||||||
|
|
@ -881,11 +900,7 @@ export function reduceBlocklist({ history, txs, address, source = null }) {
|
||||||
const txMap = txs instanceof Map ? txs : new Map(Object.entries(txs || {}));
|
const txMap = txs instanceof Map ? txs : new Map(Object.entries(txs || {}));
|
||||||
const ordered = orderByDependency([...(history || [])].filter((h) => txMap.has(h.tx_hash)).sort(byHeightThenTxid), txMap);
|
const ordered = orderByDependency([...(history || [])].filter((h) => txMap.has(h.tx_hash)).sort(byHeightThenTxid), txMap);
|
||||||
|
|
||||||
const payloadOf = (tx) => {
|
const payloadOf = (tx) => onlyPayload(tx.vout);
|
||||||
let payload = null;
|
|
||||||
for (const v of tx.vout || []) payload = parseOpReturnHex(v.scriptPubKey?.hex) ?? payload;
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pass 1: the reports. Looked up by txid, so their position does not matter.
|
// Pass 1: the reports. Looked up by txid, so their position does not matter.
|
||||||
const reports = new Map();
|
const reports = new Map();
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { parseOpReturn } from "./bns.js";
|
import { parseOpReturn } from "./bns.js";
|
||||||
import { soleCertificate } from "./resolver-web.js";
|
import { soleCertificate, onlyPayload } from "./resolver-web.js";
|
||||||
|
|
||||||
const TLD_RE = /^[a-z0-9-]{1,16}$/;
|
const TLD_RE = /^[a-z0-9-]{1,16}$/;
|
||||||
|
|
||||||
|
|
@ -101,8 +101,7 @@ export function buildTldSnapshot({
|
||||||
const tx = txMap.get(tx_hash);
|
const tx = txMap.get(tx_hash);
|
||||||
if (!tx) continue;
|
if (!tx) continue;
|
||||||
const vouts = tx.vout || [];
|
const vouts = tx.vout || [];
|
||||||
let payload = null;
|
const payload = onlyPayload(vouts); // one message per transaction (resolver-web.js)
|
||||||
for (const v of vouts) payload = parseOpReturn(v.scriptPubKey?.hex) ?? payload;
|
|
||||||
if (!payload) continue;
|
if (!payload) continue;
|
||||||
if (typeof payload.n !== "string" || !TLD_RE.test(payload.n)) continue;
|
if (typeof payload.n !== "string" || !TLD_RE.test(payload.n)) continue;
|
||||||
const tld = payload.n;
|
const tld = payload.n;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import assert from "node:assert/strict";
|
||||||
import { NameIndex, buildFromCache } from "../src/lib/name-index.js";
|
import { NameIndex, buildFromCache } from "../src/lib/name-index.js";
|
||||||
import { verifyPortfolio, renderPortfolio, heldNames, PORTFOLIO_STATUS } from "../src/lib/portfolio.js";
|
import { verifyPortfolio, renderPortfolio, heldNames, PORTFOLIO_STATUS } from "../src/lib/portfolio.js";
|
||||||
import { addressToLockingBytecode } from "../src/lib/chain.js";
|
import { addressToLockingBytecode } from "../src/lib/chain.js";
|
||||||
|
import { binToHex, sha256 } from "@bitauth/libauth";
|
||||||
|
|
||||||
const OPERATOR = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
|
const OPERATOR = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
|
||||||
const OTHER = "bchtest:qqmcn373plhjrvs5mrswuvevdvepxum8x58jhawqxs";
|
const OTHER = "bchtest:qqmcn373plhjrvs5mrswuvevdvepxum8x58jhawqxs";
|
||||||
|
|
@ -21,7 +22,7 @@ function regTx(txid, name, category) {
|
||||||
const len = (hex.length / 2).toString(16).padStart(2, "0");
|
const len = (hex.length / 2).toString(16).padStart(2, "0");
|
||||||
return {
|
return {
|
||||||
txid,
|
txid,
|
||||||
vin: [],
|
vin: [{ txid: category, vout: 0 }], // the certificate's category is created here (sole-certificate rule)
|
||||||
vout: [
|
vout: [
|
||||||
{ n: 0, scriptPubKey: { hex: `6a4c${len}${hex}` } },
|
{ n: 0, scriptPubKey: { hex: `6a4c${len}${hex}` } },
|
||||||
{ n: 1, scriptPubKey: { hex: "76a914" + "11".repeat(20) + "88ac" },
|
{ n: 1, scriptPubKey: { hex: "76a914" + "11".repeat(20) + "88ac" },
|
||||||
|
|
@ -59,12 +60,8 @@ function fakeClient({ history = [], txs = {}, utxos = {} } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// scripthash helper mirroring chain.js
|
// scripthash helper mirroring chain.js
|
||||||
let _sha;
|
const _sha = { binToHex, sha256 };
|
||||||
function shOf(address) {
|
function shOf(address) {
|
||||||
if (!_sha) {
|
|
||||||
// lazily to keep the import list short
|
|
||||||
_sha = require("@bitauth/libauth");
|
|
||||||
}
|
|
||||||
const bytes = addressToLockingBytecode(address);
|
const bytes = addressToLockingBytecode(address);
|
||||||
return _sha.binToHex(_sha.sha256.hash(bytes).slice().reverse());
|
return _sha.binToHex(_sha.sha256.hash(bytes).slice().reverse());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,53 @@ test("TLD certificates obey the same rule, in every reducer that reads them", as
|
||||||
const snap = buildTldSnapshot({ history, txs, beacon: "x" });
|
const snap = buildTldSnapshot({ history, txs, beacon: "x" });
|
||||||
assert.deepEqual((snap.tlds ?? []).map((t) => t.tld), ["zz"]);
|
assert.deepEqual((snap.tlds ?? []).map((t) => t.tld), ["zz"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- one message per transaction ----------
|
||||||
|
|
||||||
|
import { onlyPayload } from "../src/lib/resolver-web.js";
|
||||||
|
|
||||||
|
test("a transaction with two protocol messages carries none, in every reducer", async () => {
|
||||||
|
const honest = reg(1, "single.x");
|
||||||
|
assert.deepEqual(onlyPayload(honest.vout), { o: "REG", n: "single.x", r: { u: "https://honest.example" } });
|
||||||
|
assert.deepEqual(legacy.onlyPayload(honest.vout), onlyPayload(honest.vout));
|
||||||
|
|
||||||
|
// The escrow attack at the resolver: the agreed registration at output 1,
|
||||||
|
// and a second message after it that used to win.
|
||||||
|
const second = [
|
||||||
|
{ o: "REG", n: "single.x", r: { u: "https://deliverer.example" } },
|
||||||
|
{ o: "UPD", n: "single.x", r: {} },
|
||||||
|
{ o: "REG", n: "elsewhere.x", r: {} },
|
||||||
|
];
|
||||||
|
for (const payload of second) {
|
||||||
|
const tx = { ...honest, vout: [...honest.vout, data(2, payload)] };
|
||||||
|
assert.equal(onlyPayload(tx.vout), null);
|
||||||
|
assert.equal(legacy.onlyPayload(tx.vout), null);
|
||||||
|
const idx = await names([[tx, 100]]);
|
||||||
|
assert.equal(idx.size, 0, `nothing registers from a transaction that says two things (${payload.o} ${payload.n})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other data outputs are nobody's business: only BNS messages are counted.
|
||||||
|
const withMemo = { ...honest, vout: [...honest.vout, { n: 2, value: 0, scriptPubKey: { hex: "6a0568656c6c6f" } }] };
|
||||||
|
assert.ok((await names([[withMemo, 100]])).has("single.x"));
|
||||||
|
|
||||||
|
// TLD certificates and the blocklist read through the same function.
|
||||||
|
const t = reg(2, "zz", { op: "TREG" });
|
||||||
|
const doubled = { ...t, vout: [...t.vout, data(2, { o: "TREG", n: "yy", r: {} })] };
|
||||||
|
const history = [{ tx_hash: doubled.txid, height: 5 }];
|
||||||
|
const txs = new Map([[doubled.txid, doubled]]);
|
||||||
|
const el = { call: async (m, [a]) => (m === "blockchain.scripthash.get_history" ? history.map((h) => ({ ...h })) : txs.get(a)) };
|
||||||
|
assert.equal((await fetchTldMap(el)).size, 0);
|
||||||
|
assert.deepEqual((buildTldSnapshot({ history, txs, beacon: "x" }).tlds ?? []).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("what counts as a message is the same everywhere", () => {
|
||||||
|
const honest = reg(1, "single.x");
|
||||||
|
const plus = (...payloads) => [...honest.vout, ...payloads.map((p, i) => data(2 + i, p))];
|
||||||
|
for (const read of [onlyPayload, legacy.onlyPayload]) {
|
||||||
|
// list entries are messages too, though no name reducer acts on them
|
||||||
|
assert.equal(read(plus({ o: "BLK", n: "single.x", c: "scam" })), null);
|
||||||
|
assert.equal(read(plus({ o: "RPT", n: ["a.x", "b.x"] })), null);
|
||||||
|
// malformed or unknown ones are not messages, and so do not void the transaction
|
||||||
|
assert.equal(read(plus({ o: "UPD", n: 5 }, { o: "XYZ", n: "single.x" }, { o: "BLK", n: ["a.x"] }))?.o, "REG");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,11 @@ function mockProvider({ nameHistory, tldHistory, txs }) {
|
||||||
const CAT_MEMPOOL = "ff".repeat(32);
|
const CAT_MEMPOOL = "ff".repeat(32);
|
||||||
|
|
||||||
const txs = new Map();
|
const txs = new Map();
|
||||||
txs.set("tX", { txid: "tX", vin: [], vout: [beaconVout(), opreturnVout("TREG", "x", { policy: "open" }), mintVout(CAT_X, "x")] });
|
txs.set("tX", { txid: "tX", vin: [{ txid: CAT_X, vout: 0 }], vout: [beaconVout(), opreturnVout("TREG", "x", { policy: "open" }), mintVout(CAT_X, "x")] });
|
||||||
txs.set("tEarly", { txid: "tEarly", vin: [], vout: [beaconVout(), opreturnVout("REG", "early.x", {}), mintVout(CAT_EARLY, "early.x")] });
|
txs.set("tEarly", { txid: "tEarly", vin: [{ txid: CAT_EARLY, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "early.x", {}), mintVout(CAT_EARLY, "early.x")] });
|
||||||
txs.set("tLate", { txid: "tLate", vin: [], vout: [beaconVout(), opreturnVout("REG", "late.x", {}), mintVout(CAT_LATE, "late.x")] });
|
txs.set("tLate", { txid: "tLate", vin: [{ txid: CAT_LATE, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "late.x", {}), mintVout(CAT_LATE, "late.x")] });
|
||||||
txs.set("tOrphan", { txid: "tOrphan", vin: [], vout: [beaconVout(), opreturnVout("REG", "orphan.zz",{}), mintVout(CAT_ORPHAN, "orphan.zz")] });
|
txs.set("tOrphan", { txid: "tOrphan", vin: [{ txid: CAT_ORPHAN, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "orphan.zz",{}), mintVout(CAT_ORPHAN, "orphan.zz")] });
|
||||||
txs.set("tMempool", { txid: "tMempool", vin: [], vout: [beaconVout(), opreturnVout("REG", "mempool.x",{}), mintVout(CAT_MEMPOOL, "mempool.x")] });
|
txs.set("tMempool", { txid: "tMempool", vin: [{ txid: CAT_MEMPOOL, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "mempool.x",{}), mintVout(CAT_MEMPOOL, "mempool.x")] });
|
||||||
|
|
||||||
const provider = mockProvider({
|
const provider = mockProvider({
|
||||||
nameHistory: [
|
nameHistory: [
|
||||||
|
|
@ -102,8 +102,8 @@ function mockProvider({ nameHistory, tldHistory, txs }) {
|
||||||
const CAT_Y = "11".repeat(32);
|
const CAT_Y = "11".repeat(32);
|
||||||
const CAT_N = "22".repeat(32);
|
const CAT_N = "22".repeat(32);
|
||||||
const txs = new Map();
|
const txs = new Map();
|
||||||
txs.set("tYmempool", { txid: "tYmempool", vin: [], vout: [beaconVout(), opreturnVout("TREG", "y", {}), mintVout(CAT_Y, "y")] });
|
txs.set("tYmempool", { txid: "tYmempool", vin: [{ txid: CAT_Y, vout: 0 }], vout: [beaconVout(), opreturnVout("TREG", "y", {}), mintVout(CAT_Y, "y")] });
|
||||||
txs.set("tNameUnderY", { txid: "tNameUnderY", vin: [], vout: [beaconVout(), opreturnVout("REG", "name.y", {}), mintVout(CAT_N, "name.y")] });
|
txs.set("tNameUnderY", { txid: "tNameUnderY", vin: [{ txid: CAT_N, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "name.y", {}), mintVout(CAT_N, "name.y")] });
|
||||||
|
|
||||||
const provider = mockProvider({
|
const provider = mockProvider({
|
||||||
nameHistory: [{ tx_hash: "tNameUnderY", height: 0 }],
|
nameHistory: [{ tx_hash: "tNameUnderY", height: 0 }],
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ await (async () => {
|
||||||
const CAT_TWO = "bb".repeat(32);
|
const CAT_TWO = "bb".repeat(32);
|
||||||
// Each tx pays the beacon (proof it belongs there), carries the TREG/TUPD
|
// Each tx pays the beacon (proof it belongs there), carries the TREG/TUPD
|
||||||
// OP_RETURN, and mints/transfers the TLD NFT (proof of ownership).
|
// OP_RETURN, and mints/transfers the TLD NFT (proof of ownership).
|
||||||
const t1 = { txid: "t1", vin: [], vout: [beaconOutput(rawTld), opReturnOutput("TREG", "x", { policy: "open", fee_bps: 500 }), nftMintOutput(CAT_X, "x")] };
|
const t1 = { txid: "t1", vin: [{ txid: CAT_X, vout: 0 }], vout: [beaconOutput(rawTld), opReturnOutput("TREG", "x", { policy: "open", fee_bps: 500 }), nftMintOutput(CAT_X, "x")] };
|
||||||
const t2 = { txid: "t2", vin: [], vout: [beaconOutput(rawTld), opReturnOutput("TREG", "two", { policy: "open" }), nftMintOutput(CAT_TWO, "two")] };
|
const t2 = { txid: "t2", vin: [{ txid: CAT_TWO, vout: 0 }], vout: [beaconOutput(rawTld), opReturnOutput("TREG", "two", { policy: "open" }), nftMintOutput(CAT_TWO, "two")] };
|
||||||
const t3 = { txid: "t3", vin: [{ txid: "t1", vout: 2 }], vout: [beaconOutput(rawTld), opReturnOutput("TUPD", "x", { policy: "covenant", fee_bps: 1000 }), nftMintOutput(CAT_X, "x", "bchtest:qnew-owner")] };
|
const t3 = { txid: "t3", vin: [{ txid: "t1", vout: 2 }], vout: [beaconOutput(rawTld), opReturnOutput("TUPD", "x", { policy: "covenant", fee_bps: 1000 }), nftMintOutput(CAT_X, "x", "bchtest:qnew-owner")] };
|
||||||
idx.processTx(t1, 100);
|
idx.processTx(t1, 100);
|
||||||
idx.processTx(t2, 100);
|
idx.processTx(t2, 100);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue