keychat-save 1.1.0 → 1.2.1
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/index.mjs +67 -3
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -21,10 +21,12 @@ import { join } from 'node:path'
|
|
|
21
21
|
|
|
22
22
|
const CONFIG_DIR = join(homedir(), '.keychat')
|
|
23
23
|
const WIF_PATH = join(CONFIG_DIR, 'identity.wif')
|
|
24
|
+
const INDEX_PATH = join(CONFIG_DIR, 'saves-index.md')
|
|
24
25
|
const BRIDGE = 'https://www.keychat.pro/api/bridge'
|
|
25
26
|
const SAVE_PREFIX = '4b534156' // "KSAV"
|
|
26
27
|
const VERSION = '01'
|
|
27
28
|
const SUB_UNIT_SATS = 125
|
|
29
|
+
const INDEX_MARKER = '\n\n## __SAVES_INDEX__\n'
|
|
28
30
|
|
|
29
31
|
// ── crypto ───────────────────────────────────────────────────────
|
|
30
32
|
function bytesToHex (bytes) {
|
|
@@ -42,6 +44,20 @@ function decrypt (encrypted, privKey) {
|
|
|
42
44
|
return Utils.toUTF8(EncryptedMessage.decrypt(encrypted, privKey))
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// ── local index ──────────────────────────────────────────────────
|
|
48
|
+
// Newest-first markdown table: `| date | label | txid | cost |`.
|
|
49
|
+
// Returns the first txid-shaped cell; null if none.
|
|
50
|
+
function readNewestTxidFromIndex (path) {
|
|
51
|
+
const text = readFileSync(path, 'utf8')
|
|
52
|
+
for (const line of text.split('\n')) {
|
|
53
|
+
if (!line.startsWith('|')) continue
|
|
54
|
+
const cells = line.split('|').map(c => c.trim())
|
|
55
|
+
const txid = cells.find(c => /^[0-9a-f]{64}$/i.test(c))
|
|
56
|
+
if (txid) return txid.toLowerCase()
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
// ── bridge API ───────────────────────────────────────────────────
|
|
46
62
|
async function getUnspent (address) {
|
|
47
63
|
const res = await fetch(`${BRIDGE}/api/address/${address}/unspent`)
|
|
@@ -147,7 +163,13 @@ async function cmdSave (label, body) {
|
|
|
147
163
|
const address = key.toPublicKey().toAddress()
|
|
148
164
|
const pubHex = key.toPublicKey().toString()
|
|
149
165
|
|
|
150
|
-
|
|
166
|
+
// Bundle the local saves index into every save body so any single save
|
|
167
|
+
// bootstraps the full chain on a fresh device.
|
|
168
|
+
let index = ''
|
|
169
|
+
try { index = readFileSync(INDEX_PATH, 'utf8') } catch {}
|
|
170
|
+
const fullBody = body + INDEX_MARKER + index
|
|
171
|
+
|
|
172
|
+
const envelope = JSON.stringify({ kv: 1, kind: 'save', label, body: fullBody })
|
|
151
173
|
const encrypted = encrypt(envelope, key, pubHex)
|
|
152
174
|
const cipherHex = bytesToHex(encrypted)
|
|
153
175
|
const timestamp = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0')
|
|
@@ -187,6 +209,21 @@ async function cmdSave (label, body) {
|
|
|
187
209
|
await tx.sign()
|
|
188
210
|
|
|
189
211
|
const txid = await broadcast(tx.toHex())
|
|
212
|
+
|
|
213
|
+
// Prepend new entry to local saves index
|
|
214
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
215
|
+
const computeCost = (totalCost / 500).toString()
|
|
216
|
+
const newRow = `| ${date} | ${label} | ${txid} | ${computeCost}C |\n`
|
|
217
|
+
let header = '# KeyChat saves index\n\n| Date | Label | TxID | Cost |\n|------|-------|------|------|\n'
|
|
218
|
+
let existing = ''
|
|
219
|
+
try {
|
|
220
|
+
const cur = readFileSync(INDEX_PATH, 'utf8')
|
|
221
|
+
// Strip header if present, keep existing rows
|
|
222
|
+
const tableStart = cur.indexOf('|------|-------|------|------|')
|
|
223
|
+
existing = tableStart >= 0 ? cur.slice(tableStart + '|------|-------|------|------|'.length).trim() + '\n' : cur
|
|
224
|
+
} catch {}
|
|
225
|
+
writeFileSync(INDEX_PATH, header + newRow + existing, { mode: 0o600 })
|
|
226
|
+
|
|
190
227
|
console.log(`Saved to chain!`)
|
|
191
228
|
console.log(` Label: ${label}`)
|
|
192
229
|
console.log(` Size: ${tx.toHex().length / 2} bytes`)
|
|
@@ -215,8 +252,25 @@ async function cmdLoadByTxid (txid) {
|
|
|
215
252
|
const env = JSON.parse(decrypted)
|
|
216
253
|
if (env.kind === 'save') { label = env.label || 'Untitled'; body = env.body || '' }
|
|
217
254
|
} catch {}
|
|
255
|
+
|
|
256
|
+
// Extract embedded saves index. Only write to local file if local is
|
|
257
|
+
// missing (fresh device bootstrap). Existing local index is left alone
|
|
258
|
+
// so loading an older save can't wipe out newer entries.
|
|
259
|
+
const idx = body.indexOf(INDEX_MARKER)
|
|
260
|
+
let userBody = body
|
|
261
|
+
if (idx >= 0) {
|
|
262
|
+
userBody = body.slice(0, idx)
|
|
263
|
+
const embeddedIndex = body.slice(idx + INDEX_MARKER.length).trim()
|
|
264
|
+
if (embeddedIndex && !existsSync(INDEX_PATH)) {
|
|
265
|
+
try {
|
|
266
|
+
writeFileSync(INDEX_PATH, embeddedIndex + '\n', { mode: 0o600 })
|
|
267
|
+
console.error(`(restored saves index to ${INDEX_PATH})`)
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
console.log(`[${label}] — ${new Date(parseInt(tsHex, 16) * 1000).toLocaleString()}`)
|
|
219
|
-
console.log(
|
|
273
|
+
console.log(userBody)
|
|
220
274
|
}
|
|
221
275
|
|
|
222
276
|
async function cmdLoad (opts = {}) {
|
|
@@ -299,9 +353,19 @@ if (cmd === 'init') {
|
|
|
299
353
|
const txidIdx = args.indexOf('--txid')
|
|
300
354
|
if (txidIdx >= 0 && args[txidIdx + 1]) {
|
|
301
355
|
await cmdLoadByTxid(args[txidIdx + 1])
|
|
356
|
+
} else if (args.includes('--last')) {
|
|
357
|
+
// Fast path: read newest txid from local saves-index.md.
|
|
358
|
+
// The slow UTXO-scan path stalls when the address holds many UTXOs
|
|
359
|
+
// (e.g. packaged Compute pieces from the splitter).
|
|
360
|
+
const lastTxid = existsSync(INDEX_PATH) ? readNewestTxidFromIndex(INDEX_PATH) : null
|
|
361
|
+
if (lastTxid) {
|
|
362
|
+
await cmdLoadByTxid(lastTxid)
|
|
363
|
+
} else {
|
|
364
|
+
console.error('No local saves index found — falling back to UTXO scan.')
|
|
365
|
+
await cmdLoad({ last: true })
|
|
366
|
+
}
|
|
302
367
|
} else {
|
|
303
368
|
const opts = {}
|
|
304
|
-
if (args.includes('--last')) opts.last = true
|
|
305
369
|
if (args.includes('--search')) opts.search = args[args.indexOf('--search') + 1]
|
|
306
370
|
await cmdLoad(opts)
|
|
307
371
|
}
|