2026-10-15 13:32:29 -07:00
import test from 'node:test' ;
import assert from 'node:assert/strict' ;
import { MetadataIndex } from '../src/adapters/metadata-index.js' ;
2026-10-15 14:51:52 -07:00
import { PROTOCOL , census , kidsOf , lineageEvents , reduce , rows , standing , summary } from '../src/orgchain/lineage.js' ;
2026-10-15 13:32:29 -07:00
import { decodeLab , emptyLab , encodeLab , setRule } from '../src/orgchain/store.js' ;
import { accept , act , adopt as adoptBought , deposit , frozenWhy , lineageState , offer , sell , send , take , unlist , withdraw } from '../src/orgchain/actions.js' ;
import { FEE , SAT , balances , ledger , owners , tanks } from '../src/orgchain/money.js' ;
import { holderOf , keyOf } from '../src/orgchain/keys.js' ;
import { DUMMY , askId , askStatus , askWhy , dealsOf , holderOfItem , offerWhy } from '../src/orgchain/offers.js' ;
let n = 0 ;
const ev = ( op , parent = null , meta = {}, height = 1 ) => ({ id : ` ${ String ( ++ n ). padStart ( 64 , '0' ) } i0` , status : height === null ? 'pending' : 'confirmed' ,
height , metadata : { op , ... meta }, parent , protocol : PROTOCOL });
const join = ( node , to , height = 2 ) => ev ( 'join' , node . id , { to : to . id ?? to }, height );
const adopt = ( to , node , height = 2 ) => ev ( 'adopt' , to . id , { of : node . id ?? node }, height );
const why = ( s , e ) => { const entry = s . log . find ( x => x . id === e . id ); return entry . ok ? 'ok' : entry . why ; };
test ( 'an org changes hands by a handshake: it joins a new parent, which adopts it, in either order' , () => {
const acme = ev ( 'found' , null , { name : 'acme' }), ops = ev ( 'birth' , acme . id , { name : 'ops' }), bot = ev ( 'birth' , ops . id , { name : 'bot' , kind : 'agent' });
const kit = ev ( 'birth' , acme . id , { name : 'kit' }), beta = ev ( 'found' , null , { name : 'beta' }), ghost = 'f' . repeat ( 64 ) + 'i0' ;
const tries = [
2026-10-15 14:51:52 -07:00
[ adopt ( beta , bot ), 'an agent changes hands only with the org it sits in' ],
2026-10-15 13:32:29 -07:00
[ join ( kit , bot ), 'agents adopt only once they graduate' ],
[ join ( kit , acme ), "kit is already acme's" ],
[ join ( kit , kit ), 'kit cannot join itself or anything below it' ],
2026-10-15 14:51:52 -07:00
[ join ( acme , ops ), 'acme cannot join itself or anything below it' ],
[ adopt ( beta , ghost ), 'that node is not in this lineage' ],
2026-10-15 13:32:29 -07:00
[ join ( kit , ghost ), 'the new parent must be a node in this lineage' ],
];
const base = [ acme , ops , bot , kit , beta ], j = join ( kit , beta , 3 ), a = adopt ( beta , kit , 3 );
const s = reduce ([... base , ... tries . map (([ e ]) => e ), j , a ]);
tries . forEach (([ e , want ]) => assert . equal ( why ( s , e ), want ));
const k = s . nodes . get ( kit . id ), entry = id => s . log . find ( x => x . id === id );
assert . deepEqual ([ k . parent , k . root , k . edge , k . index , k . joining ], [ beta . id , beta . id , 'derived' , 0 , null ]);
assert . deepEqual ( k . moves , [{ from : acme . id , index : 1 , to : beta . id , beat : 3 , join : j . id , adopt : a . id }]);
assert . deepEqual ([ s . nodes . get ( acme . id ). children . includes ( kit . id ), kidsOf ( s . nodes , s . nodes . get ( acme . id )). map ( x => x . name )], [ true , [ 'ops' ]]);
assert . deepEqual ([ entry ( j . id ). roots , entry ( a . id ). roots ], [[ acme . id , beta . id ], [ beta . id , acme . id ]]);
assert . deepEqual ([ summary ( entry ( j . id ), s ), summary ( entry ( a . id ), s )], [ 'join kit to beta, from acme' , 'adopt kit into beta, from acme' ]);
// A miner may put the two halves in either order: the same tree, and the same record of each, either way.
const t = reduce ([... base , ... tries . map (([ e ]) => e ), a , j ]), view = ( st , id ) => { const x = st . log . find ( e => e . id === id ); return [ x . ok , x . root , x . roots , x . node ]; };
assert . deepEqual ( t . nodes . get ( kit . id ), k );
assert . deepEqual ([ view ( t , j . id ), view ( t , a . id )], [ view ( s , j . id ), view ( s , a . id )]);
// Alone, a half waits for the other.
const half = reduce ([... base , j ]), other = reduce ([... base , a ]);
assert . deepEqual ([ summary ( half . log . at ( - 1 ), half ), summary ( other . log . at ( - 1 ), other )], [ 'join kit to beta, for beta to adopt' , 'adopt kit into beta, once kit joins' ]);
assert . deepEqual ([ half . nodes . get ( kit . id ). parent , other . nodes . get ( kit . id ). parent ], [ acme . id , acme . id ]);
// acme's rows keep kit at its old index, away; beta's draw it at its new one.
assert . deepEqual ( rows ( s , acme . id ). filter ( r => r . type === 'node' ). map ( r => [ r . node . name , r . path , r . away , r . moved ]),
[[ 'acme' , '●' , false , false ], [ 'ops' , "0'" , false , false ], [ 'bot' , "0'" , false , false ], [ 'kit' , "1'" , true , true ]]);
assert . deepEqual ( rows ( s , beta . id ). filter ( r => r . type === 'node' ). map ( r => [ r . node . name , r . path , r . away ]), [[ 'beta' , '●' , false ], [ 'kit' , "0'" , false ]]);
// Its old index is never reused, and its name is free again under acme.
const u = reduce ([... base , j , a , ev ( 'birth' , acme . id , { name : 'kit' }, 4 )]), again = u . log . at ( - 1 );
assert . deepEqual ([ again . ok , u . nodes . get ( again . id ). index ], [ true , 2 ]);
});
2026-10-15 14:51:52 -07:00
test ( 'a joining org cannot graduate; a close leaves an org that moved away alone; a graduate can come back' , () => {
2026-10-15 13:32:29 -07:00
const acme = ev ( 'found' , null , { name : 'acme' }), kit = ev ( 'birth' , acme . id , { name : 'kit' }), beta = ev ( 'found' , null , { name : 'beta' });
const j = join ( kit , beta ), held = ev ( 'graduate' , acme . id , { of : kit . id }, 2 ), a = adopt ( beta , kit , 3 ), shut = ev ( 'close' , acme . id , {}, 4 );
const s = reduce ([ acme , kit , beta , j , held , a , shut ]);
assert . equal ( why ( s , held ), 'kit is joining beta' );
assert . deepEqual ([ s . nodes . get ( kit . id ). status , s . log . at ( - 1 ). also ], [ 'active' , []]);
2026-10-15 14:51:52 -07:00
// Under beta, kit graduates into its own holding, then joins acme again, as acme's next org: its first index stays
// unused. It can graduate again from there, with a fresh root of its own.
const g = ev ( 'graduate' , beta . id , { of : kit . id }, 4 ), t = reduce ([ acme , kit , beta , j , a , g , join ( kit , acme , 5 ), adopt ( acme , kit , 5 )]);
const k = t . nodes . get ( kit . id );
assert . ok ( t . log . every ( e => e . ok ), t . log . filter ( e => ! e . ok ). map ( e => e . why ). join ( '; ' ));
assert . deepEqual ([ k . parent , k . root , k . edge , k . index , k . rooted , t . roots . map ( r => r . name )], [ acme . id , acme . id , 'derived' , 1 , g . id , [ 'acme' , 'beta' ]]);
assert . deepEqual ( rows ( t , acme . id ). filter ( r => r . type === 'node' ). map ( r => [ r . node . name , r . path , r . moved ]), [[ 'acme' , '●' , false ], [ 'kit' , "0'" , true ], [ 'kit' , "1'" , false ]]);
const again = ev ( 'graduate' , acme . id , { of : kit . id }, 6 ), v = reduce ([ acme , kit , beta , j , a , g , join ( kit , acme , 5 ), adopt ( acme , kit , 5 ), again ]);
assert . deepEqual ([ v . log . at ( - 1 ). ok , v . nodes . get ( kit . id ). rooted , v . roots . map ( r => r . name )], [ true , again . id , [ 'acme' , 'beta' , 'kit' ]]);
2026-10-15 13:32:29 -07:00
// A later join replaces a pending one; an adoption waits for its own join.
const gamma = ev ( 'found' , null , { name : 'gamma' });
const u = reduce ([ acme , kit , beta , gamma , join ( kit , beta ), join ( kit , gamma ), adopt ( beta , kit , 3 )]);
assert . deepEqual ([ u . nodes . get ( kit . id ). parent , u . nodes . get ( kit . id ). joining . to , u . nodes . get ( kit . id ). moves ], [ acme . id , gamma . id , []]);
});
2026-10-15 14:51:52 -07:00
test ( 'a holding changes hands whole: it joins another as an org, its tree with it, and its sole proprietorship lapses' , () => {
const acme = ev ( 'found' , null , { name : 'acme' }), ops = ev ( 'birth' , acme . id , { name : 'ops' }), bot = ev ( 'birth' , ops . id , { name : 'bot' , kind : 'agent' });
const lab = ev ( 'birth' , ops . id , { name : 'lab' }), beta = ev ( 'found' , null , { name : 'beta' }), sole = ev ( 'charter' , acme . id , { form : 'sole' });
const base = [ acme , ops , bot , lab , beta , sole ], j = join ( acme , beta , 2 ), a = adopt ( beta , acme , 2 ), s = reduce ([... base , j , a ]);
const node = e => s . nodes . get ( e . id );
assert . ok ( s . log . every ( e => e . ok ), s . log . filter ( e => ! e . ok ). map ( e => e . why ). join ( '; ' ));
assert . deepEqual ([ node ( acme ). kind , node ( acme ). parent , node ( acme ). root , node ( acme ). edge , node ( acme ). index ], [ 'org' , beta . id , beta . id , 'derived' , 0 ]);
assert . deepEqual ([ s . roots . map ( r => r . name ), [ ops , bot , lab ]. map ( e => node ( e ). root )], [[ 'beta' ], [ beta . id , beta . id , beta . id ]]);
assert . deepEqual ( rows ( s , beta . id ). filter ( r => r . type === 'node' ). map ( r => [ r . node . name , r . path ]),
[[ 'beta' , '●' ], [ 'acme' , "0'" ], [ 'ops' , "0'" ], [ 'bot' , "0'" ], [ 'lab' , "0'" ]]);
assert . deepEqual ( census ( s , beta . id ), { orgs : 3 , agents : 1 , belts : 0 , decisions : 0 , closed : 0 });
assert . deepEqual ( standing ( s , node ( acme )), { ok : false , why : 'acme joined another holding, and a sole proprietor is a person' });
assert . deepEqual ([ summary ( s . log . at ( - 2 ), s ), summary ( s . log . at ( - 1 ), s )], [ 'join acme to beta, a holding till now' , 'adopt acme into beta, a holding till now' ]);
// Its name is free for a new holding; an agent never moves alone; the old order of events lands the same tree.
const t = reduce ([... base , a , j , ev ( 'found' , null , { name : 'acme' }, 3 ), join ( bot , beta , 3 )]);
assert . deepEqual ([ t . log . at ( - 2 ). ok , t . log . at ( - 1 ). why ], [ true , 'an agent changes hands only with the org it sits in' ]);
assert . deepEqual ( t . nodes . get ( acme . id ), node ( acme ));
});
2026-10-15 13:32:29 -07:00
// The lab: asks and offers on items and orgs (offers.js), and the flows around them (actions.js). Under the 'none' rule no
// holding mines, so every balance moves only by what the test does.
const TOPUP = 10_000 + FEE ; // what one gas top-up takes from a node's money
function money ( lab ) {
const events = lineageEvents ( new MetadataIndex ( lab . chain )), live = reduce ( events , { pending : true });
const owner = owners ( lab . keys , events , live ), name = id => live . nodes . get ( id ) ? . name ?? null ;
return { now : Object . fromEntries ([... balances ( lab . chain , owner )]. map (([ id , v ]) => [ name ( id ), v ])),
log : ledger ( lab . chain , owner , tanks ( lab . keys , events , live ), dealsOf ( lab )). map ( m => [ m . kind , m . beat , name ( m . from ), name ( m . to ), m . amount ]) };
}
const holder = ( lab , id ) => { const events = lineageEvents ( new MetadataIndex ( lab . chain )); return holderOfItem ( lab , events , lineageState ( lab ), id ) ? . node . name ?? null ; };
function twoHoldings () {
const lab = emptyLab ();
setRule ( lab , 'none' );
const acme = act ( lab , 'found' , null , { name : 'acme' }), beta = act ( lab , 'found' , null , { name : 'beta' });
deposit ( lab , acme , 10 * SAT );
deposit ( lab , beta , 10 * SAT );
return { lab , acme , beta };
}
test ( 'an ask is a sell order signed SINGLE|ANYONECANPAY: any node takes it, leading with a dummy of its own' , () => {
const { lab , acme , beta } = twoHoldings (), kit = act ( lab , 'make' , acme , { name : 'kit' });
const before = money ( lab ). now , height = lab . chain . blocks . length ;
const a = sell ( lab , kit , 2 * SAT );
assert . equal ( lab . chain . blocks . length , height ); // an item's ask is a signature, not a transaction
assert . match ( a . sig , /^[0-9a-f]{128}83$/ );
assert . equal ( askStatus ( lab , lineageState ( lab ), a ), 'open' );
assert . throws (() => sell ( lab , kit , SAT ), /^Error: kit is already listed at 2 tBTC\. Withdraw that ask first\.$/ );
assert . throws (() => sell ( lab , kit , 100 ), /^Error: Ask at least 330 sats\.$/ );
assert . throws (() => take ( lab , askId ( a ), acme ), /^Error: acme is the seller\.$/ );
take ( lab , askId ( a ), beta );
assert . equal ( lab . chain . blocks . length , height + 1 );
assert . equal ( holder ( lab , kit ), 'beta' );
const after = money ( lab ), { tx } = lab . chain . transactions . get ( a . taken . txid );
assert . deepEqual ( after . log . at ( - 1 ), [ 'deal' , height + 1 , 'beta' , 'acme' , 2 * SAT ]);
assert . deepEqual ([ after . now . acme - before . acme , after . now . beta - before . beta ], [ 2 * SAT , - 2 * SAT - 2 * FEE - DUMMY ]);
assert . deepEqual ( tx . vout . map ( v => Number ( v . value )), [ DUMMY + 1000 , 2 * SAT ]); // the dummy pads the item's coin now
assert . deepEqual ( tx . vin . map ( i => i . witness . length ), [ 2 , 1 , 2 ]);
assert . equal ( tx . vin [ 1 ]. witness [ 0 ], a . sig );
assert . equal ( askStatus ( lab , lineageState ( lab ), a ), 'done' );
assert . throws (() => take ( lab , askId ( a ), beta ), /^Error: That ask was already taken\.$/ );
assert . throws (() => unlist ( lab , askId ( a )), /on the chain now/ );
// beta lists it again, then takes acme's lower offer instead: the coin moved, so the ask can never land.
const resale = sell ( lab , kit , SAT ), bid = offer ( lab , kit , acme , SAT / 2 );
accept ( lab , bid . txid );
assert . equal ( askWhy ( lab , lineageState ( lab ), resale ), 'kit has moved since' );
assert . throws (() => take ( lab , askId ( resale ), acme ), /^Error: This ask is stale: kit has moved since\.$/ );
unlist ( lab , askId ( resale ));
assert . deepEqual ( decodeLab ( encodeLab ( lab )). asks , lab . asks );
const bad = JSON . parse ( encodeLab ( lab ));
bad . asks [ 0 ]. sig = ` ${ bad . asks [ 0 ]. sig . slice ( 0 , - 2 ) } 01` ;
assert . throws (() => decodeLab ( JSON . stringify ( bad )), /Invalid ask/ );
});
2026-10-15 14:51:52 -07:00
test ( 'an org sells cash-free on an ask: its money goes home, its item goes with it, it is frozen while listed, and its buyer adopts it at once' , () => {
2026-10-15 13:32:29 -07:00
const { lab , acme , beta } = twoHoldings (), ops = act ( lab , 'birth' , acme , { name : 'ops' });
send ( lab , acme , ops , SAT );
const kit = act ( lab , 'make' , ops , { name : 'kit' }), old = keyOf ( lab . keys , lineageState ( lab ), lineageState ( lab ). nodes . get ( ops ));
const a = sell ( lab , ops , 3 * SAT ), listed = lab . chain . blocks . length ;
2026-10-15 14:51:52 -07:00
// Its money and its gas tank went to acme's SegWit address. Its item goes with it: the two inscriptions are one coin at
// its key now, the one its ask signs, and the layout keeps who holds the item.
2026-10-15 13:32:29 -07:00
const m1 = money ( lab );
assert . equal ( m1 . now . ops ?? 0 , 0 );
assert . deepEqual ( m1 . log . filter ( x => x [ 1 ] === listed ), [[ 'pay' , listed , 'ops' , 'acme' , 7500 ], [ 'pay' , listed , 'ops' , 'acme' , SAT - TOPUP - FEE ]]);
2026-10-15 14:51:52 -07:00
assert . deepEqual ([ holder ( lab , kit ), lab . chain . utxos . get ( askId ( a )). inscriptions . map ( i => i . id ), a . layout ], [ 'ops' , [ ops , kit ], [[ kit , ops ]]]);
2026-10-15 13:32:29 -07:00
assert . throws (() => act ( lab , 'decide' , ops , { title : 'Hold the line' }), /^Error: ops is listed for sale at 3 tBTC\. Withdraw its ask first\.$/ );
assert . throws (() => send ( lab , acme , ops , SAT ), /^Error: ops is listed for sale/ );
assert . throws (() => act ( lab , 'graduate' , acme , { of : ops }), /^Error: ops is listed for sale/ );
assert . throws (() => take ( lab , askId ( a ), act ( lab , 'birth' , beta , { name : 'bot' , kind : 'agent' })), /^Error: Agents adopt only once they graduate\.$/ );
// beta takes it: the deal, the handshake and the handoff to its new keys, in one beat.
take ( lab , askId ( a ), beta );
const s = lineageState ( lab ), node = s . nodes . get ( ops ), k = keyOf ( lab . keys , s , node );
assert . equal ( lab . chain . blocks . length , listed + 2 ); // the agent's birth took one beat
assert . deepEqual ([ node . parent , node . root , node . index , node . moves . map ( x => x . from )], [ beta , beta , 0 , [ acme ]]);
assert . deepEqual ( s . log . slice ( - 2 ). map ( e => [ e . op , e . ok , summary ( e , s )]), [[ 'adopt' , true , 'adopt ops into beta, from acme' ], [ 'join' , true , 'join ops to beta, from acme' ]]);
assert . deepEqual ([ k . from , holderOf ( lab , s , ops ). who ], [ "beta root › 0'" , 'own' ]);
assert . notEqual ( k . wallet . address , old . wallet . address );
2026-10-15 14:51:52 -07:00
// The coin it came in split back: its inscription and its item, each in a coin of its own at its new key.
const coin = id => lab . chain . coins ( k . wallet . script ). find ( c => c . inscriptions . some ( i => i . id === id ));
assert . deepEqual ([ holder ( lab , kit ), coin ( ops ). inscriptions . length , coin ( kit ). inscriptions . length ], [ 'ops' , 1 , 1 ]);
2026-10-15 13:32:29 -07:00
assert . deepEqual ( money ( lab ). log . filter ( x => x [ 0 ] === 'deal' ), [[ 'deal' , listed + 2 , 'beta' , 'acme' , 3 * SAT ]]);
// It acts again as beta's, on beta's money.
assert . throws (() => act ( lab , 'decide' , ops , { title : 'New owners' }), /^Error: ops has no test BTC for gas\. beta can send some\.$/ );
send ( lab , beta , ops , SAT );
act ( lab , 'decide' , ops , { title : 'New owners' });
assert . equal ( lineageState ( lab ). nodes . get ( ops ). decisions [ 0 ]. title , 'New owners' );
});
test ( 'an offer on an org: its parent accepts cash-free, and the buyer adopts it when it is ready' , () => {
const { lab , acme , beta } = twoHoldings (), ops = act ( lab , 'birth' , acme , { name : 'ops' });
send ( lab , acme , ops , SAT );
const o = offer ( lab , ops , beta , 2 * SAT );
assert . throws (() => offer ( lab , ops , acme , SAT ), /^Error: Ops is already acme's\.$/ );
act ( lab , 'decide' , ops , { title : 'Hold the line' }); // every act spends the org's inscription, so the offer goes stale
assert . equal ( offerWhy ( lab , lineageState ( lab ), o ), "ops's inscription has moved since" );
withdraw ( lab , o . txid );
accept ( lab , offer ( lab , ops , beta , 2 * SAT ). txid );
assert . equal ( money ( lab ). now . ops ?? 0 , 0 );
assert . equal ( lineageState ( lab ). nodes . get ( ops ). parent , acme );
assert . throws (() => act ( lab , 'decide' , ops , { title : 'x' }), /^Error: ops was sold to beta, which adopts it next\.$/ );
assert . throws (() => send ( lab , acme , ops , SAT ), /^Error: ops was sold to beta/ );
assert . throws (() => act ( lab , 'graduate' , acme , { of : ops }), /^Error: ops was sold to beta/ );
assert . throws (() => sell ( lab , ops , SAT ), /^Error: Ops is held by beta\.$/ );
assert . throws (() => adoptBought ( lab , ops , acme ), /^Error: acme does not hold ops\.$/ );
adoptBought ( lab , ops , beta );
const s = lineageState ( lab );
assert . deepEqual ([ s . nodes . get ( ops ). parent , holderOf ( lab , s , ops ). who , frozenWhy ( lab , s , 'decide' , ops )], [ beta , 'own' , '' ]);
assert . deepEqual ( money ( lab ). log . filter ( x => x [ 0 ] === 'deal' ). map ( x => x . slice ( 2 )), [[ 'beta' , 'acme' , 2 * SAT ]]);
});