Add CBOR metadata indexes, local API and coordinated auto mining
This commit is contained in:
1 parent
0405452ab2
commit
aa39db3879
14 files changed
+599
-54
No files matched your search
+80
-21
@@ -1,9 +1,13 @@
|
||||
import './style.css';
|
||||
import { encodeMetadata, parseMetadataJSON } from './adapters/metadata.js';
|
||||
import { MetadataIndex, deriveProtocol } from './adapters/metadata-index.js';
|
||||
import { LocalOrdAPI } from './adapters/local-ord-api.js';
|
||||
import { mineLab, setMiner, minerStatus } from './miner.js';
|
||||
import { TestWallet } from './adapters/wallet.js';
|
||||
import { LocalRPC } from './adapters/chain.js';
|
||||
import { InscriptionAdapter, MAX_BYTES, MIME_TYPES } from './adapters/inscription.js';
|
||||
import { preview, clearPreviews } from './preview.js';
|
||||
import { STORAGE_KEY, readLab, saveLab, decodeLab, encodeLab } from './storage.js';
|
||||
import { STORAGE_KEY, readLab, mutateLab, decodeLab, encodeLab } from './storage.js';
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const btc = sats => (sats / 1e8).toFixed(8);
|
||||
@@ -47,13 +51,13 @@ function download(name, contents) {
|
||||
function closeReview() {
|
||||
reviewed = null; $('review-panel').hidden = true;
|
||||
clearPreviews($('review-preview')); $('review-preview').replaceChildren();
|
||||
$('review-text').textContent = ''; $('review-parent').textContent = '';
|
||||
$('review-metadata').textContent = ''; $('review-text').textContent = ''; $('review-parent').textContent = '';
|
||||
}
|
||||
const composerBody = () => ({ contentType: $('mime-type').value, bytes: fileBytes ?? new TextEncoder().encode($('payload').value) });
|
||||
function updateCount() { $('byte-count').textContent = `${composerBody().bytes.length} / ${MAX_BYTES} bytes`; }
|
||||
function resetComposer() {
|
||||
fileRevision++; fileBytes = null; $('body-file').value = ''; $('payload').value = '';
|
||||
$('parent').value = ''; closeReview(); updateCount();
|
||||
$('metadata').value = ''; updateMetadataCount(); $('parent').value = ''; closeReview(); updateCount();
|
||||
}
|
||||
function lineage(item, chain) {
|
||||
const p = el('p', undefined, 'tx-links');
|
||||
@@ -64,6 +68,54 @@ function lineage(item, chain) {
|
||||
}
|
||||
|
||||
|
||||
function updateMetadataCount() {
|
||||
try {
|
||||
const value = parseMetadataJSON($('metadata').value);
|
||||
$('metadata-count').textContent = `${value === undefined ? 0 : encodeMetadata(value).length} / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only`;
|
||||
} catch (error) { $('metadata-count').textContent = `${error.message} Limit: 2048 CBOR bytes.`; }
|
||||
}
|
||||
function metadataView(item) {
|
||||
const node = el('div', undefined, 'metadata-document');
|
||||
if (item.metadata !== undefined) node.append(el('strong', 'Decoded metadata'), el('pre', JSON.stringify(item.metadata, null, 2)));
|
||||
return node;
|
||||
}
|
||||
function renderIndexes(chain) {
|
||||
if (!chain) return;
|
||||
const index = new MetadataIndex(chain), selected = $('protocol').value;
|
||||
const stats = index.stats;
|
||||
$('index-stats').textContent = `Metadata index: ${stats.events} events · ${stats.withMetadata} with metadata · ${stats.confirmed} confirmed · ${stats.pending} pending · ${stats.protocols.length} protocols`;
|
||||
$('protocol').replaceChildren(new Option('Select a protocol', ''));
|
||||
index.protocols.forEach(name => $('protocol').append(new Option(JSON.stringify(name), JSON.stringify(name))));
|
||||
if ([...$('protocol').options].some(o => o.value === selected)) $('protocol').value = selected;
|
||||
const protocol = $('protocol').value ? JSON.parse($('protocol').value) : null;
|
||||
const events = protocol === null ? [] : index.forProtocol(protocol);
|
||||
$('protocol-stats').textContent = `${events.length} events · ${events.filter(e => e.status === 'confirmed').length} confirmed · ${events.filter(e => e.status === 'pending').length} pending`;
|
||||
$('protocol-events').replaceChildren();
|
||||
for (const event of events) {
|
||||
const row = el('article'); row.append(link(short(event.id), `#inscription/${event.id}`), el('pre', JSON.stringify(event, null, 2)));
|
||||
$('protocol-events').append(row);
|
||||
}
|
||||
$('derived-state').textContent = !$('reducer').value ? 'No reducer selected.' : protocol === null ? 'Select a protocol first.' : JSON.stringify(deriveProtocol(index, protocol, $('reducer').value), null, 2);
|
||||
}
|
||||
$('protocol').onchange = $('reducer').onchange = () => renderIndexes(lab.chain);
|
||||
const apiPresets = ['/r/blockheight', '/r/blockhash/1', '/r/blockinfo/1', '/r/blockinfo/:hash', '/content/:id', '/r/inscription/:id', '/r/metadata/:id', '/r/parents/:id', '/r/children/:id', '/r/tx/:txid', '/local/metadata-index', '/local/protocols', '/local/protocol?name=example'];
|
||||
apiPresets.forEach(path => $('api-preset').append(new Option(path, path)));
|
||||
$('api-preset').onchange = () => {
|
||||
const event = new MetadataIndex(lab.chain).events.at(-1);
|
||||
$('api-path').value = $('api-preset').value.replace(':id', event?.id ?? ':id').replace(':txid', event?.id.slice(0, -2) ?? ':txid').replace(':hash', lab.chain.blocks.at(-1)?.hash ?? ':hash');
|
||||
};
|
||||
$('api-try').onclick = () => {
|
||||
try {
|
||||
const response = new LocalOrdAPI(lab.chain).request($('api-path').value);
|
||||
const binary = response.body instanceof Uint8Array;
|
||||
$('api-response').textContent = JSON.stringify({ ...response, ...(binary ? { body: Array.from(response.body), encoding: 'byte array', textPreview: new TextDecoder().decode(response.body) } : {}) }, null, 2);
|
||||
} catch (error) { $('api-response').textContent = error.message; }
|
||||
};
|
||||
$('api-copy').onclick = async () => {
|
||||
try { await navigator.clipboard.writeText($('api-response').textContent); message('Local API response copied.'); }
|
||||
catch (error) { message(`Could not copy response: ${error.message}`, true); }
|
||||
};
|
||||
|
||||
function renderMempool(chain) {
|
||||
const pane = $('mempool-list');
|
||||
pane.replaceChildren();
|
||||
@@ -107,8 +159,11 @@ function render() {
|
||||
$('wallet-state').textContent = wallet ? 'LOCAL / TEST ONLY' : 'NOT CREATED';
|
||||
$('create-wallet').disabled = disabled; $('load-wallet').disabled = disabled;
|
||||
$('mine').disabled = disabled || !wallet;
|
||||
$('miner-interval').disabled = disabled || !wallet;
|
||||
$('miner-interval').value = String(lab?.miner?.interval ?? 0);
|
||||
$('miner-status').textContent = minerStatus(lab);
|
||||
$('payload').disabled = disabled || !wallet || !!fileBytes || fileLoading || $('mime-type').value.startsWith('image/') && $('mime-type').value !== 'image/svg+xml';
|
||||
for (const id of ['mime-type', 'body-file', 'parent']) $(id).disabled = disabled || !wallet || fileLoading;
|
||||
for (const id of ['mime-type', 'body-file', 'parent', 'metadata']) $(id).disabled = disabled || !wallet || fileLoading;
|
||||
$('review').disabled = disabled || !wallet || !chain?.coins(wallet.script).some(c => !c.inscription) || fileLoading || !composerBody().bytes.length;
|
||||
$('sign').disabled = disabled;
|
||||
$('export-wallet').disabled = disabled; $('export-lab').disabled = busy;
|
||||
@@ -140,8 +195,9 @@ function render() {
|
||||
const trail = el('p', undefined, 'tx-links');
|
||||
trail.append(link('View reveal transaction ↗', `#tx/${item.txid}`));
|
||||
if (item.height) trail.append(link(`View block ${item.height} ↗`, `#block/${chain.blocks[item.height - 1].hash}`));
|
||||
card.append(lineage(item, chain), trail); $('collectibles').append(card);
|
||||
card.append(metadataView(item), lineage(item, chain), trail); $('collectibles').append(card);
|
||||
}
|
||||
renderIndexes(chain);
|
||||
renderMempool(chain);
|
||||
renderBlocks(chain);
|
||||
inspect(false);
|
||||
@@ -151,13 +207,8 @@ async function mutate(action, success, onSuccess = () => {}) {
|
||||
if (busy || failed) return;
|
||||
busy = true; render();
|
||||
try {
|
||||
if (!navigator.locks) throw new Error('This app needs a secure-context browser with Web Locks. Use localhost or HTTPS.');
|
||||
await navigator.locks.request(STORAGE_KEY, async () => {
|
||||
const candidate = readLab();
|
||||
await action(candidate);
|
||||
saveLab(candidate); lab = candidate;
|
||||
});
|
||||
closeReview(); onSuccess(); message(success);
|
||||
const result = await mutateLab(action); lab = result.lab;
|
||||
if (result.changed) { closeReview(); onSuccess(); message(success); }
|
||||
} catch (error) { message(error.message, true); }
|
||||
finally { busy = false; render(); }
|
||||
}
|
||||
@@ -167,10 +218,17 @@ $('create-wallet').onclick = () => mutate(next => {
|
||||
if (next.wallet) throw new Error('A wallet already exists in this browser. Reload to use it.');
|
||||
next.wallet = TestWallet.create();
|
||||
}, 'Test wallet created. Next: mine a block to get 50 fake BTC.');
|
||||
$('mine').onclick = () => mutate(next => {
|
||||
if (!next.wallet) throw new Error('Create or load a wallet first.');
|
||||
next.chain.mine(next.wallet.script);
|
||||
}, 'Block mined. Pending transactions moved from the mempool into your newest block.');
|
||||
const mine = automatic => mutate(next => mineLab(next, automatic), 'Block mined. Pending transactions moved from the mempool into your newest block.');
|
||||
$('mine').onclick = () => mine(false);
|
||||
$('miner-interval').onchange = () => {
|
||||
const interval = Number($('miner-interval').value);
|
||||
mutate(next => setMiner(next, interval), interval ? `Auto miner set to ${interval}s.` : 'Auto mining OFF');
|
||||
};
|
||||
setInterval(() => {
|
||||
$('miner-status').textContent = minerStatus(lab);
|
||||
if (!busy && !failed && lab?.wallet && lab.miner.interval && Date.now() >= lab.miner.nextAt) mine(true);
|
||||
}, 250);
|
||||
$('metadata').oninput = () => { closeReview(); updateMetadataCount(); };
|
||||
MIME_TYPES.forEach(type => $('mime-type').append(new Option(type, type)));
|
||||
$('payload').oninput = () => { closeReview(); updateCount(); render(); };
|
||||
$('parent').onchange = closeReview;
|
||||
@@ -194,13 +252,14 @@ $('body-file').onchange = async event => {
|
||||
};
|
||||
$('review').onclick = () => {
|
||||
try {
|
||||
const payload = composerBody(), parent = $('parent').value || null;
|
||||
new InscriptionAdapter(lab.chain, lab.wallet).prepare(payload, parent);
|
||||
const payload = composerBody(), parent = $('parent').value || null, metadata = parseMetadataJSON($('metadata').value);
|
||||
new InscriptionAdapter(lab.chain, lab.wallet).prepare(payload, parent, metadata);
|
||||
closeReview();
|
||||
reviewed = { payload, parent, address: lab.wallet.address };
|
||||
reviewed = { payload, parent, metadata, address: lab.wallet.address };
|
||||
$('review-text').textContent = ['text/plain', 'application/json'].includes(payload.contentType) ? new TextDecoder().decode(payload.bytes) : `${payload.contentType} · ${payload.bytes.length} bytes`;
|
||||
if (!['text/plain', 'application/json'].includes(payload.contentType)) $('review-preview').append(preview(payload));
|
||||
$('review-parent').textContent = parent ? `Parent: ${parent}. Its output will be spent and its full value returned to this wallet. This relationship is immutable.` : 'No parent.';
|
||||
$('review-metadata').textContent = metadata === undefined ? 'No metadata.' : `Metadata (CBOR, tag 5): ${JSON.stringify(metadata, null, 2)}`;
|
||||
$('review-panel').hidden = false;
|
||||
$('sign').focus(); message('Review the payload and fake-sat costs. Nothing has been signed yet.');
|
||||
} catch (error) { message(error.message, true); }
|
||||
@@ -211,7 +270,7 @@ $('sign').onclick = () => {
|
||||
if (!consent) return;
|
||||
mutate(next => {
|
||||
if (next.wallet?.address !== consent.address) throw new Error('Wallet changed. Review again before signing.');
|
||||
new InscriptionAdapter(next.chain, next.wallet).inscribe(consent.payload, consent.parent);
|
||||
new InscriptionAdapter(next.chain, next.wallet).inscribe(consent.payload, consent.parent, consent.metadata);
|
||||
}, 'Inscription submitted. Its commit + reveal are now visible in the mempool; mine a block to confirm them.', resetComposer);
|
||||
};
|
||||
$('export-wallet').onclick = () => download('super-testnet-wallet.json', JSON.stringify(lab.wallet.backup(), null, 2));
|
||||
@@ -277,7 +336,7 @@ function inspect(scroll = true) {
|
||||
if (record.inscription) {
|
||||
add('Inscription ID', record.inscription.id); add('Content type', record.inscription.contentType);
|
||||
add('Body size', `${record.inscription.bytes.length} bytes`);
|
||||
pane.append(preview(record.inscription), lineage(record.inscription, chain));
|
||||
pane.append(preview(record.inscription), metadataView(record.inscription), lineage(record.inscription, chain));
|
||||
pane.append(link('Inspect commit transaction ↗', `#tx/${record.inscription.commitTxid}`));
|
||||
}
|
||||
pane.append(el('h3', 'Inputs → outputs'));
|
||||
|
||||
Reference in new issue
Block a user