Extend local inscription lab with MIME bodies and parent provenance
This commit is contained in:
1 parent
83a19b51ff
commit
acd2652046
12 files changed
+383
-95
No files matched your search
+105
-4
@@ -1,9 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Tx, Script } from '@cmdcode/tapscript';
|
||||
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 } from '../src/adapters/inscription.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() {
|
||||
@@ -40,8 +41,8 @@ test('signed commit/reveal, owned collectible, confirmation, raw tx/block inspec
|
||||
});
|
||||
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(71)]) assert.throws(()=>validateText(bad));
|
||||
assert.doesNotThrow(()=>validateText('🌱'.repeat(70)));
|
||||
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/);
|
||||
@@ -88,3 +89,103 @@ test('corrupted persistence is rejected rather than silently reset', () => {
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user