2026-10-05 03:54:15 +00:00
|
|
|
import { outputRanges, satAt, offsetOf } from './sats.js';
|
2026-10-05 02:33:54 +00:00
|
|
|
import { Tx } from '@cmdcode/tapscript';
|
|
|
|
|
import { readEnvelope } from './inscription.js';
|
|
|
|
|
|
|
|
|
|
// Rebuild solely from serialized transactions, never collectible/UI sidecars.
|
|
|
|
|
// Discovery is a convention, not native ord metaprotocol tag 7 support.
|
|
|
|
|
export class MetadataIndex {
|
|
|
|
|
constructor(chain) {
|
|
|
|
|
this.events = [];
|
2026-10-05 03:54:15 +00:00
|
|
|
this.utxos = new Map();
|
2026-10-05 02:33:54 +00:00
|
|
|
const visit = (raw, height, block) => {
|
|
|
|
|
const tx = Tx.decode(raw);
|
2026-10-05 03:54:15 +00:00
|
|
|
const txid = Tx.util.getTxid(tx), coinbase = tx.vin[0].txid === '0'.repeat(64);
|
|
|
|
|
const coins = coinbase ? [] : tx.vin.map(i => this.utxos.get(`${i.txid}:${i.vout}`));
|
|
|
|
|
const ranges = outputRanges(tx, coins, height, coinbase);
|
|
|
|
|
tx.vin.forEach(i => this.utxos.delete(`${i.txid}:${i.vout}`));
|
|
|
|
|
tx.vout.forEach((out, vout) => this.utxos.set(`${txid}:${vout}`, { txid, vout, value: Number(out.value), ranges: ranges[vout] }));
|
|
|
|
|
const witness = tx.vin.find(i => i.witness?.length === 3)?.witness;
|
|
|
|
|
if (!witness) return;
|
|
|
|
|
const item = readEnvelope(witness);
|
|
|
|
|
const pointer = BigInt(item.pointer ?? 0), total = tx.vout.reduce((n, o) => n + Number(o.value), 0);
|
|
|
|
|
const sat = satAt(ranges.flat(), pointer < BigInt(total) ? Number(pointer) : 0);
|
2026-10-05 02:33:54 +00:00
|
|
|
const metadata = item.metadata;
|
2026-10-05 03:54:15 +00:00
|
|
|
this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, delegate: item.delegate, pointer: item.pointer, sat,
|
|
|
|
|
reinscriptionPosition: this.events.filter(e => e.sat === sat).length,
|
2026-10-05 02:33:54 +00:00
|
|
|
status: height === null ? 'pending' : 'confirmed', height, block,
|
|
|
|
|
metadata: metadata ?? null, hasMetadata: metadata !== undefined, parent: item.parent,
|
|
|
|
|
protocol: metadata && !Array.isArray(metadata) && typeof metadata === 'object' &&
|
|
|
|
|
Object.hasOwn(metadata, 'protocol') && typeof metadata.protocol === 'string' ? metadata.protocol : null });
|
|
|
|
|
};
|
|
|
|
|
for (const block of chain.blocks) for (const raw of block.raws) visit(raw, block.height, block.hash);
|
|
|
|
|
for (const raw of chain.mempool) visit(raw, null, null);
|
2026-10-05 03:54:15 +00:00
|
|
|
for (const event of this.events) {
|
|
|
|
|
event.location = null;
|
|
|
|
|
for (const coin of this.utxos.values()) {
|
|
|
|
|
const offset = offsetOf(coin.ranges, event.sat);
|
|
|
|
|
if (offset !== null) { event.location = { txid: coin.txid, vout: coin.vout, offset }; break; }
|
|
|
|
|
}
|
|
|
|
|
event.delegatedBy = this.events.filter(e => e.delegate === event.id).map(e => e.id);
|
|
|
|
|
event.inscriptions = this.events.filter(e => e.sat === event.sat).map(e => e.id);
|
|
|
|
|
}
|
2026-10-05 02:33:54 +00:00
|
|
|
}
|
|
|
|
|
forProtocol(name) { return this.events.filter(e => e.protocol === name); }
|
|
|
|
|
get protocols() { return [...new Set(this.events.filter(e => e.protocol !== null).map(e => e.protocol))].sort(); }
|
|
|
|
|
get stats() { return { events: this.events.length, withMetadata: this.events.filter(e => e.hasMetadata).length,
|
|
|
|
|
confirmed: this.events.filter(e => e.status === 'confirmed').length,
|
|
|
|
|
pending: this.events.filter(e => e.status === 'pending').length, protocols: this.protocols }; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trusted, application-bundled reducers only. Adding a plugin requires a code change/review.
|
|
|
|
|
// No eval, dynamic imports, inscription scripts, or interpretation in MetadataIndex.
|
|
|
|
|
const reducers = Object.freeze({
|
|
|
|
|
'event-counter': events => events.reduce((state, event) => ({
|
|
|
|
|
count: state.count + 1, confirmed: state.confirmed + Number(event.status === 'confirmed'),
|
|
|
|
|
pending: state.pending + Number(event.status === 'pending'), ids: [...state.ids, event.id],
|
|
|
|
|
}), { count: 0, confirmed: 0, pending: 0, ids: [] }),
|
|
|
|
|
});
|
|
|
|
|
export function deriveProtocol(index, protocol, reducerName) {
|
|
|
|
|
if (!Object.hasOwn(reducers, reducerName)) throw new Error('Unknown trusted local reducer.');
|
|
|
|
|
return reducers[reducerName](structuredClone(index.forProtocol(protocol)));
|
|
|
|
|
}
|