Add CBOR metadata indexes, local API and coordinated auto mining

This commit is contained in:
Codex committed 2026-10-05 02:33:54 +00:00
1 parent 0405452ab2
commit aa39db3879
14 files changed
+599 -54

No files matched your search

+139
View File
@@ -189,3 +189,142 @@ test('child reveal requires a valid parent-input signature', () => {
assert.throws(() => chain.submitPackage([commit, Tx.encode(reveal).hex]), /signature/);
assert.deepEqual(chain.snapshot(), before);
});
import { encodeMetadata, decodeMetadata, parseMetadataJSON, MAX_METADATA_BYTES } from '../src/adapters/metadata.js';
import { readEnvelope } from '../src/adapters/inscription.js';
import { MetadataIndex, deriveProtocol } from '../src/adapters/metadata-index.js';
import { LocalOrdAPI } from '../src/adapters/local-ord-api.js';
import { minerPreference, minerStatus, mineLab, setMiner } from '../src/miner.js';
import { mutateLab } from '../src/storage.js';
test('CBOR known vectors, JSON values, nesting safety and optional metadata', () => {
for (const [value, hex] of [[null,'f6'],[true,'f5'],[false,'f4'],[0,'00'],[-1,'20'],[24,'1818'],['hi','626869'],[{a:1},'a1616101'],[[1,2],'820102']]) {
assert.equal(Buffer.from(encodeMetadata(value)).toString('hex'),hex);
assert.deepEqual(decodeMetadata(Buffer.from(hex,'hex')),value);
}
const value = JSON.parse('{"__proto__":{"polluted":true},"values":[-0,1.5,4294967296,-9007199254740991,"🌱",null,true]}');
assert.deepEqual(decodeMetadata(encodeMetadata(value)), value);
assert.equal({}.polluted, undefined);
assert.equal(parseMetadataJSON(' '), undefined);
assert.throws(() => parseMetadataJSON('{bad'), /Invalid metadata JSON/);
assert.throws(() => parseMetadataJSON('1e999'), /JSON values/);
assert.throws(() => parseMetadataJSON(JSON.stringify('x'.repeat(2048))), /2048/);
assert.throws(() => decodeMetadata(Uint8Array.of(0x7a,255,255,255,255)), /Truncated/);
assert.throws(() => decodeMetadata(Uint8Array.of(0,0)), /Trailing/);
assert.throws(() => encodeMetadata(Array.from({ length: 1 }, () => undefined)), /JSON values/);
let nested = 0; for (let i=0;i<66;i++) nested=[nested];
assert.throws(() => encodeMetadata(nested), /nesting/);
});
test('metadata repeated tag 5 chunks reassemble from witness; body, parent and maximum caps independent', () => {
const {ins, chain, wallet} = setup();
const parent = ins.inscribe('parent');
const metadata = { protocol: 'example', text: '🌱'.repeat(300), arbitrary: [1,true,null] };
const id = ins.inscribe({ contentType: 'image/png', bytes: new Uint8Array(MAX_BYTES).fill(128) }, parent, metadata);
const record = chain.transactions.get(id.slice(0,-2));
const parts = Script.decode(record.tx.vin[0].witness[1]);
const chunks=[]; let i=9;
while (parts[i] === '05') { chunks.push(parts[i+1]); i+=2; }
assert.ok(chunks.length > 1);
assert.ok(chunks.every(chunk => chunk.length / 2 <= 520));
assert.deepEqual(decodeMetadata(Buffer.from(chunks.join(''),'hex')),metadata);
assert.deepEqual(readEnvelope(record.tx.vin[0].witness).metadata, metadata);
assert.equal(ins.list().find(x=>x.id===id).parent,parent);
assert.deepEqual(decodeLab(encodeLab({wallet,chain})).chain.transactions.get(record.txid).inscription.metadata,metadata);
const max = 'x'.repeat(MAX_METADATA_BYTES - 3);
assert.equal(encodeMetadata(max).length,MAX_METADATA_BYTES);
ins.inscribe('max', null, max);
// Reject before any signing is invoked, including direct adapter callers.
wallet.sign = () => { throw new Error('Signing must not happen'); };
const before = chain.snapshot();
assert.throws(() => ins.inscribe('bad',null,max+'x'), /2048 CBOR/);
assert.deepEqual(chain.snapshot(),before);
});
test('raw metadata index rebuild and protocol grouping do not interpret semantics; reducer sees only ordered matching events', () => {
const {ins,chain,wallet} = setup();
const a = ins.inscribe('a',null,{protocol:'counter',op:'burn',amount:'not-a-number'});
const b = ins.inscribe('b',null,{protocol:'different',op:'execute',code:'throw new Error()'});
chain.mine(wallet.script);
const c = ins.inscribe('c',a,{protocol:'counter',op:'unknown'});
ins.inscribe('not discovered',null,{nested:{protocol:'hidden'},protocol:42});
ins.inscribe('no metadata');
// Poison/drop derived records: index takes only confirmed raw blocks and mempool.
const rawOnly = {blocks:structuredClone(chain.blocks),mempool:[...chain.mempool],transactions:new Map()};
chain.transactions.get(a.slice(0,-2)).inscription.metadata={protocol:'forged'};
rawOnly.blocks[1].metadata={protocol:'sidecar'};
const index = new MetadataIndex(rawOnly);
assert.deepEqual(index.protocols,['counter','different']);
assert.deepEqual(index.events.slice(0,3).map(e=>e.id),[a,b,c]);
assert.deepEqual(index.stats,{events:5,withMetadata:4,confirmed:2,pending:3,protocols:['counter','different']});
assert.equal(index.events[0].metadata.amount,'not-a-number');
assert.equal(index.events[0].block,chain.blocks[1].hash);
assert.equal(index.events[2].parent,a);
assert.deepEqual(deriveProtocol(index,'counter','event-counter'),{count:2,confirmed:1,pending:1,ids:[a,c]});
assert.throws(()=>deriveProtocol(index,'counter','constructor'),/Unknown trusted/);
const noProtocol = ins.inscribe('null',null,null);
const event=new MetadataIndex(chain).events.find(e=>e.id===noProtocol);
assert.equal(event.hasMetadata,true); assert.equal(event.protocol,null);
});
test('LocalOrdAPI serves exact content, CBOR metadata, lineage, transaction and block lookups', () => {
const {ins,chain,wallet} = setup();
const metadata={protocol:'a / b',n:1};
const parent=ins.inscribe('parent',null,metadata), child=ins.inscribe('child',parent), grandchild=ins.inscribe('nested',child);
chain.mine(wallet.script);
const api=new LocalOrdAPI(chain), get=p=>{const r=api.request(p); assert.equal(r.status,200);return r.body;};
assert.deepEqual(get(`/content/${parent}`),new TextEncoder().encode('parent'));
assert.equal(api.request(`/content/${parent}`).contentType,'text/plain;charset=utf-8');
assert.deepEqual(decodeMetadata(Buffer.from(get(`/r/metadata/${parent}`),'hex')),metadata);
assert.equal(get(`/r/metadata/${child}`),null);
assert.deepEqual(get(`/r/inscription/${parent}`).metadata,metadata);
assert.deepEqual(get(`/r/parents/${child}`),{ids:[parent]});
assert.deepEqual(get(`/r/children/${parent}`),{ids:[child]});
assert.deepEqual(get(`/r/children/${child}`),{ids:[grandchild]});
assert.deepEqual(get(`/r/parents/${parent}`),{ids:[]});
const txid=parent.slice(0,-2);
assert.equal(Tx.util.getTxid(get(`/r/tx/${txid}`).hex),txid);
assert.equal(get('/r/blockheight'),2);
assert.equal(get('/r/blockhash/2'),chain.blocks[1].hash);
assert.deepEqual(get('/r/blockinfo/2'),get(`/r/blockinfo/${chain.blocks[1].hash}`));
assert.ok(get('/r/blockinfo/2').txids.includes(txid));
assert.equal(get('/local/metadata-index').events.length,3);
assert.deepEqual(get('/local/protocols'),[{protocol:'a / b',events:1,confirmed:1,pending:0}]);
assert.deepEqual(get('/local/protocol?name=a%20%2F%20b').map(e=>e.id),[parent]);
for(const path of ['/r/blockhash/0','/r/blockinfo/missing','/content/missing','/r/tx/missing','/unknown']) assert.equal(api.request(path).status,404);
assert.equal(api.request('https://example.com').status,400);
const later = ins.inscribe('later', null, { protocol: 'new' });
assert.equal(get(`/r/inscription/${later}`).status, 'pending');
});
test('miner defaults/persistence, countdown, manual schedule reset, overdue mining and validation', () => {
const {chain,wallet}=setup(); const lab={chain,wallet,miner:minerPreference()};
assert.equal(minerStatus(lab),'Auto mining OFF');
assert.equal(mineLab(lab,true,1000),false);
setMiner(lab,10,1000); assert.equal(minerStatus(lab,2000),'Next block in 9s · 10s interval');
assert.deepEqual(decodeLab(encodeLab(lab)).miner,{interval:10,nextAt:11000});
assert.equal(mineLab(lab,true,10999),false);
assert.equal(mineLab(lab,false,5000),true); assert.equal(lab.miner.nextAt,15000);
new InscriptionAdapter(chain,wallet).inscribe('pending');
assert.equal(mineLab(lab,true,20000),true); assert.equal(chain.mempool.length,0);
assert.equal(lab.miner.nextAt,30000); assert.equal(chain.blocks.length,3);
setMiner(lab,0); assert.equal(lab.miner.nextAt,null);
assert.throws(()=>minerPreference({interval:2,nextAt:10}),/Invalid miner/);
assert.throws(()=>minerPreference({interval:10,nextAt:null}),/Invalid miner/);
assert.deepEqual(decodeLab(null).miner,{interval:0,nextAt:null});
});
test('two auto miners re-read deadline under shared lock; manual mining uses identical path', async () => {
const {chain,wallet}=setup();const initial={chain,wallet};setMiner(initial,10,1000);
let stored=encodeLab(initial),tail=Promise.resolve(),writes=0,lockNames=[];
const io={read:()=>decodeLab(stored),save:lab=>{stored=encodeLab(lab);writes++;},locks:{request(name,action){lockNames.push(name);const next=tail.then(action);tail=next.catch(()=>{});return next;}}};
const result=await Promise.all([mutateLab(lab=>mineLab(lab,true,11000),io),mutateLab(lab=>mineLab(lab,true,11000),io)]);
assert.deepEqual(result.map(r=>r.changed),[true,false]);assert.equal(writes,1);
assert.equal(decodeLab(stored).chain.blocks.length,2);
await mutateLab(lab=>mineLab(lab,false,11500),io);
assert.equal(decodeLab(stored).chain.blocks.length,3);
assert.equal(decodeLab(stored).miner.nextAt,21500);
assert.equal(new Set(lockNames).size,1);
await assert.rejects(mutateLab(lab=>mineLab(lab,false),{...io,save:()=>{throw new Error('quota');}}),/quota/);
assert.equal(decodeLab(stored).chain.blocks.length,3);
});
+88 -7
View File
@@ -5,11 +5,11 @@ test('create → mine → mempool → inscription → block/transaction → relo
page.on('pageerror',e=>errors.push(e.message));
page.on('request',r=>{ if(!r.url().startsWith('http://127.0.0.1:4178')) remote.push(r.url()); });
await page.goto('/');
await expect(page.getByRole('button',{name:'Mine a block'})).toBeDisabled();
await expect(page.getByRole('button',{name:'Mine now'})).toBeDisabled();
await expect(page.locator('#mempool-list')).toContainText('Mempool empty');
await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
await page.getByRole('button',{name:'Mine a block'}).click();
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#balance')).toHaveText('50.00000000');
await expect(page.locator('#blocks .block-card')).toHaveCount(1);
await expect(page.locator('#blocks .block-card').first()).toContainText('HEIGHT 1');
@@ -32,7 +32,7 @@ test('create → mine → mempool → inscription → block/transaction → relo
await expect(page.locator('.collectible pre')).toHaveText(payload);
await expect(page.locator('#pending')).toHaveText('2');
await expect(page.locator('#mempool-list .mempool-card')).toHaveCount(2);
await page.getByRole('button',{name:'Mine a block'}).click();
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#mempool-list')).toContainText('Mempool empty');
await expect(page.locator('#blocks .block-card')).toHaveCount(2);
await expect(page.locator('#blocks .block-card').first()).toContainText('HEIGHT 2');
@@ -90,7 +90,7 @@ test('wallet backup reload and rejected oversized or invalid input',async({page}
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
const address=await page.locator('#address').inputValue();
const [download]=await Promise.all([page.waitForEvent('download'),page.getByRole('button',{name:'Save test wallet'}).click()]);
await page.getByRole('button',{name:'Mine a block'}).click();
await page.getByRole('button',{name:'Mine now'}).click();
await page.locator('#payload').fill('🌱'.repeat(4097));
await page.getByRole('button',{name:'Review inscription'}).click();
await expect(page.getByRole('status')).toContainText('at most 16384');
@@ -109,7 +109,7 @@ test('two tabs serialize mining and refresh shared storage',async({page,context}
await page.goto('/'); await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
const other=await context.newPage(); await other.goto('/');
await Promise.all([page.getByRole('button',{name:'Mine a block'}).click(),other.getByRole('button',{name:'Mine a block'}).click()]);
await Promise.all([page.getByRole('button',{name:'Mine now'}).click(),other.getByRole('button',{name:'Mine now'}).click()]);
await expect(page.locator('#height')).toHaveText('2');
await expect(other.locator('#height')).toHaveText('2');
await expect(page.locator('#balance')).toHaveText('100.00000000');
@@ -120,13 +120,13 @@ test('storage failure leaves the displayed and persisted chain unchanged', async
await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
await page.evaluate(()=>{ Storage.prototype.setItem = () => { throw new DOMException('Quota exceeded','QuotaExceededError'); }; });
await page.getByRole('button',{name:'Mine a block'}).click();
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.getByRole('status')).toContainText('No changes were saved');
await expect(page.locator('#height')).toHaveText('0');
await expect(page.locator('#balance')).toHaveText('0.00000000');
await page.reload();
await expect(page.locator('#height')).toHaveText('0');
await page.getByRole('button',{name:'Mine a block'}).click();
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#balance')).toHaveText('50.00000000');
});
test('MIME upload, reset only after success, and parent/child navigation', async ({ page }) => {
@@ -175,3 +175,84 @@ test('HTML preview cannot execute, navigate or fetch remote resources', async ({
expect(remote).toEqual([]);
await expect(page).toHaveURL('http://127.0.0.1:4178/');
});
test('metadata review validation, witness persistence, indexes/reducer and local API explorer', async ({page,context}) => {
await page.goto('/'); await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#payload').fill('independent body');
await page.locator('#metadata').fill('{bad'); await page.locator('#review').click();
await expect(page.getByRole('status')).toContainText('Invalid metadata JSON');
await expect(page.locator('#review-panel')).toBeHidden();
await page.locator('#metadata').fill(JSON.stringify({large:'x'.repeat(2048)})); await page.locator('#review').click();
await expect(page.getByRole('status')).toContainText('2048 CBOR');
await expect(page.locator('#pending')).toHaveText('0');
const metadata={protocol:'example',note:'chunked '+ 'x'.repeat(600),nested:{data:true},code:'window.unwanted=true'};
await page.locator('#metadata').fill(JSON.stringify(metadata));
await page.locator('#review').click();
await expect(page.locator('#review-metadata')).toContainText('CBOR, tag 5');
await page.locator('#sign').click();
await expect(page.locator('#metadata')).toHaveValue('');
await expect(page.locator('#metadata-count')).toContainText('0 / 2048');
await expect(page.locator('.metadata-document')).toContainText('example');
await page.reload();
await expect(page.locator('.metadata-document')).toContainText('chunked');
await page.getByRole('link',{name:/^Inscription /}).click();
await expect(page.locator('#inspector .metadata-document')).toContainText('example');
await expect(page.locator('#index-stats')).toContainText('1 events · 1 with metadata · 0 confirmed · 1 pending · 1 protocols');
await page.locator('#protocol').selectOption('"example"');
await expect(page.locator('#protocol-events')).not.toContainText('independent'); // Body is not part of metadata events.
await page.locator('#reducer').selectOption('event-counter');
await expect(page.locator('#derived-state')).toContainText('"count": 1');
await expect(page.locator('#protocol-events')).toContainText('"status": "pending"');
await page.locator('#mine').click();
await expect(page.locator('#protocol-events')).toContainText('"status": "confirmed"');
await expect(page.locator('#derived-state')).toContainText('"confirmed": 1');
await expect(page.locator('#api-explorer')).toContainText('LOCAL / IN-BROWSER');
await page.locator('#api-preset').selectOption('/r/inscription/:id'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('"protocol": "example"');
await page.locator('#api-preset').selectOption('/content/:id'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('independent body');
await page.locator('#api-path').fill('/r/blockheight'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('"body": 2');
await context.grantPermissions(['clipboard-read','clipboard-write']);
await page.locator('#api-copy').click();
expect(await page.evaluate(()=>navigator.clipboard.readText())).toContain('"body": 2');
await page.locator('#api-path').fill('/missing'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('404');
expect(await page.evaluate(()=>window.unwanted)).toBeUndefined();
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBeTruthy();
});
test('auto miner default, preference reload, countdown, pending confirmation, manual reset and OFF', async ({page}) => {
await page.clock.install();
await page.goto('/'); await page.locator('#create-wallet').click();
await expect(page.locator('#miner-interval')).toHaveValue('0');
await expect(page.locator('#miner-status')).toHaveText('Auto mining OFF');
await page.locator('#miner-interval').selectOption('10');
await expect(page.locator('#miner-status')).toContainText('10s interval');
await page.clock.runFor(10000);
await expect(page.locator('#height')).toHaveText('1');
await page.locator('#payload').fill('auto confirmed'); await page.locator('#review').click(); await page.locator('#sign').click();
await expect(page.locator('#pending')).toHaveText('2');
await page.clock.runFor(10000);
await expect(page.locator('#height')).toHaveText('2'); await expect(page.locator('#pending')).toHaveText('0');
await page.locator('#miner-interval').selectOption('30');
await page.reload(); await expect(page.locator('#miner-interval')).toHaveValue('30');
await page.locator('#miner-interval').selectOption('60');
await page.clock.runFor(5000); await expect(page.locator('#miner-status')).toContainText('55s');
await page.locator('#mine').click(); await expect(page.locator('#height')).toHaveText('3');
await expect(page.locator('#miner-status')).toContainText('60s');
await page.locator('#miner-interval').selectOption('0');
await page.clock.runFor(65000); await expect(page.locator('#height')).toHaveText('3');
});
test('two tabs race one auto deadline under Web Locks', async ({page,context}) => {
const now = new Date('2026-10-05T12:00:00Z');
await page.clock.install({time:now}); await page.goto('/'); await page.locator('#create-wallet').click();
const other=await context.newPage(); await other.clock.install({time:now}); await other.goto('/');
await page.locator('#miner-interval').selectOption('10');
await expect(other.locator('#miner-interval')).toHaveValue('10');
await Promise.all([page.clock.runFor(10000),other.clock.runFor(10000)]);
await expect(page.locator('#height')).toHaveText('1'); await expect(other.locator('#height')).toHaveText('1');
await page.locator('#miner-interval').selectOption('0');
await expect(other.locator('#miner-interval')).toHaveValue('0');
});