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.
Files changed (2) hide show
  1. package/index.mjs +67 -3
  2. 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
- const envelope = JSON.stringify({ kv: 1, kind: 'save', label, body })
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(body)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keychat-save",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Save and load files to BSV blockchain via KeyChat",
5
5
  "type": "module",
6
6
  "bin": {