gitmark 0.0.71 → 0.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +17 -2
- package/README.md +16 -7
- package/bin/git-mark.js +98 -28
- package/index.html +10 -1
- package/og-image.png +0 -0
- package/og-image.svg +8 -0
- package/package.json +1 -1
- package/test/git-mark.test.js +56 -4
|
@@ -7,7 +7,22 @@
|
|
|
7
7
|
"Bash(npm install)",
|
|
8
8
|
"Bash(npm search txo_parser)",
|
|
9
9
|
"Bash(../gitmark/bin/git-mark)",
|
|
10
|
-
"Bash(../gitmark/bin/git-mark --genesis abc123:0)"
|
|
10
|
+
"Bash(../gitmark/bin/git-mark --genesis abc123:0)",
|
|
11
|
+
"Bash(git config:*)",
|
|
12
|
+
"Bash(git add:*)",
|
|
13
|
+
"Bash(git commit -m ':*)",
|
|
14
|
+
"Bash(git push:*)",
|
|
15
|
+
"Bash(gh pr create --title 'fix: use --local flag in getPrivkey for per-repo keys' --body ':*)",
|
|
16
|
+
"Bash(gh pr:*)",
|
|
17
|
+
"Bash(git checkout:*)",
|
|
18
|
+
"Bash(git pull:*)",
|
|
19
|
+
"Bash(git mark:*)",
|
|
20
|
+
"Bash(gh issue:*)",
|
|
21
|
+
"Bash(node bin/git-mark.js --version)",
|
|
22
|
+
"Bash(node bin/git-mark.js -v)",
|
|
23
|
+
"Bash(npm test:*)",
|
|
24
|
+
"Bash(gh api:*)",
|
|
25
|
+
"Bash(convert:*)"
|
|
11
26
|
],
|
|
12
27
|
"deny": [],
|
|
13
28
|
"ask": [],
|
|
@@ -16,4 +31,4 @@
|
|
|
16
31
|
"/home/melvin/remote/github.com/solidpayorg/solidpayorg"
|
|
17
32
|
]
|
|
18
33
|
}
|
|
19
|
-
}
|
|
34
|
+
}
|
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ The chain of addresses on Bitcoin mirrors the chain of commits in git. Anyone ca
|
|
|
49
49
|
| `git mark` | Anchor HEAD commit to Bitcoin |
|
|
50
50
|
| `git mark info` | Show trail state, balance, addresses |
|
|
51
51
|
| `git mark verify` | Verify all marks against Bitcoin |
|
|
52
|
+
| `git mark update` | Update blocktrails.json from git notes |
|
|
53
|
+
| `git mark --version` | Show version |
|
|
52
54
|
|
|
53
55
|
## Trail File
|
|
54
56
|
|
|
@@ -58,7 +60,7 @@ The chain of addresses on Bitcoin mirrors the chain of commits in git. Anyone ca
|
|
|
58
60
|
{
|
|
59
61
|
"version": "0.0.3",
|
|
60
62
|
"profile": "gitmark",
|
|
61
|
-
"
|
|
63
|
+
"pubkeyBase": "02abc...",
|
|
62
64
|
"chain": "tbtc4",
|
|
63
65
|
"states": ["a1b2c3", "e5f6a7"],
|
|
64
66
|
"txo": [
|
|
@@ -68,20 +70,27 @@ The chain of addresses on Bitcoin mirrors the chain of commits in git. Anyone ca
|
|
|
68
70
|
}
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
- **
|
|
73
|
+
- **pubkeyBase** — compressed pubkey (02/03 prefix), base for BIP-341 key chaining
|
|
72
74
|
- **states** — commit hashes (input to key derivation)
|
|
73
75
|
- **txo** — Bitcoin anchors (TXO URIs, self-contained and verifiable)
|
|
74
76
|
|
|
75
|
-
Private spending state stored in
|
|
77
|
+
Private spending state stored in `gitmark.txo` git config (never committed).
|
|
76
78
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
Your private key is stored in git config:
|
|
79
|
+
## Configuration
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
|
|
82
|
+
# Private key (auto-generated by init)
|
|
83
|
+
git config --local nostr.privkey <64-char-hex>
|
|
84
|
+
|
|
85
|
+
# Current UTXO (updated on each mark)
|
|
86
|
+
git config --local gitmark.txo txo:tbtc4:txid:vout?amount=X&commit=Y
|
|
87
|
+
|
|
88
|
+
# Dirty flag — set to false to keep working tree clean after marking
|
|
89
|
+
git config --local gitmark.dirty false
|
|
83
90
|
```
|
|
84
91
|
|
|
92
|
+
When `gitmark.dirty=false`, `git mark` only writes git notes + git config. Run `git mark update` to sync blocktrails.json when ready.
|
|
93
|
+
|
|
85
94
|
Same secp256k1 key used for Nostr, Bitcoin, and Solid pod authentication.
|
|
86
95
|
|
|
87
96
|
## Dependencies
|
package/bin/git-mark.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* git mark [--chain tbtc4]
|
|
9
9
|
* git mark info
|
|
10
10
|
* git mark verify
|
|
11
|
+
* git mark update
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { secp256k1, schnorr } from '@noble/curves/secp256k1';
|
|
@@ -15,7 +16,11 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
15
16
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
16
17
|
import { execSync } from 'child_process';
|
|
17
18
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
18
|
-
import { join } from 'path';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
19
24
|
|
|
20
25
|
// --- Constants ---
|
|
21
26
|
const SECP_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
|
|
@@ -183,9 +188,11 @@ async function broadcastTx(rawHex, explorer) {
|
|
|
183
188
|
|
|
184
189
|
// --- Git helpers ---
|
|
185
190
|
function gitExec(cmd) { return execSync(cmd, { encoding: 'utf8' }).trim(); }
|
|
186
|
-
function getHead() {
|
|
191
|
+
function getHead() {
|
|
192
|
+
try { return gitExec('git rev-parse HEAD'); } catch { return null; }
|
|
193
|
+
}
|
|
187
194
|
function getPrivkey() {
|
|
188
|
-
try { return gitExec('git config nostr.privkey'); } catch { return null; }
|
|
195
|
+
try { return gitExec('git config --local nostr.privkey'); } catch { return null; }
|
|
189
196
|
}
|
|
190
197
|
function setPrivkey(key) { gitExec(`git config --local nostr.privkey ${key}`); }
|
|
191
198
|
function isGitRoot() { return existsSync('.git'); }
|
|
@@ -199,11 +206,55 @@ function saveTrail(trail) {
|
|
|
199
206
|
writeFileSync(TRAIL_FILE, JSON.stringify(trail, null, 2) + '\n');
|
|
200
207
|
}
|
|
201
208
|
function loadPrivateState() {
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
// Try git config first, fall back to legacy file
|
|
210
|
+
try {
|
|
211
|
+
const txoUri = gitExec('git config --local gitmark.txo');
|
|
212
|
+
const parsed = parseTxoUri(txoUri);
|
|
213
|
+
return { txid: parsed.txid, vout: parsed.vout, amount: parsed.amount };
|
|
214
|
+
} catch {
|
|
215
|
+
if (!existsSync(PRIVATE_FILE)) return null;
|
|
216
|
+
return JSON.parse(readFileSync(PRIVATE_FILE, 'utf8'));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function savePrivateState(state, chain, head) {
|
|
220
|
+
const txoUri = `txo:${chain}:${state.txid}:${state.vout}?amount=${state.amount}${head ? '&commit=' + head : ''}`;
|
|
221
|
+
gitExec(`git config --local gitmark.txo ${txoUri}`);
|
|
222
|
+
}
|
|
223
|
+
function isDirty() {
|
|
224
|
+
try { return gitExec('git config --local gitmark.dirty') !== 'false'; } catch { return true; }
|
|
204
225
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
226
|
+
function addGitNote(commitHash, note) {
|
|
227
|
+
try { gitExec(`git notes add -f -m ${note} ${commitHash}`); } catch { /* ignore if no commits */ }
|
|
228
|
+
}
|
|
229
|
+
function loadTrailFromNotes() {
|
|
230
|
+
const trail = loadTrail();
|
|
231
|
+
if (!trail) return null;
|
|
232
|
+
try {
|
|
233
|
+
const notesList = gitExec('git notes list');
|
|
234
|
+
if (!notesList) return trail;
|
|
235
|
+
const notedCommits = new Set(notesList.split('\n').filter(Boolean).map(l => l.split(' ')[1]));
|
|
236
|
+
const allCommits = gitExec('git log --reverse --format=%H').split('\n').filter(Boolean);
|
|
237
|
+
const states = [];
|
|
238
|
+
const txos = [];
|
|
239
|
+
for (const commit of allCommits) {
|
|
240
|
+
if (!notedCommits.has(commit)) continue;
|
|
241
|
+
try {
|
|
242
|
+
const note = gitExec(`git notes show ${commit}`);
|
|
243
|
+
if (note.startsWith('txo:')) {
|
|
244
|
+
states.push(commit);
|
|
245
|
+
txos.push(note);
|
|
246
|
+
}
|
|
247
|
+
} catch { continue; }
|
|
248
|
+
}
|
|
249
|
+
trail.states = states;
|
|
250
|
+
trail.txo = txos;
|
|
251
|
+
return trail;
|
|
252
|
+
} catch {
|
|
253
|
+
return trail;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function loadFullTrail() {
|
|
257
|
+
return isDirty() ? loadTrail() : loadTrailFromNotes();
|
|
207
258
|
}
|
|
208
259
|
|
|
209
260
|
// --- Parse TXO URI ---
|
|
@@ -243,9 +294,10 @@ async function cmdInit(args) {
|
|
|
243
294
|
|
|
244
295
|
// Create trail
|
|
245
296
|
const trail = {
|
|
297
|
+
'@type': 'Blocktrail',
|
|
246
298
|
version: '0.0.3',
|
|
247
299
|
profile: 'gitmark',
|
|
248
|
-
|
|
300
|
+
pubkeyBase: pubkey,
|
|
249
301
|
chain,
|
|
250
302
|
states: [],
|
|
251
303
|
txo: []
|
|
@@ -282,7 +334,7 @@ async function cmdInit(args) {
|
|
|
282
334
|
);
|
|
283
335
|
const newTxid = await broadcastTx(rawTx, explorer);
|
|
284
336
|
|
|
285
|
-
savePrivateState({ txid: newTxid, vout: 0, amount: outputAmount });
|
|
337
|
+
savePrivateState({ txid: newTxid, vout: 0, amount: outputAmount }, chain);
|
|
286
338
|
console.log(`Funded: ${outputAmount} sats (txid: ${newTxid})`);
|
|
287
339
|
}
|
|
288
340
|
|
|
@@ -293,14 +345,14 @@ async function cmdInit(args) {
|
|
|
293
345
|
console.log(`Base public key: ${pubkey}`);
|
|
294
346
|
console.log(`Chain: ${chain}`);
|
|
295
347
|
console.log(`Address: ${pubkeyToAddress(pubkey, [], chain)}`);
|
|
296
|
-
if (!
|
|
348
|
+
if (!loadPrivateState() && voucherIdx === -1) {
|
|
297
349
|
console.log(`\nUnfunded. Use: git mark init --voucher txo:${chain}:txid:vout?amount=X&key=Y`);
|
|
298
350
|
console.log(`Or send sats to: ${pubkeyToAddress(pubkey, [], chain)}`);
|
|
299
351
|
}
|
|
300
352
|
}
|
|
301
353
|
|
|
302
354
|
async function cmdMark(args) {
|
|
303
|
-
const trail =
|
|
355
|
+
const trail = loadFullTrail();
|
|
304
356
|
if (!trail) { console.error(`No ${TRAIL_FILE} found. Run: git mark init`); process.exit(1); }
|
|
305
357
|
const priv = loadPrivateState();
|
|
306
358
|
if (!priv) { console.error('No funding. Run: git mark init --voucher txo:...'); process.exit(1); }
|
|
@@ -309,6 +361,7 @@ async function cmdMark(args) {
|
|
|
309
361
|
if (!privkey) { console.error('No private key. Set: git config nostr.privkey <hex>'); process.exit(1); }
|
|
310
362
|
|
|
311
363
|
const head = getHead();
|
|
364
|
+
if (!head) { console.error('No commits yet. Make a commit first.'); process.exit(1); }
|
|
312
365
|
const chain = trail.chain;
|
|
313
366
|
const explorer = CHAINS[chain]?.explorer;
|
|
314
367
|
if (!explorer) { console.error(`Unknown chain: ${chain}`); process.exit(1); }
|
|
@@ -323,7 +376,7 @@ async function cmdMark(args) {
|
|
|
323
376
|
: hexToBytes(privkey);
|
|
324
377
|
|
|
325
378
|
// Derive next address (chained through all states including current)
|
|
326
|
-
const nextPub = deriveChainedPubkey(hexToBytes(trail.
|
|
379
|
+
const nextPub = deriveChainedPubkey(hexToBytes(trail.pubkeyBase), allStates);
|
|
327
380
|
const nextXonly = nextPub.slice(1);
|
|
328
381
|
const nextScript = p2trScript(nextXonly);
|
|
329
382
|
|
|
@@ -345,15 +398,20 @@ async function cmdMark(args) {
|
|
|
345
398
|
);
|
|
346
399
|
const newTxid = await broadcastTx(rawTx, explorer);
|
|
347
400
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
401
|
+
const txoUri = `txo:${chain}:${newTxid}:0?amount=${outputAmount}&commit=${head}`;
|
|
402
|
+
|
|
403
|
+
// Always: git notes + git config
|
|
404
|
+
addGitNote(head, txoUri);
|
|
405
|
+
savePrivateState({ txid: newTxid, vout: 0, amount: outputAmount }, chain, head);
|
|
352
406
|
|
|
353
|
-
// Update
|
|
354
|
-
|
|
407
|
+
// Update trail file if dirty mode
|
|
408
|
+
trail.states.push(head);
|
|
409
|
+
trail.txo.push(txoUri);
|
|
410
|
+
if (isDirty()) {
|
|
411
|
+
saveTrail(trail);
|
|
412
|
+
}
|
|
355
413
|
|
|
356
|
-
const address = pubkeyToAddress(trail.
|
|
414
|
+
const address = pubkeyToAddress(trail.pubkeyBase, allStates, chain);
|
|
357
415
|
console.log(`Marked: ${head.slice(0, 8)} → ${newTxid.slice(0, 16)}...`);
|
|
358
416
|
console.log(`Address: ${address}`);
|
|
359
417
|
console.log(`Balance: ${outputAmount} sats`);
|
|
@@ -361,18 +419,18 @@ async function cmdMark(args) {
|
|
|
361
419
|
}
|
|
362
420
|
|
|
363
421
|
async function cmdInfo() {
|
|
364
|
-
const trail =
|
|
422
|
+
const trail = loadFullTrail();
|
|
365
423
|
if (!trail) { console.error(`No ${TRAIL_FILE} found.`); process.exit(1); }
|
|
366
424
|
const priv = loadPrivateState();
|
|
367
425
|
|
|
368
426
|
console.log(`Profile: ${trail.profile}`);
|
|
369
427
|
console.log(`Version: ${trail.version}`);
|
|
370
428
|
console.log(`Chain: ${trail.chain}`);
|
|
371
|
-
console.log(`Base public key: ${trail.
|
|
372
|
-
console.log(`Base address: ${pubkeyToAddress(trail.
|
|
429
|
+
console.log(`Base public key: ${trail.pubkeyBase}`);
|
|
430
|
+
console.log(`Base address: ${pubkeyToAddress(trail.pubkeyBase, [], trail.chain)}`);
|
|
373
431
|
console.log(`Marks: ${trail.states.length}`);
|
|
374
432
|
if (trail.states.length > 0) {
|
|
375
|
-
const currentAddr = pubkeyToAddress(trail.
|
|
433
|
+
const currentAddr = pubkeyToAddress(trail.pubkeyBase, trail.states, trail.chain);
|
|
376
434
|
console.log(`Current address: ${currentAddr}`);
|
|
377
435
|
console.log(`Last commit: ${trail.states[trail.states.length - 1]}`);
|
|
378
436
|
console.log(`Last TXO: ${trail.txo[trail.txo.length - 1]}`);
|
|
@@ -385,7 +443,7 @@ async function cmdInfo() {
|
|
|
385
443
|
}
|
|
386
444
|
|
|
387
445
|
async function cmdVerify() {
|
|
388
|
-
const trail =
|
|
446
|
+
const trail = loadFullTrail();
|
|
389
447
|
if (!trail) { console.error(`No ${TRAIL_FILE} found.`); process.exit(1); }
|
|
390
448
|
if (trail.states.length === 0) { console.log('No marks to verify.'); return; }
|
|
391
449
|
|
|
@@ -397,7 +455,7 @@ async function cmdVerify() {
|
|
|
397
455
|
|
|
398
456
|
for (let i = 0; i < trail.states.length; i++) {
|
|
399
457
|
const statesUpTo = trail.states.slice(0, i + 1);
|
|
400
|
-
const expectedAddr = pubkeyToAddress(trail.
|
|
458
|
+
const expectedAddr = pubkeyToAddress(trail.pubkeyBase, statesUpTo, trail.chain);
|
|
401
459
|
const txoUri = trail.txo[i];
|
|
402
460
|
const parsed = parseTxoUri(txoUri);
|
|
403
461
|
|
|
@@ -408,7 +466,7 @@ async function cmdVerify() {
|
|
|
408
466
|
const out = txData.vout?.[parsed.vout];
|
|
409
467
|
if (!out) { console.log(` [${i}] FAIL — output ${parsed.vout} not found`); ok = false; continue; }
|
|
410
468
|
if (out.scriptpubkey_address === expectedAddr) {
|
|
411
|
-
console.log(` [${i}] OK — ${trail.states[i].slice(0, 8)} → ${expectedAddr
|
|
469
|
+
console.log(` [${i}] OK — ${trail.states[i].slice(0, 8)} → ${expectedAddr}`);
|
|
412
470
|
} else {
|
|
413
471
|
console.log(` [${i}] FAIL — address mismatch`);
|
|
414
472
|
console.log(` expected: ${expectedAddr}`);
|
|
@@ -425,11 +483,18 @@ async function cmdVerify() {
|
|
|
425
483
|
process.exit(ok ? 0 : 1);
|
|
426
484
|
}
|
|
427
485
|
|
|
486
|
+
function cmdUpdate() {
|
|
487
|
+
const trail = loadTrailFromNotes();
|
|
488
|
+
if (!trail) { console.error(`No ${TRAIL_FILE} found. Run: git mark init`); process.exit(1); }
|
|
489
|
+
saveTrail(trail);
|
|
490
|
+
console.log(`Updated ${TRAIL_FILE} from git notes (${trail.states.length} marks)`);
|
|
491
|
+
}
|
|
492
|
+
|
|
428
493
|
// --- Exports for testing ---
|
|
429
494
|
export {
|
|
430
495
|
taggedHash, btScalar, deriveChainedPrivkey, deriveChainedPubkey,
|
|
431
496
|
pubkeyToAddress, parseTxoUri, p2trScript, buildTransaction,
|
|
432
|
-
TRAIL_FILE, PRIVATE_FILE, CHAINS
|
|
497
|
+
TRAIL_FILE, PRIVATE_FILE, CHAINS, isDirty, loadTrailFromNotes, loadFullTrail
|
|
433
498
|
};
|
|
434
499
|
|
|
435
500
|
// --- CLI ---
|
|
@@ -438,12 +503,16 @@ if (isMain) {
|
|
|
438
503
|
const args = process.argv.slice(2);
|
|
439
504
|
const cmd = args[0];
|
|
440
505
|
|
|
441
|
-
if (cmd === '
|
|
506
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
507
|
+
console.log(PKG.version);
|
|
508
|
+
} else if (cmd === 'init') {
|
|
442
509
|
cmdInit(args.slice(1));
|
|
443
510
|
} else if (cmd === 'info') {
|
|
444
511
|
cmdInfo();
|
|
445
512
|
} else if (cmd === 'verify') {
|
|
446
513
|
cmdVerify();
|
|
514
|
+
} else if (cmd === 'update') {
|
|
515
|
+
cmdUpdate();
|
|
447
516
|
} else if (cmd === 'mark' || !cmd || (cmd && !cmd.startsWith('-'))) {
|
|
448
517
|
if (!existsSync(TRAIL_FILE) && cmd !== 'mark') {
|
|
449
518
|
console.log('Usage:');
|
|
@@ -451,6 +520,7 @@ if (isMain) {
|
|
|
451
520
|
console.log(' git mark # anchor HEAD to Bitcoin');
|
|
452
521
|
console.log(' git mark info # show trail state');
|
|
453
522
|
console.log(' git mark verify # verify trail against Bitcoin');
|
|
523
|
+
console.log(' git mark update # update blocktrails.json from git notes');
|
|
454
524
|
} else {
|
|
455
525
|
cmdMark(args.slice(cmd === 'mark' ? 1 : 0));
|
|
456
526
|
}
|
package/index.html
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
<meta property="og:description" content="Mark your git commits on Bitcoin using blocktrails key chaining. Tamper-proof history, globally verifiable.">
|
|
10
10
|
<meta property="og:type" content="website">
|
|
11
11
|
<meta property="og:url" content="https://git-mark.com">
|
|
12
|
+
<meta property="og:image" content="https://git-mark.com/og-image.png">
|
|
13
|
+
<meta property="og:image:width" content="1200">
|
|
14
|
+
<meta property="og:image:height" content="630">
|
|
15
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
16
|
+
<meta name="twitter:title" content="git mark — Anchor Git Commits to Bitcoin">
|
|
17
|
+
<meta name="twitter:description" content="Mark your git commits on Bitcoin using blocktrails key chaining. Tamper-proof history, globally verifiable.">
|
|
18
|
+
<meta name="twitter:image" content="https://git-mark.com/og-image.png">
|
|
12
19
|
<style>
|
|
13
20
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
21
|
body { font-family: Georgia, 'Times New Roman', serif; background: #fafaf8; color: #2c2c2c; line-height: 1.7; padding: 2rem; }
|
|
@@ -89,7 +96,7 @@ git mark info</pre>
|
|
|
89
96
|
<pre>{
|
|
90
97
|
"version": "0.0.3",
|
|
91
98
|
"profile": "gitmark",
|
|
92
|
-
"
|
|
99
|
+
"pubkeyBase": "02abc...",
|
|
93
100
|
"chain": "tbtc4",
|
|
94
101
|
"states": ["a1b2c3...", "e5f6a7..."],
|
|
95
102
|
"txo": [
|
|
@@ -122,6 +129,8 @@ git mark info</pre>
|
|
|
122
129
|
<tr><td><code>git mark</code></td><td>Anchor HEAD commit to Bitcoin</td></tr>
|
|
123
130
|
<tr><td><code>git mark info</code></td><td>Show trail state, balance, addresses</td></tr>
|
|
124
131
|
<tr><td><code>git mark verify</code></td><td>Verify all marks against Bitcoin</td></tr>
|
|
132
|
+
<tr><td><code>git mark update</code></td><td>Update blocktrails.json from git notes</td></tr>
|
|
133
|
+
<tr><td><code>git mark --version</code></td><td>Show version</td></tr>
|
|
125
134
|
</table>
|
|
126
135
|
|
|
127
136
|
<h2>Related</h2>
|
package/og-image.png
ADDED
|
Binary file
|
package/og-image.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
2
|
+
<rect width="1200" height="630" fill="#1a1a2e"/>
|
|
3
|
+
<rect x="0" y="0" width="1200" height="4" fill="#f7931a"/>
|
|
4
|
+
<text x="600" y="240" text-anchor="middle" font-family="Georgia, serif" font-size="96" font-weight="bold" fill="#f7931a">git mark</text>
|
|
5
|
+
<text x="600" y="320" text-anchor="middle" font-family="Georgia, serif" font-size="36" fill="#cccccc">Anchor Git Commits to Bitcoin</text>
|
|
6
|
+
<text x="600" y="400" text-anchor="middle" font-family="'SFMono-Regular', Consolas, monospace" font-size="22" fill="#888888">Tamper-proof history via BIP-341 key chaining</text>
|
|
7
|
+
<text x="600" y="540" text-anchor="middle" font-family="Georgia, serif" font-size="24" fill="#666666">git-mark.com</text>
|
|
8
|
+
</svg>
|
package/package.json
CHANGED
package/test/git-mark.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
taggedHash, btScalar, deriveChainedPrivkey, deriveChainedPubkey,
|
|
8
|
-
pubkeyToAddress, parseTxoUri, p2trScript, CHAINS
|
|
8
|
+
pubkeyToAddress, parseTxoUri, p2trScript, CHAINS, isDirty, loadFullTrail, loadTrailFromNotes
|
|
9
9
|
} from '../bin/git-mark.js';
|
|
10
10
|
|
|
11
11
|
describe('Key chaining', () => {
|
|
@@ -167,7 +167,7 @@ describe('Trail format', () => {
|
|
|
167
167
|
const trail = {
|
|
168
168
|
version: '0.0.3',
|
|
169
169
|
profile: 'gitmark',
|
|
170
|
-
|
|
170
|
+
pubkeyBase: pubkey,
|
|
171
171
|
chain: 'tbtc4',
|
|
172
172
|
states: [],
|
|
173
173
|
txo: []
|
|
@@ -175,8 +175,8 @@ describe('Trail format', () => {
|
|
|
175
175
|
|
|
176
176
|
assert.strictEqual(trail.version, '0.0.3');
|
|
177
177
|
assert.strictEqual(trail.profile, 'gitmark');
|
|
178
|
-
assert.strictEqual(trail.
|
|
179
|
-
assert.ok(trail.
|
|
178
|
+
assert.strictEqual(trail.pubkeyBase.length, 66); // compressed hex
|
|
179
|
+
assert.ok(trail.pubkeyBase.startsWith('02') || trail.pubkeyBase.startsWith('03'));
|
|
180
180
|
assert.ok(Array.isArray(trail.states));
|
|
181
181
|
assert.ok(Array.isArray(trail.txo));
|
|
182
182
|
});
|
|
@@ -188,4 +188,56 @@ describe('Trail format', () => {
|
|
|
188
188
|
};
|
|
189
189
|
assert.strictEqual(trail.states.length, trail.txo.length);
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
it('txo URIs include amount and commit params', () => {
|
|
193
|
+
const txoUri = 'txo:tbtc4:abc123:0?amount=9700&commit=deadbeef';
|
|
194
|
+
const parsed = parseTxoUri(txoUri);
|
|
195
|
+
assert.strictEqual(parsed.chain, 'tbtc4');
|
|
196
|
+
assert.strictEqual(parsed.txid, 'abc123');
|
|
197
|
+
assert.strictEqual(parsed.amount, 9700);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Dirty flag', () => {
|
|
202
|
+
it('isDirty returns true by default (no config set)', () => {
|
|
203
|
+
assert.strictEqual(isDirty(), true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('loadFullTrail returns null when no trail file exists', () => {
|
|
207
|
+
const trail = loadFullTrail();
|
|
208
|
+
assert.strictEqual(trail, null);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('loadTrailFromNotes returns null when no trail file exists', () => {
|
|
212
|
+
const trail = loadTrailFromNotes();
|
|
213
|
+
assert.strictEqual(trail, null);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('loadFullTrail uses loadTrail when dirty is true', () => {
|
|
217
|
+
// Default is dirty=true, so loadFullTrail should behave like loadTrail
|
|
218
|
+
const full = loadFullTrail();
|
|
219
|
+
assert.strictEqual(full, null); // no blocktrails.json in test dir
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('TXO URI in git config format', () => {
|
|
224
|
+
it('txo URI with amount and commit is parseable', () => {
|
|
225
|
+
const uri = 'txo:tbtc4:abc123:0?amount=9700&commit=deadbeef';
|
|
226
|
+
const parsed = parseTxoUri(uri);
|
|
227
|
+
assert.strictEqual(parsed.txid, 'abc123');
|
|
228
|
+
assert.strictEqual(parsed.vout, 0);
|
|
229
|
+
assert.strictEqual(parsed.amount, 9700);
|
|
230
|
+
assert.strictEqual(parsed.chain, 'tbtc4');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('txo URI roundtrips through format used by savePrivateState', () => {
|
|
234
|
+
const state = { txid: 'abcdef1234', vout: 0, amount: 14668 };
|
|
235
|
+
const chain = 'tbtc4';
|
|
236
|
+
const head = 'deadbeef';
|
|
237
|
+
const uri = `txo:${chain}:${state.txid}:${state.vout}?amount=${state.amount}&commit=${head}`;
|
|
238
|
+
const parsed = parseTxoUri(uri);
|
|
239
|
+
assert.strictEqual(parsed.txid, state.txid);
|
|
240
|
+
assert.strictEqual(parsed.vout, state.vout);
|
|
241
|
+
assert.strictEqual(parsed.amount, state.amount);
|
|
242
|
+
});
|
|
191
243
|
});
|