@smartledger/bsv 3.3.4 → 3.4.0
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/CHANGELOG.md +30 -21
- package/README.md +169 -40
- package/anchor-entry.js +1 -0
- package/bin/cli.js +349 -0
- package/bsv-covenant.min.js +5 -5
- package/bsv-gdaf.min.js +6 -6
- package/bsv-ltp.min.js +6 -6
- package/bsv-smartcontract.min.js +9 -9
- package/bsv.bundle.js +5 -5
- package/bsv.min.js +8 -8
- package/build/webpack.anchor.config.js +21 -0
- package/build/webpack.didweb.config.js +21 -0
- package/build/webpack.statuslist.config.js +22 -0
- package/build/webpack.vcjwt.config.js +21 -0
- package/demos/README.md +1 -1
- package/demos/browser-test.html +1208 -0
- package/demos/smart_contract_demo.html +1 -1
- package/demos/smart_contract_demo.js +1 -1
- package/demos/web3keys.html +3 -3
- package/didweb-entry.js +1 -0
- package/docs/DOCUMENTATION_REVIEW_REPORT.md +11 -11
- package/docs/FIX_CREATEHMAC_ISSUE.md +1 -1
- package/docs/MODULE_REFERENCE_COMPLETE.md +28 -28
- package/docs/SMARTLEDGER_BSV_USAGE_ANSWERS.md +4 -4
- package/docs/SMARTLEDGER_BSV_USAGE_EXAMPLES.js +2 -2
- package/docs/SMARTLEDGER_BSV_USAGE_GUIDE.md +3 -3
- package/docs/SMART_CONTRACT_DEVELOPMENT_GUIDE.md +1 -1
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +2 -2
- package/docs/getting-started/INSTALLATION.md +25 -25
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/docs/technical/roadmap.md +3 -3
- package/index.js +35 -0
- package/lib/anchor/index.js +102 -0
- package/lib/browser-utxo-manager-es5.js +316 -0
- package/lib/browser-utxo-manager.js +533 -0
- package/lib/didweb/index.js +177 -0
- package/lib/statuslist/index.js +164 -0
- package/lib/vcjwt/index.js +189 -0
- package/package.json +13 -5
- package/statuslist-entry.js +1 -0
- package/tests/browser-compatibility/test-cdn-vs-local.html +2 -2
- package/vcjwt-entry.js +1 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-Compatible UTXO Manager
|
|
5
|
+
* Lightweight UTXO management for browser environments with configurable storage
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Storage types available for browser UTXO management
|
|
10
|
+
*/
|
|
11
|
+
var STORAGE_TYPES = {
|
|
12
|
+
MEMORY: 'memory', // In-memory only (lost on page reload)
|
|
13
|
+
SESSION: 'session', // sessionStorage (lost when tab closes)
|
|
14
|
+
LOCAL: 'local' // localStorage (persists until cleared)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Browser-compatible UTXO Manager
|
|
19
|
+
* Provides UTXO tracking and management for browser applications
|
|
20
|
+
*/
|
|
21
|
+
function BrowserUTXOManager(options) {
|
|
22
|
+
options = options || {}
|
|
23
|
+
/**
|
|
24
|
+
* Create a new browser UTXO manager
|
|
25
|
+
* @param {Object} options - Configuration options
|
|
26
|
+
* @param {string} options.storage - Storage type: 'memory', 'session', or 'local' (default: 'memory')
|
|
27
|
+
* @param {string} options.storageKey - Key for browser storage (default: 'smartledger-utxos')
|
|
28
|
+
* @param {boolean} options.autoSave - Auto-save after each operation (default: true)
|
|
29
|
+
* @param {number} options.maxUTXOs - Maximum UTXOs to store (default: 1000)
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.options = {
|
|
33
|
+
storage: options.storage || STORAGE_TYPES.MEMORY,
|
|
34
|
+
storageKey: options.storageKey || 'smartledger-utxos',
|
|
35
|
+
autoSave: options.autoSave !== false,
|
|
36
|
+
maxUTXOs: options.maxUTXOs || 1000,
|
|
37
|
+
...options
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate storage type
|
|
41
|
+
if (!Object.values(STORAGE_TYPES).includes(this.options.storage)) {
|
|
42
|
+
throw new Error(`Invalid storage type: ${this.options.storage}. Must be one of: ${Object.values(STORAGE_TYPES).join(', ')}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Initialize storage
|
|
46
|
+
this.utxos = new Map() // Main UTXO store: key = "txid:vout", value = utxo object
|
|
47
|
+
this.addressIndex = new Map() // Address index: key = address, value = Set of utxo keys
|
|
48
|
+
this.spentUTXOs = new Map() // Spent UTXO tracking
|
|
49
|
+
this.metadata = {
|
|
50
|
+
totalUTXOs: 0,
|
|
51
|
+
totalValue: 0,
|
|
52
|
+
createdAt: new Date().toISOString(),
|
|
53
|
+
lastUpdated: new Date().toISOString()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load existing data
|
|
57
|
+
this.loadFromStorage()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get storage interface based on configuration
|
|
62
|
+
* @returns {Object} Storage interface (memory, sessionStorage, or localStorage)
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_getStorage() {
|
|
66
|
+
switch (this.options.storage) {
|
|
67
|
+
case STORAGE_TYPES.MEMORY:
|
|
68
|
+
return null // Memory storage handled by class properties
|
|
69
|
+
case STORAGE_TYPES.SESSION:
|
|
70
|
+
return typeof sessionStorage !== 'undefined' ? sessionStorage : null
|
|
71
|
+
case STORAGE_TYPES.LOCAL:
|
|
72
|
+
return typeof localStorage !== 'undefined' ? localStorage : null
|
|
73
|
+
default:
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load UTXOs from configured storage
|
|
80
|
+
*/
|
|
81
|
+
loadFromStorage() {
|
|
82
|
+
try {
|
|
83
|
+
if (this.options.storage === STORAGE_TYPES.MEMORY) {
|
|
84
|
+
// Memory storage - nothing to load, start fresh
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const storage = this._getStorage()
|
|
89
|
+
if (!storage) {
|
|
90
|
+
console.warn('BrowserUTXOManager: Storage not available, using memory mode')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stored = storage.getItem(this.options.storageKey)
|
|
95
|
+
if (!stored) {
|
|
96
|
+
return // No existing data
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = JSON.parse(stored)
|
|
100
|
+
|
|
101
|
+
// Restore UTXOs
|
|
102
|
+
if (data.utxos) {
|
|
103
|
+
data.utxos.forEach(utxoData => {
|
|
104
|
+
const key = `${utxoData.txid}:${utxoData.vout}`
|
|
105
|
+
this.utxos.set(key, utxoData)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Restore address index
|
|
110
|
+
if (data.addressIndex) {
|
|
111
|
+
Object.entries(data.addressIndex).forEach(([address, utxoKeys]) => {
|
|
112
|
+
this.addressIndex.set(address, new Set(utxoKeys))
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Restore spent UTXOs
|
|
117
|
+
if (data.spentUTXOs) {
|
|
118
|
+
data.spentUTXOs.forEach(spentData => {
|
|
119
|
+
const key = `${spentData.txid}:${spentData.vout}`
|
|
120
|
+
this.spentUTXOs.set(key, spentData)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Restore metadata
|
|
125
|
+
if (data.metadata) {
|
|
126
|
+
this.metadata = { ...this.metadata, ...data.metadata }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this._updateMetadata()
|
|
130
|
+
console.log(`✅ BrowserUTXOManager: Loaded ${this.utxos.size} UTXOs from ${this.options.storage} storage`)
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('BrowserUTXOManager: Error loading from storage:', error.message)
|
|
134
|
+
// Continue with empty state
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Save UTXOs to configured storage
|
|
140
|
+
*/
|
|
141
|
+
saveToStorage() {
|
|
142
|
+
try {
|
|
143
|
+
if (this.options.storage === STORAGE_TYPES.MEMORY) {
|
|
144
|
+
return // Memory storage - nothing to persist
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const storage = this._getStorage()
|
|
148
|
+
if (!storage) {
|
|
149
|
+
return // Storage not available
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Prepare data for serialization
|
|
153
|
+
const data = {
|
|
154
|
+
utxos: Array.from(this.utxos.values()),
|
|
155
|
+
addressIndex: {},
|
|
156
|
+
spentUTXOs: Array.from(this.spentUTXOs.values()),
|
|
157
|
+
metadata: this.metadata
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Convert address index to serializable format
|
|
161
|
+
this.addressIndex.forEach((utxoKeys, address) => {
|
|
162
|
+
data.addressIndex[address] = Array.from(utxoKeys)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
storage.setItem(this.options.storageKey, JSON.stringify(data))
|
|
166
|
+
console.log(`💾 BrowserUTXOManager: Saved ${this.utxos.size} UTXOs to ${this.options.storage} storage`)
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('BrowserUTXOManager: Error saving to storage:', error.message)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add a UTXO to the manager
|
|
175
|
+
* @param {Object} utxo - UTXO object {txid, vout, address, satoshis, script}
|
|
176
|
+
* @returns {boolean} - true if added, false if already exists or limit exceeded
|
|
177
|
+
*/
|
|
178
|
+
addUTXO(utxo) {
|
|
179
|
+
try {
|
|
180
|
+
// Validate UTXO
|
|
181
|
+
if (!utxo.txid || typeof utxo.vout !== 'number' || !utxo.address || typeof utxo.satoshis !== 'number') {
|
|
182
|
+
throw new Error('Invalid UTXO: missing required fields (txid, vout, address, satoshis)')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const key = `${utxo.txid}:${utxo.vout}`
|
|
186
|
+
|
|
187
|
+
// Check if already exists
|
|
188
|
+
if (this.utxos.has(key)) {
|
|
189
|
+
console.log(`⚠️ UTXO already exists: ${key}`)
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check limits
|
|
194
|
+
if (this.utxos.size >= this.options.maxUTXOs) {
|
|
195
|
+
console.warn(`⚠️ Maximum UTXO limit reached (${this.options.maxUTXOs})`)
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add timestamp
|
|
200
|
+
const utxoWithMeta = {
|
|
201
|
+
...utxo,
|
|
202
|
+
addedAt: new Date().toISOString()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Store UTXO
|
|
206
|
+
this.utxos.set(key, utxoWithMeta)
|
|
207
|
+
|
|
208
|
+
// Update address index
|
|
209
|
+
if (!this.addressIndex.has(utxo.address)) {
|
|
210
|
+
this.addressIndex.set(utxo.address, new Set())
|
|
211
|
+
}
|
|
212
|
+
this.addressIndex.get(utxo.address).add(key)
|
|
213
|
+
|
|
214
|
+
this._updateMetadata()
|
|
215
|
+
|
|
216
|
+
if (this.options.autoSave) {
|
|
217
|
+
this.saveToStorage()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`✅ UTXO added: ${key} (${utxo.satoshis} sats)`)
|
|
221
|
+
return true
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('BrowserUTXOManager: Error adding UTXO:', error.message)
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get all UTXOs for a specific address
|
|
231
|
+
* @param {string} address - Bitcoin address
|
|
232
|
+
* @returns {Array} Array of UTXO objects
|
|
233
|
+
*/
|
|
234
|
+
getUTXOsForAddress(address) {
|
|
235
|
+
try {
|
|
236
|
+
const utxoKeys = this.addressIndex.get(address)
|
|
237
|
+
if (!utxoKeys) {
|
|
238
|
+
return []
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const utxos = []
|
|
242
|
+
utxoKeys.forEach(key => {
|
|
243
|
+
const utxo = this.utxos.get(key)
|
|
244
|
+
if (utxo) {
|
|
245
|
+
utxos.push(utxo)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return utxos.sort((a, b) => b.satoshis - a.satoshis) // Sort by value descending
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('BrowserUTXOManager: Error getting UTXOs for address:', error.message)
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get total balance for an address
|
|
259
|
+
* @param {string} address - Bitcoin address
|
|
260
|
+
* @returns {number} Total satoshis
|
|
261
|
+
*/
|
|
262
|
+
getBalance(address) {
|
|
263
|
+
const utxos = this.getUTXOsForAddress(address)
|
|
264
|
+
return utxos.reduce((total, utxo) => total + utxo.satoshis, 0)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Spend UTXOs (mark as spent and remove from available set)
|
|
269
|
+
* @param {Array} inputs - Array of input objects {txid, vout} or {txid, vout, spentInTx}
|
|
270
|
+
* @param {string} spentInTx - Optional transaction ID where UTXOs were spent
|
|
271
|
+
* @returns {Array} Array of spent UTXO objects
|
|
272
|
+
*/
|
|
273
|
+
spendUTXOs(inputs, spentInTx = 'browser-spend') {
|
|
274
|
+
const spentUTXOs = []
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
inputs.forEach(input => {
|
|
278
|
+
const key = `${input.txid}:${input.vout}`
|
|
279
|
+
const utxo = this.utxos.get(key)
|
|
280
|
+
|
|
281
|
+
if (!utxo) {
|
|
282
|
+
console.warn(`⚠️ UTXO not found: ${key}`)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mark as spent
|
|
287
|
+
const spentUTXO = {
|
|
288
|
+
...utxo,
|
|
289
|
+
spentAt: new Date().toISOString(),
|
|
290
|
+
spentInTx: input.spentInTx || spentInTx
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.spentUTXOs.set(key, spentUTXO)
|
|
294
|
+
spentUTXOs.push(spentUTXO)
|
|
295
|
+
|
|
296
|
+
// Remove from available UTXOs
|
|
297
|
+
this.utxos.delete(key)
|
|
298
|
+
|
|
299
|
+
// Update address index
|
|
300
|
+
const addressSet = this.addressIndex.get(utxo.address)
|
|
301
|
+
if (addressSet) {
|
|
302
|
+
addressSet.delete(key)
|
|
303
|
+
if (addressSet.size === 0) {
|
|
304
|
+
this.addressIndex.delete(utxo.address)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(`❌ UTXO spent: ${key} in ${spentUTXO.spentInTx}`)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
this._updateMetadata()
|
|
312
|
+
|
|
313
|
+
if (this.options.autoSave) {
|
|
314
|
+
this.saveToStorage()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('BrowserUTXOManager: Error spending UTXOs:', error.message)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return spentUTXOs
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if a UTXO is available (unspent)
|
|
326
|
+
* @param {string} txid - Transaction ID
|
|
327
|
+
* @param {number} vout - Output index
|
|
328
|
+
* @returns {boolean} True if UTXO is available
|
|
329
|
+
*/
|
|
330
|
+
isUTXOAvailable(txid, vout) {
|
|
331
|
+
const key = `${txid}:${vout}`
|
|
332
|
+
return this.utxos.has(key)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get UTXO details
|
|
337
|
+
* @param {string} txid - Transaction ID
|
|
338
|
+
* @param {number} vout - Output index
|
|
339
|
+
* @returns {Object|null} UTXO object or null if not found
|
|
340
|
+
*/
|
|
341
|
+
getUTXO(txid, vout) {
|
|
342
|
+
const key = `${txid}:${vout}`
|
|
343
|
+
|
|
344
|
+
// Check if available
|
|
345
|
+
if (this.utxos.has(key)) {
|
|
346
|
+
return { status: 'available', utxo: this.utxos.get(key) }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check if spent
|
|
350
|
+
if (this.spentUTXOs.has(key)) {
|
|
351
|
+
return { status: 'spent', utxo: this.spentUTXOs.get(key) }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { status: 'not_found', utxo: null }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Create mock UTXOs for testing (browser-compatible)
|
|
359
|
+
* @param {string} address - Target address
|
|
360
|
+
* @param {number} count - Number of UTXOs to create
|
|
361
|
+
* @param {number} satoshis - Satoshis per UTXO
|
|
362
|
+
* @returns {Array} Array of created UTXOs
|
|
363
|
+
*/
|
|
364
|
+
createMockUTXOs(address, count = 5, satoshis = 100000) {
|
|
365
|
+
const mockUTXOs = []
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
for (let i = 0; i < count; i++) {
|
|
369
|
+
// Generate random txid using Web Crypto API
|
|
370
|
+
const txidArray = new Uint8Array(32)
|
|
371
|
+
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
|
|
372
|
+
window.crypto.getRandomValues(txidArray)
|
|
373
|
+
} else {
|
|
374
|
+
// Fallback for environments without Web Crypto
|
|
375
|
+
for (let j = 0; j < 32; j++) {
|
|
376
|
+
txidArray[j] = Math.floor(Math.random() * 256)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const txid = Array.from(txidArray).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
381
|
+
const script = `76a914${Array.from(new Uint8Array(20)).map(b => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('')}88ac`
|
|
382
|
+
|
|
383
|
+
const utxo = {
|
|
384
|
+
txid,
|
|
385
|
+
vout: i,
|
|
386
|
+
address,
|
|
387
|
+
satoshis,
|
|
388
|
+
script,
|
|
389
|
+
isMock: true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.addUTXO(utxo)) {
|
|
393
|
+
mockUTXOs.push(utxo)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('BrowserUTXOManager: Error creating mock UTXOs:', error.message)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return mockUTXOs
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get manager statistics
|
|
406
|
+
* @returns {Object} Statistics object
|
|
407
|
+
*/
|
|
408
|
+
getStats() {
|
|
409
|
+
const addresses = Array.from(this.addressIndex.keys())
|
|
410
|
+
const balancesByAddress = {}
|
|
411
|
+
|
|
412
|
+
addresses.forEach(address => {
|
|
413
|
+
balancesByAddress[address] = this.getBalance(address)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
totalUTXOs: this.utxos.size,
|
|
418
|
+
totalSpent: this.spentUTXOs.size,
|
|
419
|
+
totalValue: this.metadata.totalValue,
|
|
420
|
+
totalAddresses: addresses.length,
|
|
421
|
+
storageType: this.options.storage,
|
|
422
|
+
storageKey: this.options.storageKey,
|
|
423
|
+
balancesByAddress,
|
|
424
|
+
createdAt: this.metadata.createdAt,
|
|
425
|
+
lastUpdated: this.metadata.lastUpdated
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Clear all UTXOs and reset state
|
|
431
|
+
* @param {boolean} clearStorage - Also clear browser storage (default: true)
|
|
432
|
+
*/
|
|
433
|
+
reset(clearStorage = true) {
|
|
434
|
+
this.utxos.clear()
|
|
435
|
+
this.addressIndex.clear()
|
|
436
|
+
this.spentUTXOs.clear()
|
|
437
|
+
|
|
438
|
+
this.metadata = {
|
|
439
|
+
totalUTXOs: 0,
|
|
440
|
+
totalValue: 0,
|
|
441
|
+
createdAt: new Date().toISOString(),
|
|
442
|
+
lastUpdated: new Date().toISOString()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (clearStorage && this.options.storage !== STORAGE_TYPES.MEMORY) {
|
|
446
|
+
const storage = this._getStorage()
|
|
447
|
+
if (storage) {
|
|
448
|
+
storage.removeItem(this.options.storageKey)
|
|
449
|
+
console.log(`🔄 Cleared ${this.options.storage} storage`)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
console.log('🔄 BrowserUTXOManager reset complete')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Update internal metadata
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
_updateMetadata() {
|
|
461
|
+
this.metadata.totalUTXOs = this.utxos.size
|
|
462
|
+
this.metadata.totalValue = Array.from(this.utxos.values())
|
|
463
|
+
.reduce((total, utxo) => total + utxo.satoshis, 0)
|
|
464
|
+
this.metadata.lastUpdated = new Date().toISOString()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Export UTXOs as JSON
|
|
469
|
+
* @returns {string} JSON string of all data
|
|
470
|
+
*/
|
|
471
|
+
exportData() {
|
|
472
|
+
const data = {
|
|
473
|
+
utxos: Array.from(this.utxos.values()),
|
|
474
|
+
spentUTXOs: Array.from(this.spentUTXOs.values()),
|
|
475
|
+
metadata: this.metadata,
|
|
476
|
+
exportedAt: new Date().toISOString()
|
|
477
|
+
}
|
|
478
|
+
return JSON.stringify(data, null, 2)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Import UTXOs from JSON
|
|
483
|
+
* @param {string} jsonData - JSON string to import
|
|
484
|
+
* @param {boolean} merge - Merge with existing data (default: false)
|
|
485
|
+
* @returns {boolean} Success status
|
|
486
|
+
*/
|
|
487
|
+
importData(jsonData, merge = false) {
|
|
488
|
+
try {
|
|
489
|
+
const data = JSON.parse(jsonData)
|
|
490
|
+
|
|
491
|
+
if (!merge) {
|
|
492
|
+
this.reset(false) // Don't clear storage yet
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Import UTXOs
|
|
496
|
+
if (data.utxos && Array.isArray(data.utxos)) {
|
|
497
|
+
data.utxos.forEach(utxo => {
|
|
498
|
+
this.addUTXO(utxo)
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Import spent UTXOs
|
|
503
|
+
if (data.spentUTXOs && Array.isArray(data.spentUTXOs)) {
|
|
504
|
+
data.spentUTXOs.forEach(spentUTXO => {
|
|
505
|
+
const key = `${spentUTXO.txid}:${spentUTXO.vout}`
|
|
506
|
+
this.spentUTXOs.set(key, spentUTXO)
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this._updateMetadata()
|
|
511
|
+
|
|
512
|
+
if (this.options.autoSave) {
|
|
513
|
+
this.saveToStorage()
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log('✅ BrowserUTXOManager: Imported ' + (data.utxos && data.utxos.length || 0) + ' UTXOs')
|
|
517
|
+
return true
|
|
518
|
+
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error('BrowserUTXOManager: Error importing data:', error.message)
|
|
521
|
+
return false
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Export for both CommonJS and ES modules
|
|
527
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
528
|
+
module.exports = BrowserUTXOManager
|
|
529
|
+
module.exports.STORAGE_TYPES = STORAGE_TYPES
|
|
530
|
+
} else if (typeof window !== 'undefined') {
|
|
531
|
+
window.BrowserUTXOManager = BrowserUTXOManager
|
|
532
|
+
window.BrowserUTXOManager.STORAGE_TYPES = STORAGE_TYPES
|
|
533
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DID:web Module
|
|
5
|
+
* Legally-recognizable DID (did:web) generation and management
|
|
6
|
+
* Supports ES256 (P-256) and ES256K (secp256k1) keys
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
var crypto = require('crypto')
|
|
10
|
+
|
|
11
|
+
// Generate issuer keys (ES256 or ES256K)
|
|
12
|
+
async function generateIssuerKeys(opts) {
|
|
13
|
+
opts = opts || {}
|
|
14
|
+
var alg = opts.alg || 'ES256'
|
|
15
|
+
var kid = opts.kid || 'key-' + Date.now()
|
|
16
|
+
|
|
17
|
+
if (alg !== 'ES256' && alg !== 'ES256K') {
|
|
18
|
+
throw new Error('Invalid algorithm. Must be ES256 or ES256K')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var keyPair
|
|
22
|
+
if (alg === 'ES256') {
|
|
23
|
+
// P-256 (NIST curve)
|
|
24
|
+
keyPair = crypto.generateKeyPairSync('ec', {
|
|
25
|
+
namedCurve: 'P-256',
|
|
26
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
27
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
28
|
+
})
|
|
29
|
+
} else {
|
|
30
|
+
// secp256k1
|
|
31
|
+
keyPair = crypto.generateKeyPairSync('ec', {
|
|
32
|
+
namedCurve: 'secp256k1',
|
|
33
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
34
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Convert to JWK format
|
|
39
|
+
var publicJwk = crypto.createPublicKey(keyPair.publicKey).export({ format: 'jwk' })
|
|
40
|
+
var privateJwk = crypto.createPrivateKey(keyPair.privateKey).export({ format: 'jwk' })
|
|
41
|
+
|
|
42
|
+
// Add required JWK fields
|
|
43
|
+
publicJwk.kid = kid
|
|
44
|
+
publicJwk.alg = alg
|
|
45
|
+
publicJwk.use = 'sig'
|
|
46
|
+
publicJwk.kty = 'EC'
|
|
47
|
+
|
|
48
|
+
privateJwk.kid = kid
|
|
49
|
+
privateJwk.alg = alg
|
|
50
|
+
privateJwk.use = 'sig'
|
|
51
|
+
privateJwk.kty = 'EC'
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
privateJwk: privateJwk,
|
|
55
|
+
publicJwk: publicJwk,
|
|
56
|
+
kid: kid,
|
|
57
|
+
alg: alg
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build did:web documents (did.json and jwks.json)
|
|
62
|
+
function buildDidWebDocuments(params) {
|
|
63
|
+
if (!params.domain) {
|
|
64
|
+
throw new Error('domain is required')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var domain = params.domain
|
|
68
|
+
var did = 'did:web:' + domain.replace(/:/g, '%3A')
|
|
69
|
+
|
|
70
|
+
var verificationMethods = []
|
|
71
|
+
var publicKeys = []
|
|
72
|
+
|
|
73
|
+
// Add P-256 key if provided
|
|
74
|
+
if (params.p256) {
|
|
75
|
+
var p256Method = {
|
|
76
|
+
id: did + '#' + params.p256.kid,
|
|
77
|
+
type: 'JsonWebKey2020',
|
|
78
|
+
controller: did,
|
|
79
|
+
publicKeyJwk: params.p256.jwk
|
|
80
|
+
}
|
|
81
|
+
verificationMethods.push(p256Method)
|
|
82
|
+
publicKeys.push(params.p256.jwk)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add secp256k1 key if provided
|
|
86
|
+
if (params.k1) {
|
|
87
|
+
var k1Method = {
|
|
88
|
+
id: did + '#' + params.k1.kid,
|
|
89
|
+
type: 'JsonWebKey2020',
|
|
90
|
+
controller: did,
|
|
91
|
+
publicKeyJwk: params.k1.jwk
|
|
92
|
+
}
|
|
93
|
+
verificationMethods.push(k1Method)
|
|
94
|
+
publicKeys.push(params.k1.jwk)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (verificationMethods.length === 0) {
|
|
98
|
+
throw new Error('At least one key (p256 or k1) must be provided')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Build DID Document
|
|
102
|
+
var didDocument = {
|
|
103
|
+
'@context': [
|
|
104
|
+
'https://www.w3.org/ns/did/v1',
|
|
105
|
+
'https://w3id.org/security/suites/jws-2020/v1'
|
|
106
|
+
],
|
|
107
|
+
id: did,
|
|
108
|
+
verificationMethod: verificationMethods,
|
|
109
|
+
authentication: verificationMethods.map(function(vm) { return vm.id }),
|
|
110
|
+
assertionMethod: verificationMethods.map(function(vm) { return vm.id })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (params.controllerName) {
|
|
114
|
+
didDocument.controller = params.controllerName
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build JWKS
|
|
118
|
+
var jwks = {
|
|
119
|
+
keys: publicKeys
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
did: did,
|
|
124
|
+
didDocument: didDocument,
|
|
125
|
+
jwks: jwks
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Rotate issuer key
|
|
130
|
+
function rotateIssuerKey(params) {
|
|
131
|
+
if (!params.domain || !params.newKey) {
|
|
132
|
+
throw new Error('domain and newKey are required')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
var domain = params.domain
|
|
136
|
+
var did = 'did:web:' + domain.replace(/:/g, '%3A')
|
|
137
|
+
var keepOldForDays = params.keepOldForDays || 30
|
|
138
|
+
|
|
139
|
+
// Create verification method for new key
|
|
140
|
+
var newMethod = {
|
|
141
|
+
id: did + '#' + params.newKey.kid,
|
|
142
|
+
type: 'JsonWebKey2020',
|
|
143
|
+
controller: did,
|
|
144
|
+
publicKeyJwk: params.newKey.jwk
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build updated DID Document with new key as primary
|
|
148
|
+
var didDocument = {
|
|
149
|
+
'@context': [
|
|
150
|
+
'https://www.w3.org/ns/did/v1',
|
|
151
|
+
'https://w3id.org/security/suites/jws-2020/v1'
|
|
152
|
+
],
|
|
153
|
+
id: did,
|
|
154
|
+
verificationMethod: [newMethod],
|
|
155
|
+
authentication: [newMethod.id],
|
|
156
|
+
assertionMethod: [newMethod.id],
|
|
157
|
+
rotationInfo: {
|
|
158
|
+
rotatedAt: new Date().toISOString(),
|
|
159
|
+
gracePeriodDays: keepOldForDays
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var jwks = {
|
|
164
|
+
keys: [params.newKey.jwk]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
didDocument: didDocument,
|
|
169
|
+
jwks: jwks
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
generateIssuerKeys: generateIssuerKeys,
|
|
175
|
+
buildDidWebDocuments: buildDidWebDocuments,
|
|
176
|
+
rotateIssuerKey: rotateIssuerKey
|
|
177
|
+
}
|