@@ -205,11 +205,55 @@ function saveTrail(trail) {
205205 writeFileSync ( TRAIL_FILE , JSON . stringify ( trail , null , 2 ) + '\n' ) ;
206206}
207207function loadPrivateState ( ) {
208- if ( ! existsSync ( PRIVATE_FILE ) ) return null ;
209- return JSON . parse ( readFileSync ( PRIVATE_FILE , 'utf8' ) ) ;
208+ // Try git config first, fall back to legacy file
209+ try {
210+ const txoUri = gitExec ( 'git config --local gitmark.txo' ) ;
211+ const parsed = parseTxoUri ( txoUri ) ;
212+ return { txid : parsed . txid , vout : parsed . vout , amount : parsed . amount } ;
213+ } catch {
214+ if ( ! existsSync ( PRIVATE_FILE ) ) return null ;
215+ return JSON . parse ( readFileSync ( PRIVATE_FILE , 'utf8' ) ) ;
216+ }
217+ }
218+ function savePrivateState ( state , chain , head ) {
219+ const txoUri = `txo:${ chain } :${ state . txid } :${ state . vout } ?amount=${ state . amount } ${ head ? '&commit=' + head : '' } ` ;
220+ gitExec ( `git config --local gitmark.txo ${ txoUri } ` ) ;
221+ }
222+ function isDirty ( ) {
223+ try { return gitExec ( 'git config --local gitmark.dirty' ) !== 'false' ; } catch { return true ; }
224+ }
225+ function addGitNote ( commitHash , note ) {
226+ try { gitExec ( `git notes add -f -m ${ note } ${ commitHash } ` ) ; } catch { /* ignore if no commits */ }
227+ }
228+ function loadTrailFromNotes ( ) {
229+ const trail = loadTrail ( ) ;
230+ if ( ! trail ) return null ;
231+ try {
232+ const notesList = gitExec ( 'git notes list' ) ;
233+ if ( ! notesList ) return trail ;
234+ const notedCommits = new Set ( notesList . split ( '\n' ) . filter ( Boolean ) . map ( l => l . split ( ' ' ) [ 1 ] ) ) ;
235+ const allCommits = gitExec ( 'git log --reverse --format=%H' ) . split ( '\n' ) . filter ( Boolean ) ;
236+ const states = [ ] ;
237+ const txos = [ ] ;
238+ for ( const commit of allCommits ) {
239+ if ( ! notedCommits . has ( commit ) ) continue ;
240+ try {
241+ const note = gitExec ( `git notes show ${ commit } ` ) ;
242+ if ( note . startsWith ( 'txo:' ) ) {
243+ states . push ( commit ) ;
244+ txos . push ( note ) ;
245+ }
246+ } catch { continue ; }
247+ }
248+ trail . states = states ;
249+ trail . txo = txos ;
250+ return trail ;
251+ } catch {
252+ return trail ;
253+ }
210254}
211- function savePrivateState ( state ) {
212- writeFileSync ( PRIVATE_FILE , JSON . stringify ( state , null , 2 ) + '\n' ) ;
255+ function loadFullTrail ( ) {
256+ return isDirty ( ) ? loadTrail ( ) : loadTrailFromNotes ( ) ;
213257}
214258
215259// --- Parse TXO URI ---
@@ -289,7 +333,7 @@ async function cmdInit(args) {
289333 ) ;
290334 const newTxid = await broadcastTx ( rawTx , explorer ) ;
291335
292- savePrivateState ( { txid : newTxid , vout : 0 , amount : outputAmount } ) ;
336+ savePrivateState ( { txid : newTxid , vout : 0 , amount : outputAmount } , chain ) ;
293337 console . log ( `Funded: ${ outputAmount } sats (txid: ${ newTxid } )` ) ;
294338 }
295339
@@ -300,14 +344,14 @@ async function cmdInit(args) {
300344 console . log ( `Base public key: ${ pubkey } ` ) ;
301345 console . log ( `Chain: ${ chain } ` ) ;
302346 console . log ( `Address: ${ pubkeyToAddress ( pubkey , [ ] , chain ) } ` ) ;
303- if ( ! existsSync ( PRIVATE_FILE ) && voucherIdx === - 1 ) {
347+ if ( ! loadPrivateState ( ) && voucherIdx === - 1 ) {
304348 console . log ( `\nUnfunded. Use: git mark init --voucher txo:${ chain } :txid:vout?amount=X&key=Y` ) ;
305349 console . log ( `Or send sats to: ${ pubkeyToAddress ( pubkey , [ ] , chain ) } ` ) ;
306350 }
307351}
308352
309353async function cmdMark ( args ) {
310- const trail = loadTrail ( ) ;
354+ const trail = loadFullTrail ( ) ;
311355 if ( ! trail ) { console . error ( `No ${ TRAIL_FILE } found. Run: git mark init` ) ; process . exit ( 1 ) ; }
312356 const priv = loadPrivateState ( ) ;
313357 if ( ! priv ) { console . error ( 'No funding. Run: git mark init --voucher txo:...' ) ; process . exit ( 1 ) ; }
@@ -353,13 +397,18 @@ async function cmdMark(args) {
353397 ) ;
354398 const newTxid = await broadcastTx ( rawTx , explorer ) ;
355399
356- // Update trail
357- trail . states . push ( head ) ;
358- trail . txo . push ( `txo:${ chain } :${ newTxid } :0?commit=${ head } ` ) ;
359- saveTrail ( trail ) ;
400+ const txoUri = `txo:${ chain } :${ newTxid } :0?amount=${ outputAmount } &commit=${ head } ` ;
401+
402+ // Always: git notes + git config
403+ addGitNote ( head , txoUri ) ;
404+ savePrivateState ( { txid : newTxid , vout : 0 , amount : outputAmount } , chain , head ) ;
360405
361- // Update private state
362- savePrivateState ( { txid : newTxid , vout : 0 , amount : outputAmount } ) ;
406+ // Update trail file if dirty mode
407+ trail . states . push ( head ) ;
408+ trail . txo . push ( txoUri ) ;
409+ if ( isDirty ( ) ) {
410+ saveTrail ( trail ) ;
411+ }
363412
364413 const address = pubkeyToAddress ( trail . publicKeyBase , allStates , chain ) ;
365414 console . log ( `Marked: ${ head . slice ( 0 , 8 ) } → ${ newTxid . slice ( 0 , 16 ) } ...` ) ;
@@ -369,7 +418,7 @@ async function cmdMark(args) {
369418}
370419
371420async function cmdInfo ( ) {
372- const trail = loadTrail ( ) ;
421+ const trail = loadFullTrail ( ) ;
373422 if ( ! trail ) { console . error ( `No ${ TRAIL_FILE } found.` ) ; process . exit ( 1 ) ; }
374423 const priv = loadPrivateState ( ) ;
375424
@@ -393,7 +442,7 @@ async function cmdInfo() {
393442}
394443
395444async function cmdVerify ( ) {
396- const trail = loadTrail ( ) ;
445+ const trail = loadFullTrail ( ) ;
397446 if ( ! trail ) { console . error ( `No ${ TRAIL_FILE } found.` ) ; process . exit ( 1 ) ; }
398447 if ( trail . states . length === 0 ) { console . log ( 'No marks to verify.' ) ; return ; }
399448
@@ -437,7 +486,7 @@ async function cmdVerify() {
437486export {
438487 taggedHash , btScalar , deriveChainedPrivkey , deriveChainedPubkey ,
439488 pubkeyToAddress , parseTxoUri , p2trScript , buildTransaction ,
440- TRAIL_FILE , PRIVATE_FILE , CHAINS
489+ TRAIL_FILE , PRIVATE_FILE , CHAINS , isDirty , loadTrailFromNotes , loadFullTrail
441490} ;
442491
443492// --- CLI ---
0 commit comments