From 9f27897967baf8daf4c33399141a591727adb1de Mon Sep 17 00:00:00 2001 From: recovery Date: Mon, 5 Oct 2026 03:56:58 +0000 Subject: [PATCH] Index delegates and sat locations --- src/adapters/local-ord-api.js | 33 +++++++++++++++++++++++++++++---- src/adapters/metadata-index.js | 26 +++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/adapters/local-ord-api.js b/src/adapters/local-ord-api.js index 3c6b243..f6b9ec8 100644 --- a/src/adapters/local-ord-api.js +++ b/src/adapters/local-ord-api.js @@ -1,3 +1,4 @@ +import { offsetOf } from './sats.js'; import { Tx } from '@cmdcode/tapscript'; import { readEnvelope } from './inscription.js'; import { MetadataIndex } from './metadata-index.js'; @@ -13,6 +14,15 @@ export class LocalOrdAPI { const chain = this.chain, index = this.index ?? new MetadataIndex(chain); const blockInfo = block => ({ height: block.height, hash: block.hash, previous: block.previous, time: block.time, txids: block.raws.map(raw => Tx.util.getTxid(raw)) }); + if (root === 'local' && kind === 'sat' && /^(0|[1-9][0-9]*)$/.test(key ?? '')) { + const sat = Number(key), events = index.events.filter(e => e.sat === sat); + const coin = [...index.utxos.values()].find(c => c.ranges.some(([a,b]) => sat >= a && sat < b)); + if (coin || events.length) return response({ sat, model: 'synthetic-local-fifo', inscriptions: events.map(e => e.id), location: coin ? { txid: coin.txid, vout: coin.vout, offset: offsetOf(coin.ranges, sat) } : null, ranges: coin?.ranges ?? [] }); + } + if (root === 'local' && kind === 'utxo') { + const coin = index.utxos.get(key); + if (coin) return response({ ...coin, inscriptions: index.events.filter(e => e.location?.txid === coin.txid && e.location.vout === coin.vout).map(e => e.id) }); + } if (url.pathname === '/local/metadata-index') return response({ ...index.stats, events: index.events }); if (url.pathname === '/local/protocols') return response(index.protocols.map(protocol => { const events = index.forProtocol(protocol); @@ -32,15 +42,16 @@ export class LocalOrdAPI { const event = index.events.find(e => e.id === id); if (event) { if (root === 'content') { - const record = chain.transactions.get(id.slice(0, -2)); - const item = readEnvelope(Tx.decode(record.raw).vin[0].witness); - return response(item.bytes, item.contentType); + return resolveContent(index, id, target => { + const record = chain.transactions.get(target.replace(/i[0-9]+$/, '')); + return readEnvelope(Tx.decode(record.raw).vin.find(i => i.witness?.length === 3).witness); + }); } if (kind === 'inscription') return response(event); // ord's metadata endpoint returns CBOR hex. Expose decoded JSON in inscription/index responses. if (kind === 'metadata') { const record = chain.transactions.get(id.slice(0, -2)); - const item = readEnvelope(Tx.decode(record.raw).vin[0].witness); + const item = readEnvelope(Tx.decode(record.raw).vin.find(i => i.witness?.length === 3).witness); return response(item.metadataBytes ? Array.from(item.metadataBytes, b => b.toString(16).padStart(2, '0')).join('') : null); } if (kind === 'parents') return response({ ids: event.parent ? [event.parent] : [] }); @@ -49,3 +60,17 @@ export class LocalOrdAPI { return response({ error: 'Not found in this local lab.' }, undefined, 404); } } + +// Identity is the requested id even when the bytes/type come from another inscription. +export function resolveContent(index, id, read) { + const visited = new Set(); let target = id; + while (true) { + if (visited.has(target)) return { status: 508, contentType: 'application/json', body: { error: 'Delegate loop detected.' } }; + visited.add(target); + const event = index.events.find(e => e.id === target); + if (!event) return { status: 404, contentType: 'application/json', body: { error: 'Delegate target not found.' } }; + if (event.delegate) { target = event.delegate; continue; } + const item = read(target); + return { status: 200, contentType: item.contentType, body: item.bytes, inscriptionId: id, contentId: target }; + } +} diff --git a/src/adapters/metadata-index.js b/src/adapters/metadata-index.js index e71fb90..68dda34 100644 --- a/src/adapters/metadata-index.js +++ b/src/adapters/metadata-index.js @@ -1,3 +1,4 @@ +import { outputRanges, satAt, offsetOf } from './sats.js'; import { Tx } from '@cmdcode/tapscript'; import { readEnvelope } from './inscription.js'; @@ -6,12 +7,22 @@ import { readEnvelope } from './inscription.js'; export class MetadataIndex { constructor(chain) { this.events = []; + this.utxos = new Map(); const visit = (raw, height, block) => { const tx = Tx.decode(raw); - if (tx.vin[0]?.witness?.length !== 3) return; - const item = readEnvelope(tx.vin[0].witness); + 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); const metadata = item.metadata; - this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, + 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, status: height === null ? 'pending' : 'confirmed', height, block, metadata: metadata ?? null, hasMetadata: metadata !== undefined, parent: item.parent, protocol: metadata && !Array.isArray(metadata) && typeof metadata === 'object' && @@ -19,6 +30,15 @@ export class MetadataIndex { }; 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); + 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); + } } 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(); }