txo_parser 0.0.2 → 0.0.4
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/LICENSE +13 -17
- package/README.md +3 -1
- package/demo/blocktrail.jsonld +10 -0
- package/demo/index.html +82 -0
- package/demo/ledger-history.jsonld +9 -0
- package/demo/lib/bitcoin.js +378 -0
- package/demo/panes/blocktrails-pane.js +673 -0
- package/demo/panes/builder-pane.js +190 -0
- package/demo/panes/faucet-pane.js +137 -0
- package/demo/panes/ledger-pane.js +662 -0
- package/demo/panes/parser-pane.js +171 -0
- package/demo/panes/spec-pane.js +124 -0
- package/demo/panes/voucher-pane.js +693 -0
- package/demo/voucher-data.jsonld +31 -0
- package/demo/webledger.jsonld +9 -0
- package/index.js +29 -8
- package/package.json +3 -3
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { html, render, keyed, onUnmount } from 'https://losos.org/losos/html.js'
|
|
2
|
+
import { Blocktrail, verify } from 'https://esm.sh/blocktrails@0.0.11'
|
|
3
|
+
import {
|
|
4
|
+
decodeKey, bytesToHex, hexToU8, sha256,
|
|
5
|
+
buildTransaction, estimateVsize, fetchUtxos, fetchTxDetails, broadcastTx, getFeeRate,
|
|
6
|
+
wpToP2trAddress, parseVoucherFromItem, hexToBytes
|
|
7
|
+
} from '../lib/bitcoin.js'
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
label: 'Ledger',
|
|
11
|
+
icon: '\uD83D\uDCCA',
|
|
12
|
+
|
|
13
|
+
canHandle(subject, store) {
|
|
14
|
+
var node = store.get(subject.value)
|
|
15
|
+
if (!node) return false
|
|
16
|
+
var type = store.type(node)
|
|
17
|
+
return type && type.includes('VoucherPool')
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
render(subject, lionStore, container, rawData) {
|
|
21
|
+
|
|
22
|
+
// ── URLs ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
var LEDGER_URL = new URL('webledger.jsonld', location.href).href
|
|
25
|
+
var TRAIL_URL = new URL('blocktrail.jsonld', location.href).href
|
|
26
|
+
var HISTORY_URL = new URL('ledger-history.jsonld', location.href).href
|
|
27
|
+
|
|
28
|
+
// ── State ───────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
var ledger = null
|
|
31
|
+
var trail = null
|
|
32
|
+
var privkeyHex = ''
|
|
33
|
+
var history = []
|
|
34
|
+
var saving = false
|
|
35
|
+
var error = null
|
|
36
|
+
var editingEntry = null // index being edited, or 'new'
|
|
37
|
+
|
|
38
|
+
// ── Load data ───────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
var loaded = 0
|
|
41
|
+
var totalLoads = 3
|
|
42
|
+
|
|
43
|
+
function checkReady() {
|
|
44
|
+
loaded++
|
|
45
|
+
if (loaded >= totalLoads) renderApp()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Load ledger
|
|
49
|
+
fetch(LEDGER_URL + '?t=' + Date.now(), { cache: 'no-store' })
|
|
50
|
+
.then(function(r) { return r.ok ? r.json() : null })
|
|
51
|
+
.then(function(data) {
|
|
52
|
+
if (data) ledger = data
|
|
53
|
+
else ledger = { '@context': 'https://w3id.org/webledgers', '@id': '#this', type: 'WebLedger', name: 'Voucher Pool Ledger', defaultCurrency: 'satoshi', entries: [] }
|
|
54
|
+
checkReady()
|
|
55
|
+
})
|
|
56
|
+
.catch(function() { ledger = { '@context': 'https://w3id.org/webledgers', '@id': '#this', type: 'WebLedger', name: 'Voucher Pool Ledger', defaultCurrency: 'satoshi', entries: [] }; checkReady() })
|
|
57
|
+
|
|
58
|
+
// Load trail
|
|
59
|
+
fetch(TRAIL_URL + '?t=' + Date.now(), { cache: 'no-store' })
|
|
60
|
+
.then(function(r) { return r.ok ? r.json() : null })
|
|
61
|
+
.then(function(data) {
|
|
62
|
+
if (data && data['bt:privkey']) {
|
|
63
|
+
try {
|
|
64
|
+
privkeyHex = data['bt:privkey']
|
|
65
|
+
trail = new Blocktrail(privkeyHex)
|
|
66
|
+
var stateNodes = data['bt:state'] || []
|
|
67
|
+
if (!Array.isArray(stateNodes)) stateNodes = [stateNodes]
|
|
68
|
+
stateNodes.forEach(function(n) {
|
|
69
|
+
var val = typeof n === 'string' ? n : (n['bt:value'] || '')
|
|
70
|
+
if (val) {
|
|
71
|
+
if (trail.states.length === 0) trail.genesis(val)
|
|
72
|
+
else trail.advance(val)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
} catch(e) { console.warn('Trail load error:', e) }
|
|
76
|
+
}
|
|
77
|
+
checkReady()
|
|
78
|
+
})
|
|
79
|
+
.catch(function() { checkReady() })
|
|
80
|
+
|
|
81
|
+
// Load history
|
|
82
|
+
fetch(HISTORY_URL + '?t=' + Date.now(), { cache: 'no-store' })
|
|
83
|
+
.then(function(r) { return r.ok ? r.json() : null })
|
|
84
|
+
.then(function(data) {
|
|
85
|
+
if (data && data['bt:snapshots']) {
|
|
86
|
+
history = Array.isArray(data['bt:snapshots']) ? data['bt:snapshots'] : [data['bt:snapshots']]
|
|
87
|
+
}
|
|
88
|
+
checkReady()
|
|
89
|
+
})
|
|
90
|
+
.catch(function() { checkReady() })
|
|
91
|
+
|
|
92
|
+
// ── Helpers ─────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function toast(msg) {
|
|
95
|
+
var t = document.createElement('div')
|
|
96
|
+
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:rgba(59,130,246,0.9);color:#fff;padding:8px 20px;border-radius:8px;font-size:0.85rem;font-weight:600;z-index:999;animation:v-fade 0.3s'
|
|
97
|
+
t.textContent = msg
|
|
98
|
+
document.body.appendChild(t)
|
|
99
|
+
setTimeout(function() { t.remove() }, 2000)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function copyText(text) {
|
|
103
|
+
navigator.clipboard.writeText(text).then(function() { toast('Copied') })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function truncate(s, start, end) {
|
|
107
|
+
start = start || 8; end = end || 6
|
|
108
|
+
if (!s || s.length <= start + end + 3) return s
|
|
109
|
+
return s.slice(0, start) + '\u2026' + s.slice(-end)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function hashContent(content) {
|
|
113
|
+
var str = typeof content === 'string' ? content : JSON.stringify(content)
|
|
114
|
+
var hash = await sha256(str)
|
|
115
|
+
return bytesToHex(hash)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Ledger operations ───────────────────────────────
|
|
119
|
+
|
|
120
|
+
function addEntry() {
|
|
121
|
+
var urlInput = container.querySelector('.wl-new-url')
|
|
122
|
+
var amountInput = container.querySelector('.wl-new-amount')
|
|
123
|
+
var url = urlInput && urlInput.value.trim()
|
|
124
|
+
var amount = amountInput && amountInput.value.trim()
|
|
125
|
+
if (!url || !amount) { toast('Enter URL and amount'); return }
|
|
126
|
+
|
|
127
|
+
if (!ledger.entries) ledger.entries = []
|
|
128
|
+
ledger.entries.push({ type: 'Entry', url: url, amount: amount })
|
|
129
|
+
editingEntry = null
|
|
130
|
+
renderApp()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeEntry(index) {
|
|
134
|
+
ledger.entries.splice(index, 1)
|
|
135
|
+
renderApp()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function updateEntry(index) {
|
|
139
|
+
var urlInput = container.querySelector('.wl-edit-url')
|
|
140
|
+
var amountInput = container.querySelector('.wl-edit-amount')
|
|
141
|
+
var url = urlInput && urlInput.value.trim()
|
|
142
|
+
var amount = amountInput && amountInput.value.trim()
|
|
143
|
+
if (!url || !amount) return
|
|
144
|
+
ledger.entries[index] = { type: 'Entry', url: url, amount: amount }
|
|
145
|
+
editingEntry = null
|
|
146
|
+
renderApp()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Verify ───────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
var verifyResult = null
|
|
152
|
+
var verifying = false
|
|
153
|
+
|
|
154
|
+
async function verifyLedger() {
|
|
155
|
+
if (verifying) return
|
|
156
|
+
verifying = true
|
|
157
|
+
verifyResult = { running: true }
|
|
158
|
+
renderApp()
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// 1. Hash current ledger
|
|
162
|
+
var currentHash = await hashContent(JSON.stringify(ledger, null, 2))
|
|
163
|
+
verifyResult.currentHash = currentHash
|
|
164
|
+
|
|
165
|
+
// 2. Get latest state from trail
|
|
166
|
+
if (!trail || trail.states.length === 0) {
|
|
167
|
+
verifyResult = { running: false, status: 'no-trail', message: 'No blocktrail configured. Anchor the ledger first.' }
|
|
168
|
+
verifying = false; renderApp(); return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var latestState = trail.states[trail.states.length - 1]
|
|
172
|
+
verifyResult.anchoredHash = latestState
|
|
173
|
+
|
|
174
|
+
await new Promise(function(r) { setTimeout(r, 400) })
|
|
175
|
+
|
|
176
|
+
// 3. Compare
|
|
177
|
+
var hashMatch = currentHash === latestState
|
|
178
|
+
verifyResult.hashMatch = hashMatch
|
|
179
|
+
|
|
180
|
+
// 4. Verify the full blocktrail chain
|
|
181
|
+
if (hashMatch) {
|
|
182
|
+
await new Promise(function(r) { setTimeout(r, 300) })
|
|
183
|
+
var exp = trail.export()
|
|
184
|
+
var wpBytes = exp.witnessPrograms.map(function(wp) { return hexToU8(wp) })
|
|
185
|
+
var chainResult = verify(exp.pubkeyBase, exp.states, wpBytes)
|
|
186
|
+
verifyResult.chainValid = chainResult.valid
|
|
187
|
+
verifyResult.chainError = chainResult.error
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 5. Check if latest state is on-chain
|
|
191
|
+
if (hashMatch && verifyResult.chainValid) {
|
|
192
|
+
await new Promise(function(r) { setTimeout(r, 300) })
|
|
193
|
+
try {
|
|
194
|
+
var exp = trail.export()
|
|
195
|
+
var latestWp = exp.witnessPrograms[exp.witnessPrograms.length - 1]
|
|
196
|
+
var addr = wpToP2trAddress(latestWp, true)
|
|
197
|
+
var utxos = await fetchUtxos(addr)
|
|
198
|
+
verifyResult.onChain = utxos.length > 0
|
|
199
|
+
if (utxos.length > 0) {
|
|
200
|
+
verifyResult.txid = utxos[0].txid
|
|
201
|
+
verifyResult.confirmed = utxos[0].status && utxos[0].status.confirmed
|
|
202
|
+
}
|
|
203
|
+
} catch(e) {
|
|
204
|
+
verifyResult.onChain = null // couldn't check
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
verifyResult.running = false
|
|
209
|
+
verifyResult.status = hashMatch && verifyResult.chainValid ? 'valid' : hashMatch ? 'chain-broken' : 'modified'
|
|
210
|
+
} catch(e) {
|
|
211
|
+
verifyResult = { running: false, status: 'error', message: e.message }
|
|
212
|
+
}
|
|
213
|
+
verifying = false
|
|
214
|
+
renderApp()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Save + Anchor ───────────────────────────────────
|
|
218
|
+
|
|
219
|
+
async function saveLedger(andAnchor) {
|
|
220
|
+
if (saving) return
|
|
221
|
+
saving = true
|
|
222
|
+
error = null
|
|
223
|
+
renderApp()
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// 1. Save ledger
|
|
227
|
+
var ledgerJson = JSON.stringify(ledger, null, 2)
|
|
228
|
+
await fetch(LEDGER_URL, {
|
|
229
|
+
method: 'PUT',
|
|
230
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
231
|
+
body: ledgerJson
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (!andAnchor) {
|
|
235
|
+
toast('Ledger saved')
|
|
236
|
+
saving = false
|
|
237
|
+
renderApp()
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Hash the ledger content
|
|
242
|
+
var contentHash = await hashContent(ledgerJson)
|
|
243
|
+
|
|
244
|
+
// 3. Snapshot to history
|
|
245
|
+
var snapshot = {
|
|
246
|
+
'@type': 'bt:Snapshot',
|
|
247
|
+
'bt:contentHash': 'sha256:' + contentHash,
|
|
248
|
+
'bt:timestamp': new Date().toISOString(),
|
|
249
|
+
'bt:content': JSON.parse(ledgerJson)
|
|
250
|
+
}
|
|
251
|
+
history.push(snapshot)
|
|
252
|
+
await fetch(HISTORY_URL, {
|
|
253
|
+
method: 'PUT',
|
|
254
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
'@context': { 'schema': 'https://schema.org/', 'bt': 'https://blocktrails.org/ns/' },
|
|
257
|
+
'@id': '#this',
|
|
258
|
+
'@type': 'bt:History',
|
|
259
|
+
'bt:snapshots': history
|
|
260
|
+
}, null, 2)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// 4. Advance blocktrail with the hash as state
|
|
264
|
+
if (!trail || !privkeyHex) {
|
|
265
|
+
toast('Saved + snapshot. No blocktrail configured \u2014 go to Blocktrails tab to initialize.')
|
|
266
|
+
saving = false
|
|
267
|
+
renderApp()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var stateValue = contentHash
|
|
272
|
+
if (trail.states.length === 0) {
|
|
273
|
+
trail.genesis(stateValue)
|
|
274
|
+
} else {
|
|
275
|
+
trail.advance(stateValue)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 5. Try to broadcast on-chain
|
|
279
|
+
var exp = trail.export()
|
|
280
|
+
var currentIdx = trail.states.length - 1
|
|
281
|
+
var prevIdx = currentIdx - 1
|
|
282
|
+
var newWp = exp.witnessPrograms[currentIdx]
|
|
283
|
+
var newAddr = wpToP2trAddress(newWp, true)
|
|
284
|
+
var txid = null
|
|
285
|
+
|
|
286
|
+
if (prevIdx >= 0) {
|
|
287
|
+
// Spend from previous state
|
|
288
|
+
var prevAddr = wpToP2trAddress(exp.witnessPrograms[prevIdx], true)
|
|
289
|
+
try {
|
|
290
|
+
var utxos = await fetchUtxos(prevAddr)
|
|
291
|
+
if (utxos.length > 0) {
|
|
292
|
+
var utxo = utxos[0]
|
|
293
|
+
var prevStates = trail.states.slice(0, currentIdx)
|
|
294
|
+
var signingKeyHex = null
|
|
295
|
+
if (prevIdx === 0) {
|
|
296
|
+
var g = (await import('https://esm.sh/blocktrails@0.0.11')).genesis
|
|
297
|
+
signingKeyHex = g(hexToBytes(privkeyHex), trail.states[0]).derivedPrivkey
|
|
298
|
+
} else {
|
|
299
|
+
var tr = (await import('https://esm.sh/blocktrails@0.0.11')).transition
|
|
300
|
+
signingKeyHex = tr(hexToBytes(privkeyHex), trail.states.slice(0, prevIdx), trail.states[prevIdx]).signingPrivkey
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
var txDetails = await fetchTxDetails(utxo.txid)
|
|
304
|
+
var prevOut = txDetails.vout[utxo.vout]
|
|
305
|
+
var inputScript = hexToU8(prevOut.scriptpubkey)
|
|
306
|
+
|
|
307
|
+
var outputScript = new Uint8Array(34)
|
|
308
|
+
outputScript[0] = 0x51; outputScript[1] = 0x20
|
|
309
|
+
outputScript.set(hexToU8(newWp), 2)
|
|
310
|
+
|
|
311
|
+
var feeRate = await getFeeRate()
|
|
312
|
+
var fee = Math.ceil(estimateVsize(1, 1) * feeRate)
|
|
313
|
+
var outputAmount = utxo.value - fee
|
|
314
|
+
if (outputAmount > 546) {
|
|
315
|
+
var rawTx = await buildTransaction(
|
|
316
|
+
[{ txid: utxo.txid, vout: utxo.vout, amount: utxo.value, scriptPubKey: inputScript }],
|
|
317
|
+
[{ amount: outputAmount, scriptPubKey: outputScript }],
|
|
318
|
+
hexToU8(signingKeyHex)
|
|
319
|
+
)
|
|
320
|
+
txid = await broadcastTx(rawTx)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch(e) {
|
|
324
|
+
console.warn('On-chain anchor failed (trail still saved):', e)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 6. Save trail
|
|
329
|
+
var trailJsonLd = {
|
|
330
|
+
'@context': { 'schema': 'https://schema.org/', 'bt': 'https://blocktrails.org/ns/' },
|
|
331
|
+
'@id': '#this',
|
|
332
|
+
'@type': 'bt:Trail',
|
|
333
|
+
'bt:privkey': privkeyHex,
|
|
334
|
+
'bt:pubkeyBase': exp.pubkeyBase,
|
|
335
|
+
'bt:state': exp.states.map(function(s, i) {
|
|
336
|
+
var stateObj = {
|
|
337
|
+
'@type': 'bt:State',
|
|
338
|
+
'bt:value': s,
|
|
339
|
+
'bt:witnessProgram': exp.witnessPrograms[i],
|
|
340
|
+
'bt:address': wpToP2trAddress(exp.witnessPrograms[i], true),
|
|
341
|
+
'bt:source': 'webledger.jsonld',
|
|
342
|
+
'bt:contentHash': 'sha256:' + s,
|
|
343
|
+
'bt:timestamp': i === currentIdx ? new Date().toISOString() : undefined
|
|
344
|
+
}
|
|
345
|
+
if (i === currentIdx && txid) {
|
|
346
|
+
stateObj['bt:txid'] = txid
|
|
347
|
+
stateObj['bt:confirmed'] = false
|
|
348
|
+
}
|
|
349
|
+
return stateObj
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
await fetch(TRAIL_URL, {
|
|
353
|
+
method: 'PUT',
|
|
354
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
355
|
+
body: JSON.stringify(trailJsonLd, null, 2)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
snapshot['bt:txid'] = txid || null
|
|
359
|
+
// Update history with txid
|
|
360
|
+
await fetch(HISTORY_URL, {
|
|
361
|
+
method: 'PUT',
|
|
362
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
'@context': { 'schema': 'https://schema.org/', 'bt': 'https://blocktrails.org/ns/' },
|
|
365
|
+
'@id': '#this',
|
|
366
|
+
'@type': 'bt:History',
|
|
367
|
+
'bt:snapshots': history
|
|
368
|
+
}, null, 2)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
toast(txid ? 'Saved + anchored on Bitcoin! ' + truncate(txid) : 'Saved + trail advanced (off-chain)')
|
|
372
|
+
} catch(e) {
|
|
373
|
+
error = e.message
|
|
374
|
+
toast('Save failed: ' + e.message)
|
|
375
|
+
console.error(e)
|
|
376
|
+
}
|
|
377
|
+
saving = false
|
|
378
|
+
renderApp()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Render ───────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
function renderApp() {
|
|
384
|
+
if (!ledger) return
|
|
385
|
+
|
|
386
|
+
var entries = ledger.entries || []
|
|
387
|
+
var totalBalance = entries.reduce(function(s, e) {
|
|
388
|
+
var amt = typeof e.amount === 'string' ? parseInt(e.amount) || 0 : 0
|
|
389
|
+
return s + amt
|
|
390
|
+
}, 0)
|
|
391
|
+
var hasTrail = trail && trail.states.length > 0
|
|
392
|
+
var stateCount = trail ? trail.states.length : 0
|
|
393
|
+
|
|
394
|
+
render(container, html`
|
|
395
|
+
<style>
|
|
396
|
+
.wl-wrap { padding: 0 16px 40px; }
|
|
397
|
+
.wl-hero { text-align: center; padding: 40px 0 32px; }
|
|
398
|
+
.wl-hero-icon { font-size: 3rem; margin-bottom: 8px; }
|
|
399
|
+
.wl-hero-title { font-size: 1.6rem; font-weight: 800; margin-bottom: 4px; }
|
|
400
|
+
.wl-hero-sub { font-size: 0.9rem; color: rgba(255,255,255,0.35); }
|
|
401
|
+
.wl-stats { display: flex; gap: 16px; justify-content: center; margin-bottom: 24px; flex-wrap: wrap; }
|
|
402
|
+
.wl-stat { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 24px; text-align: center; }
|
|
403
|
+
.wl-stat-val { font-size: 1.5rem; font-weight: 700; color: #3b82f6; }
|
|
404
|
+
.wl-stat-label { font-size: 0.7rem; color: rgba(255,255,255,0.35); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }
|
|
405
|
+
.wl-card { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 24px; margin-bottom: 20px; backdrop-filter: blur(12px); }
|
|
406
|
+
.wl-card h2 { font-size: 1rem; font-weight: 600; color: rgba(255,255,255,0.9); margin: 0 0 16px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
|
|
407
|
+
.wl-input { padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.9); font: 0.88rem -apple-system, sans-serif; outline: none; }
|
|
408
|
+
.wl-input::placeholder { color: rgba(255,255,255,0.25); }
|
|
409
|
+
.wl-input:focus { border-color: rgba(59,130,246,0.5); }
|
|
410
|
+
.wl-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 20px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.8); font: 600 0.85rem -apple-system, sans-serif; cursor: pointer; transition: all 0.15s; }
|
|
411
|
+
.wl-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
|
412
|
+
.wl-btn-primary { background: linear-gradient(135deg, #3b82f6, #2563eb); border-color: rgba(59,130,246,0.4); color: #fff; box-shadow: 0 4px 16px rgba(59,130,246,0.2); }
|
|
413
|
+
.wl-btn-primary:hover { box-shadow: 0 6px 24px rgba(59,130,246,0.3); }
|
|
414
|
+
.wl-btn-anchor { background: linear-gradient(135deg, #f7931a, #e8850f); border-color: rgba(247,147,26,0.4); color: #000; box-shadow: 0 4px 16px rgba(247,147,26,0.2); }
|
|
415
|
+
.wl-btn-anchor:hover { box-shadow: 0 6px 24px rgba(247,147,26,0.3); }
|
|
416
|
+
.wl-btn-sm { padding: 5px 10px; font-size: 0.78rem; border-radius: 6px; }
|
|
417
|
+
.wl-btn-danger { color: #ef4444; }
|
|
418
|
+
.wl-btn-danger:hover { background: rgba(239,68,68,0.12); border-color: rgba(239,68,68,0.3); }
|
|
419
|
+
.wl-entry { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; margin-bottom: 8px; transition: background 0.15s; }
|
|
420
|
+
.wl-entry:hover { background: rgba(255,255,255,0.06); }
|
|
421
|
+
.wl-entry-url { flex: 1; font-size: 0.88rem; color: rgba(255,255,255,0.7); font-family: 'SF Mono', 'Fira Code', monospace; word-break: break-all; cursor: pointer; }
|
|
422
|
+
.wl-entry-url:hover { color: rgba(255,255,255,0.9); }
|
|
423
|
+
.wl-entry-amount { font-size: 1.1rem; font-weight: 700; color: #3b82f6; min-width: 80px; text-align: right; }
|
|
424
|
+
.wl-entry-amount small { font-size: 0.65em; color: rgba(255,255,255,0.3); font-weight: 400; }
|
|
425
|
+
.wl-entry-actions { display: flex; gap: 4px; }
|
|
426
|
+
.wl-add-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
427
|
+
.wl-empty { text-align: center; padding: 32px; color: rgba(255,255,255,0.25); font-style: italic; }
|
|
428
|
+
.wl-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: #ef4444; border-radius: 8px; padding: 10px 14px; font-size: 0.85rem; margin-bottom: 12px; }
|
|
429
|
+
.wl-history-item { padding: 10px 14px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); border-radius: 8px; margin-bottom: 6px; font-size: 0.82rem; }
|
|
430
|
+
.wl-history-hash { color: rgba(59,130,246,0.6); font-family: 'SF Mono', monospace; font-size: 0.75rem; }
|
|
431
|
+
.wl-history-time { color: rgba(255,255,255,0.25); font-size: 0.72rem; }
|
|
432
|
+
.wl-history-entries { color: rgba(255,255,255,0.4); font-size: 0.75rem; margin-top: 4px; }
|
|
433
|
+
.wl-spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.15); border-top-color: #3b82f6; border-radius: 50%; animation: v-spin 0.6s linear infinite; display: inline-block; }
|
|
434
|
+
.wl-verify-card { margin-top: 16px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; }
|
|
435
|
+
@keyframes bt-slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
436
|
+
.wl-verify-header { display: flex; align-items: center; gap: 10px; font-size: 1.1rem; font-weight: 700; margin-bottom: 14px; }
|
|
437
|
+
.wl-verify-icon { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1rem; }
|
|
438
|
+
.wl-vi-pass { background: rgba(16,185,129,0.15); color: #10b981; border: 2px solid rgba(16,185,129,0.4); box-shadow: 0 0 16px rgba(16,185,129,0.2); }
|
|
439
|
+
.wl-vi-fail { background: rgba(239,68,68,0.15); color: #ef4444; border: 2px solid rgba(239,68,68,0.4); }
|
|
440
|
+
.wl-vi-warn { background: rgba(251,191,36,0.15); color: #fbbf24; border: 2px solid rgba(251,191,36,0.4); }
|
|
441
|
+
.wl-vi-info { background: rgba(59,130,246,0.15); color: #3b82f6; border: 2px solid rgba(59,130,246,0.4); }
|
|
442
|
+
.wl-verify-steps { display: flex; flex-direction: column; gap: 8px; }
|
|
443
|
+
.wl-verify-step { display: flex; align-items: center; gap: 10px; font-size: 0.85rem; }
|
|
444
|
+
.wl-vdot { width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; flex-shrink: 0; }
|
|
445
|
+
.wl-vdot-pass { background: rgba(16,185,129,0.15); color: #10b981; border: 1px solid rgba(16,185,129,0.3); }
|
|
446
|
+
.wl-vdot-fail { background: rgba(239,68,68,0.15); color: #ef4444; border: 1px solid rgba(239,68,68,0.3); }
|
|
447
|
+
.wl-verify-label { color: rgba(255,255,255,0.4); font-weight: 600; min-width: 110px; }
|
|
448
|
+
.wl-verify-val { color: rgba(255,255,255,0.6); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.82rem; }
|
|
449
|
+
.wl-save-bar { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; flex-wrap: wrap; }
|
|
450
|
+
@media (max-width: 600px) { .wl-add-row { flex-direction: column; } .wl-entry { flex-wrap: wrap; } }
|
|
451
|
+
</style>
|
|
452
|
+
|
|
453
|
+
<div class="wl-wrap">
|
|
454
|
+
<div class="wl-hero">
|
|
455
|
+
<div class="wl-hero-icon">\uD83D\uDCCA</div>
|
|
456
|
+
<div class="wl-hero-title">${ledger.name || 'Web Ledger'}</div>
|
|
457
|
+
<div class="wl-hero-sub">${ledger.description || 'URI-to-balance mappings anchored to Bitcoin'}</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
${error ? html`<div class="wl-error">${error}</div>` : null}
|
|
461
|
+
|
|
462
|
+
<div class="wl-stats">
|
|
463
|
+
<div class="wl-stat">
|
|
464
|
+
<div class="wl-stat-val">${String(entries.length)}</div>
|
|
465
|
+
<div class="wl-stat-label">Entries</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="wl-stat">
|
|
468
|
+
<div class="wl-stat-val">${totalBalance.toLocaleString()}</div>
|
|
469
|
+
<div class="wl-stat-label">${ledger.defaultCurrency || 'units'}</div>
|
|
470
|
+
</div>
|
|
471
|
+
<div class="wl-stat">
|
|
472
|
+
<div class="wl-stat-val">${String(stateCount)}</div>
|
|
473
|
+
<div class="wl-stat-label">Anchored</div>
|
|
474
|
+
</div>
|
|
475
|
+
<div class="wl-stat">
|
|
476
|
+
<div class="wl-stat-val">${String(history.length)}</div>
|
|
477
|
+
<div class="wl-stat-label">Snapshots</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
<div class="wl-card">
|
|
482
|
+
<h2>
|
|
483
|
+
Entries
|
|
484
|
+
<button class="wl-btn wl-btn-sm" onclick="${function() { editingEntry = editingEntry === 'new' ? null : 'new'; renderApp() }}">
|
|
485
|
+
${editingEntry === 'new' ? '\u2716 Cancel' : '+ Add Entry'}
|
|
486
|
+
</button>
|
|
487
|
+
</h2>
|
|
488
|
+
|
|
489
|
+
${editingEntry === 'new' ? html`
|
|
490
|
+
<div class="wl-add-row" style="margin-bottom:12px">
|
|
491
|
+
<input class="wl-input wl-new-url" placeholder="did:nostr:pubkey or any URI" style="flex:2"
|
|
492
|
+
onkeydown="${function(e) { if (e.key === 'Enter') addEntry() }}" />
|
|
493
|
+
<input class="wl-input wl-new-amount" placeholder="Amount" style="flex:0.5;min-width:100px"
|
|
494
|
+
onkeydown="${function(e) { if (e.key === 'Enter') addEntry() }}" />
|
|
495
|
+
<button class="wl-btn wl-btn-primary wl-btn-sm" onclick="${addEntry}">Add</button>
|
|
496
|
+
</div>
|
|
497
|
+
` : null}
|
|
498
|
+
|
|
499
|
+
${entries.length === 0 ? html`
|
|
500
|
+
<div class="wl-empty">No entries. Add a URI and balance to get started.</div>
|
|
501
|
+
` : null}
|
|
502
|
+
|
|
503
|
+
${entries.map(function(entry, i) {
|
|
504
|
+
var amountStr = typeof entry.amount === 'string' ? entry.amount : JSON.stringify(entry.amount)
|
|
505
|
+
if (editingEntry === i) {
|
|
506
|
+
return html`
|
|
507
|
+
<div class="wl-add-row" style="margin-bottom:8px">
|
|
508
|
+
<input class="wl-input wl-edit-url" value="${entry.url}" style="flex:2" />
|
|
509
|
+
<input class="wl-input wl-edit-amount" value="${amountStr}" style="flex:0.5;min-width:100px" />
|
|
510
|
+
<button class="wl-btn wl-btn-primary wl-btn-sm" onclick="${function() { updateEntry(i) }}">\u2713</button>
|
|
511
|
+
<button class="wl-btn wl-btn-sm" onclick="${function() { editingEntry = null; renderApp() }}">\u2716</button>
|
|
512
|
+
</div>
|
|
513
|
+
`
|
|
514
|
+
}
|
|
515
|
+
return html`
|
|
516
|
+
<div class="wl-entry">
|
|
517
|
+
<div class="wl-entry-url" onclick="${function() { copyText(entry.url) }}" title="${entry.url}">
|
|
518
|
+
${truncate(entry.url, 20, 12)}
|
|
519
|
+
</div>
|
|
520
|
+
<div class="wl-entry-amount">
|
|
521
|
+
${parseInt(amountStr).toLocaleString()} ${html`<small>${ledger.defaultCurrency || ''}</small>`}
|
|
522
|
+
</div>
|
|
523
|
+
<div class="wl-entry-actions">
|
|
524
|
+
<button class="wl-btn wl-btn-sm" onclick="${function() { editingEntry = i; renderApp() }}">\u270E</button>
|
|
525
|
+
<button class="wl-btn wl-btn-sm wl-btn-danger" onclick="${function() { removeEntry(i); }}">\u2716</button>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
`
|
|
529
|
+
})}
|
|
530
|
+
|
|
531
|
+
<div class="wl-save-bar">
|
|
532
|
+
<button class="wl-btn" onclick="${verifyLedger}" disabled="${verifying}">
|
|
533
|
+
${verifying ? html`<span class="wl-spinner"></span>` : '\u2713'} Verify
|
|
534
|
+
</button>
|
|
535
|
+
<button class="wl-btn wl-btn-primary" onclick="${function() { saveLedger(false) }}" disabled="${saving}">
|
|
536
|
+
${saving ? html`<span class="wl-spinner"></span>` : '\uD83D\uDCBE'} Save
|
|
537
|
+
</button>
|
|
538
|
+
<button class="wl-btn wl-btn-anchor" onclick="${function() { saveLedger(true) }}" disabled="${saving || !privkeyHex}">
|
|
539
|
+
${saving ? html`<span class="wl-spinner"></span>` : '\u26D3'} Save + Anchor to Bitcoin
|
|
540
|
+
</button>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
${verifyResult && !verifyResult.running ? html`
|
|
544
|
+
<div class="wl-verify-card" style="animation: bt-slideIn 0.3s ease">
|
|
545
|
+
<div class="wl-verify-header">
|
|
546
|
+
${verifyResult.status === 'valid' ? html`
|
|
547
|
+
<span class="wl-verify-icon wl-vi-pass">\u2713</span>
|
|
548
|
+
<span>Ledger Verified</span>
|
|
549
|
+
` : verifyResult.status === 'modified' ? html`
|
|
550
|
+
<span class="wl-verify-icon wl-vi-warn">\u26A0</span>
|
|
551
|
+
<span>Ledger Modified</span>
|
|
552
|
+
` : verifyResult.status === 'no-trail' ? html`
|
|
553
|
+
<span class="wl-verify-icon wl-vi-info">\u2139</span>
|
|
554
|
+
<span>Not Anchored</span>
|
|
555
|
+
` : html`
|
|
556
|
+
<span class="wl-verify-icon wl-vi-fail">\u2716</span>
|
|
557
|
+
<span>${verifyResult.message || 'Verification Failed'}</span>
|
|
558
|
+
`}
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
${verifyResult.currentHash ? html`
|
|
562
|
+
<div class="wl-verify-steps">
|
|
563
|
+
<div class="wl-verify-step">
|
|
564
|
+
<span class="${'wl-vdot ' + (verifyResult.hashMatch ? 'wl-vdot-pass' : 'wl-vdot-fail')}">
|
|
565
|
+
${verifyResult.hashMatch ? '\u2713' : '\u2716'}
|
|
566
|
+
</span>
|
|
567
|
+
<span class="wl-verify-label">Content hash</span>
|
|
568
|
+
<span class="wl-verify-val" onclick="${function() { copyText(verifyResult.currentHash) }}" style="cursor:pointer">
|
|
569
|
+
${truncate('sha256:' + verifyResult.currentHash, 14, 8)}
|
|
570
|
+
</span>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
${verifyResult.hashMatch === false ? html`
|
|
574
|
+
<div class="wl-verify-step">
|
|
575
|
+
<span class="wl-vdot wl-vdot-fail">\u2716</span>
|
|
576
|
+
<span class="wl-verify-label">Anchored hash</span>
|
|
577
|
+
<span class="wl-verify-val" style="color:#ef4444">${truncate('sha256:' + verifyResult.anchoredHash, 14, 8)}</span>
|
|
578
|
+
</div>
|
|
579
|
+
` : null}
|
|
580
|
+
|
|
581
|
+
${verifyResult.chainValid !== undefined ? html`
|
|
582
|
+
<div class="wl-verify-step">
|
|
583
|
+
<span class="${'wl-vdot ' + (verifyResult.chainValid ? 'wl-vdot-pass' : 'wl-vdot-fail')}">
|
|
584
|
+
${verifyResult.chainValid ? '\u2713' : '\u2716'}
|
|
585
|
+
</span>
|
|
586
|
+
<span class="wl-verify-label">Blocktrail chain</span>
|
|
587
|
+
<span class="wl-verify-val">${verifyResult.chainValid ? 'All witness programs match' : verifyResult.chainError || 'Invalid'}</span>
|
|
588
|
+
</div>
|
|
589
|
+
` : null}
|
|
590
|
+
|
|
591
|
+
${verifyResult.onChain !== undefined && verifyResult.onChain !== null ? html`
|
|
592
|
+
<div class="wl-verify-step">
|
|
593
|
+
<span class="${'wl-vdot ' + (verifyResult.onChain ? 'wl-vdot-pass' : 'wl-vdot-fail')}">
|
|
594
|
+
${verifyResult.onChain ? '\u2713' : '\u2716'}
|
|
595
|
+
</span>
|
|
596
|
+
<span class="wl-verify-label">Bitcoin</span>
|
|
597
|
+
<span class="wl-verify-val">
|
|
598
|
+
${verifyResult.confirmed ? 'Confirmed' : verifyResult.onChain ? 'Pending' : 'Not found'}
|
|
599
|
+
${verifyResult.txid ? html` \u00B7 <a href="${'https://mempool.space/testnet4/tx/' + verifyResult.txid}" target="_blank" rel="noopener" style="color:rgba(247,147,26,0.6);text-decoration:none">${truncate(verifyResult.txid, 6, 4)}</a>` : null}
|
|
600
|
+
</span>
|
|
601
|
+
</div>
|
|
602
|
+
` : null}
|
|
603
|
+
</div>
|
|
604
|
+
` : null}
|
|
605
|
+
|
|
606
|
+
${verifyResult.status === 'modified' ? html`
|
|
607
|
+
<div style="margin-top:12px;font-size:0.78rem;color:rgba(255,255,255,0.3)">
|
|
608
|
+
The ledger has been modified since last anchor. Click "Save + Anchor to Bitcoin" to record the new state.
|
|
609
|
+
</div>
|
|
610
|
+
` : null}
|
|
611
|
+
</div>
|
|
612
|
+
` : verifyResult && verifyResult.running ? html`
|
|
613
|
+
<div class="wl-verify-card" style="animation: bt-slideIn 0.3s ease">
|
|
614
|
+
<div class="wl-verify-header">
|
|
615
|
+
<span class="wl-spinner"></span>
|
|
616
|
+
<span>Verifying\u2026</span>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
` : null}
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
${history.length > 0 ? html`
|
|
623
|
+
<div class="wl-card">
|
|
624
|
+
<h2>
|
|
625
|
+
History
|
|
626
|
+
<span style="font-size:0.75rem;color:rgba(255,255,255,0.25);font-weight:400">${String(history.length)} snapshot${history.length > 1 ? 's' : ''}</span>
|
|
627
|
+
</h2>
|
|
628
|
+
${history.slice().reverse().map(function(snap) {
|
|
629
|
+
var hash = snap['bt:contentHash'] || ''
|
|
630
|
+
var time = snap['bt:timestamp'] || ''
|
|
631
|
+
var txid = snap['bt:txid'] || ''
|
|
632
|
+
var content = snap['bt:content']
|
|
633
|
+
var entryCount = content && content.entries ? content.entries.length : '?'
|
|
634
|
+
var totalAmt = 0
|
|
635
|
+
if (content && content.entries) {
|
|
636
|
+
content.entries.forEach(function(e) { totalAmt += parseInt(e.amount) || 0 })
|
|
637
|
+
}
|
|
638
|
+
return html`
|
|
639
|
+
<div class="wl-history-item">
|
|
640
|
+
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:4px">
|
|
641
|
+
<span class="wl-history-hash" onclick="${function() { copyText(hash) }}" style="cursor:pointer" title="Click to copy hash">
|
|
642
|
+
${truncate(hash, 12, 8)}
|
|
643
|
+
</span>
|
|
644
|
+
<span class="wl-history-time">${time ? new Date(time).toLocaleString() : ''}</span>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="wl-history-entries">
|
|
647
|
+
${String(entryCount)} entries \u00B7 ${totalAmt.toLocaleString()} total
|
|
648
|
+
${txid ? html` \u00B7 <a href="${'https://mempool.space/testnet4/tx/' + txid}" target="_blank" rel="noopener" style="color:rgba(247,147,26,0.6);text-decoration:none">${truncate(txid, 6, 4)}</a>` : null}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
`
|
|
652
|
+
})}
|
|
653
|
+
</div>
|
|
654
|
+
` : null}
|
|
655
|
+
</div>
|
|
656
|
+
`)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
renderApp()
|
|
660
|
+
onUnmount(container, function() {})
|
|
661
|
+
}
|
|
662
|
+
}
|