478 lines
29 KiB
JavaScript
478 lines
29 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Tx, Script, Tap } from '@cmdcode/tapscript';
|
|
import { TestWallet } from '../src/adapters/wallet.js';
|
|
import { LocalChain, LocalRPC } from '../src/adapters/chain.js';
|
|
import { InscriptionAdapter, validateText, MAX_BYTES, MIME_TYPES, envelope, serializeParent, POSTAGE, FEE } from '../src/adapters/inscription.js';
|
|
import { getVin, getVout } from '../src/upstream/primitives.js';
|
|
import { encodeLab, decodeLab } from '../src/storage.js';
|
|
|
|
function setup() {
|
|
const wallet = TestWallet.create(), chain = new LocalChain();
|
|
chain.mine(wallet.script);
|
|
return { wallet, chain, ins: new InscriptionAdapter(chain, wallet) };
|
|
}
|
|
test('signed commit/reveal, owned collectible, confirmation, raw tx/block inspection and restoration', () => {
|
|
const { wallet, chain, ins } = setup();
|
|
assert.match(wallet.address, /^bcrt1p/);
|
|
assert.equal(chain.balance(wallet.script), 5e9);
|
|
const id = ins.inscribe('Hello from v0 🌱');
|
|
assert.equal(chain.mempool.length, 2);
|
|
assert.equal(chain.balance(wallet.script), 5e9 - 1000);
|
|
assert.equal(ins.list()[0].height, null);
|
|
assert.equal(ins.list()[0].id, id);
|
|
assert.deepEqual(new InscriptionAdapter(chain, TestWallet.create()).list(), []);
|
|
const pendingRestored = decodeLab(encodeLab({wallet,chain}));
|
|
assert.equal(new InscriptionAdapter(pendingRestored.chain,pendingRestored.wallet).list()[0].id, id);
|
|
const block = chain.mine(wallet.script);
|
|
assert.equal(chain.mempool.length, 0);
|
|
assert.equal(ins.list()[0].height, 2);
|
|
assert.equal(chain.balance(wallet.script), 1e10 - 1000);
|
|
const rpc = new LocalRPC(chain);
|
|
const revealId = id.slice(0,-2);
|
|
assert.equal(Tx.util.getTxid(rpc.call('getrawtransaction',[revealId])), revealId);
|
|
assert.ok(rpc.call('getblock',[block.hash]).tx.includes(revealId));
|
|
assert.equal(rpc.call('getblockhash',[2]),block.hash);
|
|
assert.equal(rpc.call('getblockcount'),2);
|
|
assert.throws(()=>rpc.call('sendrawtransaction'),/Unsupported/);
|
|
const restored = decodeLab(encodeLab({wallet,chain}));
|
|
assert.equal(restored.wallet.address,wallet.address);
|
|
assert.deepEqual(new InscriptionAdapter(restored.chain,restored.wallet).list(),ins.list());
|
|
});
|
|
test('empty wallet, UTF-8 limit, and untagged/mainnet wallet imports rejected', () => {
|
|
assert.throws(()=>new InscriptionAdapter(new LocalChain(),TestWallet.create()).inscribe('hi'), /Mine a block/);
|
|
for (const bad of ['', ' ', '🌱'.repeat(MAX_BYTES / 4 + 1)]) assert.throws(()=>validateText(bad));
|
|
assert.doesNotThrow(()=>validateText('🌱'.repeat(MAX_BYTES / 4)));
|
|
assert.throws(()=>TestWallet.load({secret:'1'.repeat(64)}), /Only Super Testnet/);
|
|
const backup = TestWallet.create().backup(); backup.network='mainnet';
|
|
assert.throws(()=>TestWallet.load(backup), /Only Super Testnet/);
|
|
});
|
|
test('invalid reveal signature rolls back the entire package, and double-spends fail', () => {
|
|
const {wallet,chain,ins} = setup();
|
|
const before = chain.snapshot(); ins.inscribe('atomic');
|
|
const [commit,reveal] = chain.mempool;
|
|
const bad = Tx.decode(reveal); bad.vin[0].witness[0]='00'.repeat(64);
|
|
const clean = new LocalChain(before);
|
|
assert.throws(()=>clean.submitPackage([commit,Tx.encode(bad).hex]),/signature|Signature/);
|
|
assert.deepEqual(clean.snapshot(),before);
|
|
assert.equal(clean.balance(wallet.script),5e9);
|
|
clean.submitPackage([commit,reveal]);
|
|
assert.throws(()=>clean.submitPackage([commit]),/Duplicate/);
|
|
const spend = Tx.decode(commit); spend.vout[1].value = Number(spend.vout[1].value)-1;
|
|
assert.throws(()=>clean.submitPackage([Tx.encode(spend).hex]),/spent/);
|
|
});
|
|
test('tampering with reveal payload, control block or value is rejected', () => {
|
|
const {chain,ins} = setup(); const before=chain.snapshot(); ins.inscribe('original');
|
|
const [commit,reveal]=chain.mempool;
|
|
for (const mutate of [
|
|
tx=>{ const script=Script.decode(tx.vin[0].witness[1]); script[8]='6861636b6564'; tx.vin[0].witness[1]=Script.encode(script,false).hex; },
|
|
tx=>{ tx.vin[0].witness[2]='c0'+'00'.repeat(32); },
|
|
tx=>{ tx.vout[0].value=5000; },
|
|
]) {
|
|
const tx=Tx.decode(reveal); mutate(tx); const candidate=new LocalChain(before);
|
|
assert.throws(()=>candidate.submitPackage([commit,Tx.encode(tx).hex]));
|
|
assert.deepEqual(candidate.snapshot(),before);
|
|
}
|
|
});
|
|
test('ordinary signed spends preserve inscriptions and subsequent inscriptions use change', () => {
|
|
const {wallet,chain,ins}=setup(); const first=ins.inscribe('first'); ins.inscribe('second');
|
|
assert.equal(ins.list().length,2);
|
|
const record=chain.transactions.get(first.slice(0,-2));
|
|
const spend=Tx.create({vin:[{txid:record.txid,vout:0,sequence:0xffffffff,prevout:record.tx.vout[0]}],vout:[{value:500,scriptPubKey:wallet.script}]});
|
|
spend.vin[0].witness=[wallet.sign(spend)];
|
|
const sat = record.inscription.sat;
|
|
chain.submitPackage([Tx.encode(spend).hex]);
|
|
assert.equal(ins.list().find(i=>i.id===first).sat,sat);
|
|
assert.equal(chain.satLocation(sat).txid,Tx.util.getTxid(spend));
|
|
chain.mine(wallet.script); assert.equal(ins.list().length,2);
|
|
});
|
|
test('corrupted persistence is rejected rather than silently reset', () => {
|
|
assert.throws(()=>decodeLab('{bad'));
|
|
const {chain}=setup(); const snapshot=chain.snapshot(); snapshot.blocks[0].hash='0'.repeat(64);
|
|
assert.throws(()=>new LocalChain(snapshot),/block hash/);
|
|
assert.throws(()=>new LocalChain({...snapshot,network:'mainnet'}),/network/);
|
|
});
|
|
|
|
test('all MIME bodies round-trip arbitrary bytes, chunk boundaries, cap, and snapshots', () => {
|
|
const { wallet, chain, ins } = setup();
|
|
for (const contentType of MIME_TYPES) {
|
|
const bytes = Uint8Array.from({ length: MAX_BYTES }, (_, i) => i % 256);
|
|
const id = ins.inscribe({ contentType, bytes });
|
|
const item = ins.list().find(i => i.id === id);
|
|
assert.deepEqual(item.bytes, Array.from(bytes)); assert.equal(item.contentType, contentType);
|
|
}
|
|
const before = chain.snapshot();
|
|
assert.throws(() => ins.inscribe({ contentType: 'image/png', bytes: new Uint8Array(MAX_BYTES + 1) }), /at most/);
|
|
assert.deepEqual(chain.snapshot(), before);
|
|
const restored = decodeLab(encodeLab({ wallet, chain }));
|
|
assert.deepEqual(new InscriptionAdapter(restored.chain, restored.wallet).list(), ins.list());
|
|
});
|
|
test('tag 3 encoding reverses txid and uses minimal little-endian index', () => {
|
|
const txid = '0123456789abcdef'.repeat(4);
|
|
const reversed = txid.match(/../g).reverse().join('');
|
|
for (const [index, suffix] of [[0,''],[1,'01'],[256,'0001'],[65536,'000001'],[4294967295,'ffffffff']])
|
|
assert.equal(serializeParent(`${txid}i${index}`), reversed + suffix);
|
|
assert.throws(() => serializeParent(`${txid}i4294967296`));
|
|
});
|
|
test('parent stays owned across children, nested lineage, mining and replay', () => {
|
|
const { wallet, chain, ins } = setup();
|
|
const parent = ins.inscribe('parent'), child = ins.inscribe('child', parent);
|
|
const grandchild = ins.inscribe('grandchild', child);
|
|
ins.inscribe('sibling', parent); // Spend its relocated output, not its original reveal.
|
|
assert.equal(ins.list().length, 4);
|
|
assert.equal(ins.list().find(i => i.id === parent).parent, null);
|
|
assert.equal(ins.list().find(i => i.id === child).parent, parent);
|
|
assert.equal(ins.list().find(i => i.id === grandchild).parent, child);
|
|
const reveal = chain.transactions.get(child.slice(0,-2)).tx;
|
|
assert.equal(reveal.vin.length, 2); assert.equal(Number(reveal.vout[1].value), POSTAGE);
|
|
assert.throws(() => new InscriptionAdapter(chain, TestWallet.create()).prepare('fraud', parent), /owned/);
|
|
chain.mine(wallet.script);
|
|
const restored = decodeLab(encodeLab({ wallet, chain }));
|
|
assert.deepEqual(new InscriptionAdapter(restored.chain, restored.wallet).list(), ins.list());
|
|
assert.equal(ins.list().find(i => i.id === parent).height, 2);
|
|
});
|
|
// Build genuinely signed malformed packages so rejection tests exercise provenance, not signature failure.
|
|
function customReveal(chain, wallet, tag, spentParent, redirect = false) {
|
|
const coin = chain.coins(wallet.script).find(c => !c.inscription);
|
|
const script = envelope(wallet.pubkey, 'claim', tag), leaf = Tap.encodeScript(script);
|
|
const [tapkey, control] = Tap.getPubKey(wallet.pubkey, { target: leaf });
|
|
const commit = Tx.create({ vin: [getVin(coin.txid, coin.vout, coin.value, coin.scriptPubKey)],
|
|
vout: [getVout(POSTAGE + FEE, ['OP_1', tapkey]), getVout(coin.value - POSTAGE - 2 * FEE, wallet.script)] });
|
|
commit.vin[0].witness = [wallet.sign(commit)];
|
|
const reveal = Tx.create({ vin: [getVin(Tx.util.getTxid(commit), 0, POSTAGE + FEE, ['OP_1', tapkey]),
|
|
...(spentParent ? [getVin(spentParent.txid, spentParent.vout, spentParent.value, spentParent.scriptPubKey)] : [])],
|
|
vout: [getVout(POSTAGE, wallet.script), ...(spentParent ? [getVout(spentParent.value, redirect ? TestWallet.create().script : wallet.script)] : [])] });
|
|
reveal.vin[0].witness = [wallet.sign(reveal, leaf), Script.encode(script, false).hex, control];
|
|
if (spentParent) reveal.vin[1].witness = [wallet.sign(reveal, undefined, 1)];
|
|
return [Tx.encode(commit).hex, Tx.encode(reveal).hex];
|
|
}
|
|
test('false parent claims, untagged parent spends, wrong parent and redirected returns are rejected atomically', () => {
|
|
const { wallet, chain, ins } = setup(); const parent = ins.inscribe('parent');
|
|
const other = ins.inscribe('other');
|
|
const coin = chain.coins(wallet.script).find(c => c.inscription?.id === parent);
|
|
for (const [tag, spent, redirect, reason] of [
|
|
[parent, null, false, /actually be spent/],
|
|
[null, coin, false, /correct tag/],
|
|
[other, coin, false, /actually be spent/],
|
|
[parent, coin, true, /preserved/],
|
|
]) {
|
|
const before = chain.snapshot();
|
|
assert.throws(() => chain.submitPackage(customReveal(chain, wallet, tag, spent, redirect)), reason);
|
|
assert.deepEqual(chain.snapshot(), before);
|
|
assert.equal(ins.list().find(i => i.id === parent).parent, null);
|
|
}
|
|
});
|
|
|
|
test('single bytes and one-byte final chunks preserve exact binary values', () => {
|
|
const { ins } = setup();
|
|
for (const byte of [0, 1, 3, 16, 127, 128, 129, 255]) {
|
|
for (const size of [1, 521]) {
|
|
const bytes = new Uint8Array(size).fill(byte);
|
|
const id = ins.inscribe({ contentType: 'image/png', bytes });
|
|
assert.deepEqual(ins.list().find(i => i.id === id).bytes, Array.from(bytes));
|
|
}
|
|
}
|
|
});
|
|
|
|
test('legacy text envelopes still decode without changing their bytes', async () => {
|
|
const { readEnvelope, MIME, textHex } = await import('../src/adapters/inscription.js');
|
|
const wallet = TestWallet.create(), text = 'original v0 🌱';
|
|
const script = Script.encode([wallet.pubkey, 'OP_CHECKSIG', 'OP_0', 'OP_IF', textHex('ord'), '01', textHex(MIME), 'OP_0', textHex(text), 'OP_ENDIF'], false).hex;
|
|
const parsed = readEnvelope(['00'.repeat(64), script, 'unused']);
|
|
assert.equal(parsed.text, text); assert.equal(parsed.parent, null);
|
|
assert.deepEqual(parsed.bytes, new TextEncoder().encode(text));
|
|
});
|
|
|
|
test('child reveal requires a valid parent-input signature', () => {
|
|
const { chain, wallet, ins } = setup();
|
|
const parent = ins.inscribe('parent'), before = chain.snapshot();
|
|
const coin = chain.coins(wallet.script).find(c => c.inscription?.id === parent);
|
|
const [commit, raw] = customReveal(chain, wallet, parent, coin);
|
|
const reveal = Tx.decode(raw); reveal.vin[1].witness[0] = '00'.repeat(64);
|
|
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 witness = record.tx.vin.find(i => i.witness.length === 3).witness;
|
|
const parts = Script.decode(witness[1]);
|
|
const chunks=[]; let i=11;
|
|
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(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);
|
|
});
|
|
|
|
import { encodePointer, decodePointer } from '../src/adapters/inscription.js';
|
|
import { resolveContent } from '../src/adapters/local-ord-api.js';
|
|
import { recursivePath } from '../src/recursion.js';
|
|
|
|
test('delegate tag 11 uses parent serialization and accepts future ids before signing', () => {
|
|
const {ins,chain,wallet} = setup();
|
|
const delegate = '0123456789abcdef'.repeat(4) + 'i256';
|
|
const id = ins.inscribe('fallback', null, {protocol:'delegate'}, {delegate});
|
|
const witness = chain.transactions.get(id.slice(0,-2)).tx.vin[0].witness;
|
|
assert.ok(witness[1].includes('010b22' + serializeParent(delegate)));
|
|
assert.equal(readEnvelope(witness).delegate, delegate);
|
|
assert.equal(new LocalOrdAPI(chain).request(`/content/${id}`).status,404);
|
|
wallet.sign = () => { throw new Error('must not sign'); };
|
|
assert.throws(() => ins.inscribe('bad',null,undefined,{delegate:'invalid'}), /Invalid parent/);
|
|
});
|
|
|
|
test('delegates resolve A to B to C content/type while identity and metadata stay A', () => {
|
|
const {ins,chain} = setup();
|
|
const c = ins.inscribe({contentType:'text/html',bytes:new TextEncoder().encode('<p>terminal</p>')});
|
|
const b = ins.inscribe('b',null,undefined,{delegate:c});
|
|
const a = ins.inscribe('a',null,{identity:'a'},{delegate:b});
|
|
const api = new LocalOrdAPI(chain), response = api.request(`/content/${a}`);
|
|
assert.equal(response.inscriptionId,a); assert.equal(response.contentId,c);
|
|
assert.equal(response.contentType,'text/html'); assert.equal(new TextDecoder().decode(response.body),'<p>terminal</p>');
|
|
assert.deepEqual(api.request(`/r/inscription/${a}`).body.metadata,{identity:'a'});
|
|
assert.deepEqual(api.request(`/r/inscription/${b}`).body.delegatedBy,[a]);
|
|
});
|
|
|
|
test('forward delegate becomes available and loops have deterministic 508 responses', () => {
|
|
// Resolution graph fixture: a future transaction id cannot be precomputed from
|
|
// a mutually hash-dependent cycle of actual Bitcoin transactions.
|
|
const index = { events: [{id:'a',delegate:'b'}] };
|
|
const read = () => ({bytes:Uint8Array.of(42),contentType:'image/png'});
|
|
assert.equal(resolveContent(index,'a',read).status,404);
|
|
index.events.push({id:'b',delegate:null});
|
|
assert.deepEqual(resolveContent(index,'a',read).body,Uint8Array.of(42));
|
|
index.events[1].delegate='a';
|
|
assert.deepEqual(resolveContent(index,'a',read),{status:508,contentType:'application/json',body:{error:'Delegate loop detected.'}});
|
|
index.events[0].delegate='a'; assert.equal(resolveContent(index,'a',read).status,508);
|
|
});
|
|
|
|
test('FIFO sats, parent preserving spends, same-sat reinscription stack and raw rebuild', () => {
|
|
const {ins,chain,wallet} = setup();
|
|
const a = ins.inscribe('original',null,{protocol:'stack'});
|
|
const sat = ins.list().find(i=>i.id===a).sat;
|
|
for (let n=0;n<3;n++) ins.inscribe('child '+n,a);
|
|
assert.equal(ins.list().find(i=>i.id===a).sat,sat);
|
|
assert.equal(chain.satLocation(sat).offset,0);
|
|
const b = ins.inscribe('second',null,{protocol:'stack'},{reinscribe:a});
|
|
const c = ins.inscribe('third',null,undefined,{reinscribe:b});
|
|
ins.inscribe('stack child',a);
|
|
const api = new LocalOrdAPI(chain), stack = api.request(`/local/sat/${sat}`).body;
|
|
assert.deepEqual(stack.inscriptions,[a,b,c]);
|
|
for (const [n,id] of [a,b,c].entries()) {
|
|
const event=api.request(`/r/inscription/${id}`).body;
|
|
assert.equal(event.sat,sat); assert.equal(event.reinscriptionPosition,n);
|
|
assert.deepEqual(event.location,stack.location);
|
|
}
|
|
assert.equal(new TextDecoder().decode(api.request(`/content/${a}`).body),'original');
|
|
assert.deepEqual(api.request(`/local/utxo/${stack.location.txid}:${stack.location.vout}`).body.inscriptions,[a,b,c]);
|
|
chain.mine(wallet.script);
|
|
const restored = new LocalChain(chain.snapshot());
|
|
assert.deepEqual(new MetadataIndex(restored).events,new MetadataIndex(chain).events);
|
|
assert.equal(new InscriptionAdapter(restored,wallet).list().find(i=>i.id===b).sat,sat);
|
|
});
|
|
|
|
for (const [pointer, encoded] of [[0,''],[255,'ff'],[256,'0001'],[99999,'9f8601']]) {
|
|
test(`pointer ${pointer}: minimal LE encoding and output position/fallback`, () => {
|
|
assert.equal(encodePointer(pointer),encoded); assert.equal(decodePointer(encoded),String(pointer));
|
|
const {ins,chain} = setup(); const id=ins.inscribe('point',null,undefined,{pointer});
|
|
const record=chain.transactions.get(id.slice(0,-2));
|
|
assert.equal(readEnvelope(record.tx.vin[0].witness).pointer,String(pointer));
|
|
const event=new MetadataIndex(chain).events[0];
|
|
assert.equal(event.location.offset,pointer < POSTAGE ? pointer : 0);
|
|
assert.equal(event.sat,pointer < POSTAGE ? pointer : 0);
|
|
});
|
|
}
|
|
|
|
test('recursive path whitelist rejects all URL, traversal and mutation forms', () => {
|
|
const id='a'.repeat(64)+'i0';
|
|
for(const path of [`/content/${id}`,`/r/inscription/${id}`,`/r/metadata/${id}`,`/r/parents/${id}`,`/r/children/${id}`,`/r/tx/${'a'.repeat(64)}`,'/r/blockheight','/r/blockhash/2','/r/blockinfo/2',`/r/blockinfo/${'a'.repeat(64)}`]) assert.equal(recursivePath(path),true,path);
|
|
for(const path of ['https://example.com','//example.com','/\\example.com','/r/../local/protocols','/r/blockheight?x=1','/r/blockheight#x','/local/metadata-index','/content/%2f','/r/blockheight/','/r/blockheight\n']) assert.equal(recursivePath(path),false,path);
|
|
});
|
|
|
|
test('a real future delegate resolves after its independently funded reveal arrives', () => {
|
|
const {chain,ins} = setup(), futureWallet=TestWallet.create();
|
|
chain.mine(futureWallet.script);
|
|
const future = new LocalChain(chain.snapshot());
|
|
const target = new InscriptionAdapter(future,futureWallet).inscribe('arrived later');
|
|
const id = ins.inscribe('waiting',null,undefined,{delegate:target});
|
|
const api = new LocalOrdAPI(chain);
|
|
assert.equal(api.request(`/content/${id}`).status,404);
|
|
chain.submitPackage(future.mempool);
|
|
assert.equal(new TextDecoder().decode(api.request(`/content/${id}`).body),'arrived later');
|
|
});
|
|
|
|
test('multiple input ranges flow FIFO across outputs and protect inscribed fee tails atomically', () => {
|
|
const {chain,wallet,ins}=setup();
|
|
const first=ins.inscribe('offset',null,undefined,{pointer:700});
|
|
const coin=chain.coins(wallet.script).find(c=>c.inscription?.id===first);
|
|
const funding=chain.coins(wallet.script).find(c=>!c.inscription);
|
|
const spend=Tx.create({vin:[getVin(coin.txid,coin.vout,coin.value,coin.scriptPubKey),getVin(funding.txid,funding.vout,funding.value,funding.scriptPubKey)],vout:[getVout(500,wallet.script),getVout(funding.value,wallet.script)]});
|
|
spend.vin.forEach((input,i)=>{input.witness=[wallet.sign(spend,undefined,i)];});
|
|
chain.submitPackage([Tx.encode(spend).hex]);
|
|
const sat=chain.transactions.get(first.slice(0,-2)).inscription.sat;
|
|
assert.deepEqual(chain.satLocation(sat),{txid:Tx.util.getTxid(spend),vout:1,offset:200});
|
|
const moved=chain.coins(wallet.script).find(c=>c.inscription?.id===first);
|
|
assert.deepEqual(moved.ranges[0],[500,1000]);
|
|
assert.equal(moved.ranges[1][0],1500);
|
|
chain.mine(wallet.script);
|
|
const before=chain.snapshot();
|
|
// Put a late inscribed sat in the fee tail on a separate small output.
|
|
const late=ins.inscribe('late',null,undefined,{pointer:999});
|
|
const tail=chain.coins(wallet.script).find(c=>c.inscription?.id===late);
|
|
const burn=Tx.create({vin:[getVin(tail.txid,tail.vout,tail.value,tail.scriptPubKey)],vout:[getVout(500,wallet.script)]});
|
|
burn.vin[0].witness=[wallet.sign(burn)];
|
|
const after=chain.snapshot();
|
|
assert.throws(()=>chain.submitPackage([Tx.encode(burn).hex]),/fee loss/);
|
|
assert.deepEqual(chain.snapshot(),after); assert.ok(after.mempool.length>before.mempool.length);
|
|
});
|
|
|
|
test("native metaprotocol tag 7 round-trips and overrides metadata.protocol for tracking", () => {
|
|
const {ins,chain} = setup();
|
|
const id = ins.inscribe("hello", null, {protocol:"metadata-fallback", note:"kept"}, {metaprotocol:"tea-v1"});
|
|
const record = chain.transactions.get(id.slice(0,-2));
|
|
const witness = record.tx.vin.find(i => i.witness?.length === 3).witness;
|
|
const item = readEnvelope(witness);
|
|
assert.equal(item.metaprotocol, "tea-v1");
|
|
assert.deepEqual(item.metadata, {protocol:"metadata-fallback", note:"kept"});
|
|
const event = new MetadataIndex(chain).events.find(e => e.id === id);
|
|
assert.equal(event.metaprotocol, "tea-v1");
|
|
assert.equal(event.protocol, "tea-v1");
|
|
assert.deepEqual(new MetadataIndex(chain).forProtocol("tea-v1").map(e => e.id), [id]);
|
|
});
|
|
|
|
test("metadata.protocol remains a fallback when native metaprotocol is absent", () => {
|
|
const {ins,chain} = setup();
|
|
const id = ins.inscribe("legacy", null, {protocol:"legacy-protocol"});
|
|
const event = new MetadataIndex(chain).events.find(e => e.id === id);
|
|
assert.equal(event.metaprotocol, null);
|
|
assert.equal(event.protocol, "legacy-protocol");
|
|
});
|