Build browser-native Super Testnet v0 inscription loop
This commit is contained in:
1 parent
d3272032c8
commit
4fa4130150
25 files changed
+2393
-2
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Tx, Script } 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 { 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(71)]) assert.throws(()=>validateText(bad));
|
||||
assert.doesNotThrow(()=>validateText('🌱'.repeat(70)));
|
||||
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('collectibles are protected 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)];
|
||||
assert.throws(()=>chain.submitPackage([Tx.encode(spend).hex]),/protects inscription/);
|
||||
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/);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('create → mine → review/sign → owned inscription → block/transaction → reload/load', async ({page}) => {
|
||||
const errors=[], remote=[];
|
||||
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 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 expect(page.locator('#balance')).toHaveText('50.00000000');
|
||||
const payload='<script>window.unwanted=true</script> Hello 🌱';
|
||||
await page.getByLabel('What will you put on your chain?').fill(payload);
|
||||
await page.getByRole('button',{name:'Review inscription'}).click();
|
||||
await expect(page.locator('#review-text')).toHaveText(payload);
|
||||
await expect(page.locator('#pending')).toHaveText('0');
|
||||
await page.getByRole('button',{name:'Sign & inscribe'}).click();
|
||||
await expect(page.locator('#pending')).toHaveText('2');
|
||||
await expect(page.locator('.collectible pre')).toHaveText(payload);
|
||||
await expect(page.locator('.collectible .tag')).toContainText('PENDING');
|
||||
await expect(page.locator('#balance')).toHaveText('49.99999000');
|
||||
await page.reload();
|
||||
await expect(page.locator('.collectible pre')).toHaveText(payload);
|
||||
await expect(page.locator('#pending')).toHaveText('2');
|
||||
await page.getByRole('button',{name:'Mine a block'}).click();
|
||||
await expect(page.locator('.collectible .tag')).toHaveText('CONFIRMED · BLOCK 2');
|
||||
await page.getByRole('link',{name:'View reveal transaction'}).click();
|
||||
await expect(page.locator('#inspector')).toContainText('1 confirmation(s)');
|
||||
await expect(page.locator('#inspector')).toContainText('Raw Bitcoin transaction');
|
||||
await page.getByRole('link',{name:'Inspect commit transaction'}).click();
|
||||
await expect(page.locator('#inspector')).toContainText('4,999,998,000 sats');
|
||||
await page.getByRole('link',{name:'View block 2'}).click();
|
||||
await expect(page.locator('#inspector h2')).toHaveText('Block 2');
|
||||
await expect(page.locator('#inspector a')).toHaveCount(3);
|
||||
await page.getByRole('link',{name:/^Inscription /}).click();
|
||||
await expect(page.locator('#inspector')).toContainText('Inscription detail');
|
||||
const [download] = await Promise.all([page.waitForEvent('download'),page.getByRole('button',{name:'Save complete lab'}).click()]);
|
||||
const backupPath=await download.path();
|
||||
page.on('dialog',dialog=>dialog.accept());
|
||||
await page.getByRole('button',{name:'Reset local lab'}).click();
|
||||
await expect(page.getByRole('button',{name:'Create test wallet'})).toBeVisible();
|
||||
await page.locator('#load-wallet').setInputFiles(backupPath);
|
||||
await expect(page.locator('.collectible pre')).toHaveText(payload);
|
||||
await expect(page.locator('#height')).toHaveText('2');
|
||||
await page.reload();
|
||||
await expect(page.locator('#balance')).toHaveText('99.99999000');
|
||||
expect(await page.evaluate(()=>window.unwanted)).toBeUndefined();
|
||||
expect(errors).toEqual([]); expect(remote).toEqual([]);
|
||||
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBeTruthy();
|
||||
await page.screenshot({path:`test-results/${test.info().project.name}-complete.png`,fullPage:true});
|
||||
});
|
||||
|
||||
test('wallet backup reload and rejected oversized or invalid input',async({page})=>{
|
||||
await page.goto('/'); await page.getByRole('button',{name:'Create test wallet'}).click();
|
||||
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.locator('#payload').fill('🌱'.repeat(71));
|
||||
await page.getByRole('button',{name:'Review inscription'}).click();
|
||||
await expect(page.getByRole('status')).toContainText('at most 280');
|
||||
await expect(page.locator('#pending')).toHaveText('0');
|
||||
await page.locator('#load-wallet').setInputFiles({name:'bad.json',mimeType:'application/json',buffer:Buffer.from('{"network":"mainnet"}')});
|
||||
await expect(page.getByRole('status')).toContainText('Only Super Testnet');
|
||||
await expect(page.locator('#address')).toHaveValue(address);
|
||||
page.on('dialog',d=>d.accept());
|
||||
await page.locator('#load-wallet').setInputFiles(await download.path());
|
||||
await expect(page.getByRole('status')).toContainText('Test wallet loaded.');
|
||||
await expect(page.locator('#address')).toHaveValue(address);
|
||||
await expect(page.locator('#balance')).toHaveText('50.00000000');
|
||||
});
|
||||
|
||||
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 expect(page.locator('#height')).toHaveText('2');
|
||||
await expect(other.locator('#height')).toHaveText('2');
|
||||
await expect(page.locator('#balance')).toHaveText('100.00000000');
|
||||
});
|
||||
|
||||
test('storage failure leaves the displayed and persisted chain unchanged', async ({page}) => {
|
||||
await page.goto('/');
|
||||
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 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 expect(page.locator('#balance')).toHaveText('50.00000000');
|
||||
});
|
||||
Reference in new issue
Block a user