@bsv/sdk 1.8.0 → 1.8.2
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/dist/cjs/package.json +1 -1
- package/dist/cjs/src/kvstore/GlobalKVStore.js +420 -0
- package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -0
- package/dist/cjs/src/kvstore/LocalKVStore.js +6 -6
- package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
- package/dist/cjs/src/kvstore/index.js +3 -1
- package/dist/cjs/src/kvstore/index.js.map +1 -1
- package/dist/cjs/src/kvstore/kvStoreInterpreter.js +74 -0
- package/dist/cjs/src/kvstore/kvStoreInterpreter.js.map +1 -0
- package/dist/cjs/src/kvstore/types.js +11 -0
- package/dist/cjs/src/kvstore/types.js.map +1 -0
- package/dist/cjs/src/overlay-tools/Historian.js +153 -0
- package/dist/cjs/src/overlay-tools/Historian.js.map +1 -0
- package/dist/cjs/src/script/templates/PushDrop.js +2 -2
- package/dist/cjs/src/script/templates/PushDrop.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/kvstore/GlobalKVStore.js +416 -0
- package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -0
- package/dist/esm/src/kvstore/LocalKVStore.js +6 -6
- package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
- package/dist/esm/src/kvstore/index.js +1 -0
- package/dist/esm/src/kvstore/index.js.map +1 -1
- package/dist/esm/src/kvstore/kvStoreInterpreter.js +47 -0
- package/dist/esm/src/kvstore/kvStoreInterpreter.js.map +1 -0
- package/dist/esm/src/kvstore/types.js +8 -0
- package/dist/esm/src/kvstore/types.js.map +1 -0
- package/dist/esm/src/overlay-tools/Historian.js +155 -0
- package/dist/esm/src/overlay-tools/Historian.js.map +1 -0
- package/dist/esm/src/script/templates/PushDrop.js +2 -2
- package/dist/esm/src/script/templates/PushDrop.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/kvstore/GlobalKVStore.d.ts +129 -0
- package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -0
- package/dist/types/src/kvstore/index.d.ts +1 -0
- package/dist/types/src/kvstore/index.d.ts.map +1 -1
- package/dist/types/src/kvstore/kvStoreInterpreter.d.ts +22 -0
- package/dist/types/src/kvstore/kvStoreInterpreter.d.ts.map +1 -0
- package/dist/types/src/kvstore/types.d.ts +106 -0
- package/dist/types/src/kvstore/types.d.ts.map +1 -0
- package/dist/types/src/overlay-tools/Historian.d.ts +92 -0
- package/dist/types/src/overlay-tools/Historian.d.ts.map +1 -0
- package/dist/types/src/script/templates/PushDrop.d.ts +6 -5
- package/dist/types/src/script/templates/PushDrop.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/compat.md +15 -27
- package/docs/reference/identity.md +12 -16
- package/docs/reference/kvstore.md +471 -4
- package/docs/reference/messages.md +0 -8
- package/docs/reference/overlay-tools.md +15 -22
- package/docs/reference/primitives.md +168 -168
- package/docs/reference/registry.md +9 -19
- package/docs/reference/script.md +35 -48
- package/docs/reference/storage.md +10 -14
- package/docs/reference/totp.md +5 -5
- package/docs/reference/transaction.md +117 -69
- package/docs/reference/wallet.md +131 -135
- package/package.json +1 -1
- package/src/kvstore/GlobalKVStore.ts +478 -0
- package/src/kvstore/LocalKVStore.ts +7 -7
- package/src/kvstore/__tests/GlobalKVStore.test.ts +965 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +72 -0
- package/src/kvstore/index.ts +1 -0
- package/src/kvstore/kvStoreInterpreter.ts +49 -0
- package/src/kvstore/types.ts +114 -0
- package/src/overlay-tools/Historian.ts +195 -0
- package/src/overlay-tools/__tests/Historian.test.ts +690 -0
- package/src/script/templates/PushDrop.ts +6 -5
package/package.json
CHANGED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import Transaction from '../transaction/Transaction.js'
|
|
2
|
+
import * as Utils from '../primitives/utils.js'
|
|
3
|
+
import { TopicBroadcaster, LookupResolver } from '../overlay-tools/index.js'
|
|
4
|
+
import { BroadcastResponse, BroadcastFailure } from '../transaction/Broadcaster.js'
|
|
5
|
+
import { WalletInterface, WalletProtocol, CreateActionInput, OutpointString, PubKeyHex, CreateActionOutput, HexString } from '../wallet/Wallet.interfaces.js'
|
|
6
|
+
import { PushDrop } from '../script/index.js'
|
|
7
|
+
import WalletClient from '../wallet/WalletClient.js'
|
|
8
|
+
import { Beef } from '../transaction/Beef.js'
|
|
9
|
+
import { Historian } from '../overlay-tools/Historian.js'
|
|
10
|
+
import { KVContext, kvStoreInterpreter } from './kvStoreInterpreter.js'
|
|
11
|
+
import { ProtoWallet } from '../wallet/ProtoWallet.js'
|
|
12
|
+
import { kvProtocol, KVStoreConfig, KVStoreQuery, KVStoreEntry, KVStoreGetOptions, KVStoreSetOptions, KVStoreRemoveOptions } from './types.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default configuration values for GlobalKVStore operations.
|
|
16
|
+
* Provides sensible defaults for overlay connection and protocol settings.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_CONFIG: KVStoreConfig = {
|
|
19
|
+
protocolID: [1, 'kvstore'],
|
|
20
|
+
serviceName: 'ls_kvstore',
|
|
21
|
+
tokenAmount: 1,
|
|
22
|
+
topics: ['tm_kvstore'],
|
|
23
|
+
networkPreset: 'mainnet',
|
|
24
|
+
acceptDelayedBroadcast: false,
|
|
25
|
+
tokenSetDescription: '', // Will be set dynamically
|
|
26
|
+
tokenUpdateDescription: '', // Will be set dynamically
|
|
27
|
+
tokenRemovalDescription: '' // Will be set dynamically
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Implements a global key-value storage system which uses an overlay service to track key-value pairs.
|
|
32
|
+
* Each key-value pair is represented by a PushDrop token output.
|
|
33
|
+
* Allows getting, setting, and removing key-value pairs with optional fetching by protocolID and history tracking.
|
|
34
|
+
*/
|
|
35
|
+
export class GlobalKVStore {
|
|
36
|
+
/**
|
|
37
|
+
* The wallet interface used to create transactions and perform cryptographic operations.
|
|
38
|
+
* @readonly
|
|
39
|
+
*/
|
|
40
|
+
private readonly wallet: WalletInterface
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for the KVStore instance containing all runtime options.
|
|
44
|
+
* @private
|
|
45
|
+
* @readonly
|
|
46
|
+
*/
|
|
47
|
+
private readonly config: KVStoreConfig
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Historian instance used to extract history from transaction outputs.
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
private readonly historian: Historian<string, KVContext>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lookup resolver used to query the overlay for transaction outputs.
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
private readonly lookupResolver: LookupResolver
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Topic broadcaster used to broadcast transactions to the overlay.
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
private readonly topicBroadcaster: TopicBroadcaster
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A map to store locks for each key to ensure atomic updates.
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
private readonly keyLocks: Map<string, Array<(value: void | PromiseLike<void>) => void>> = new Map()
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cached user identity key
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
private cachedIdentityKey: PubKeyHex | null = null
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates an instance of the GlobalKVStore.
|
|
81
|
+
*
|
|
82
|
+
* @param {KVStoreConfig} [config={}] - Configuration options for the KVStore. Defaults to empty object.
|
|
83
|
+
* @param {WalletInterface} [config.wallet] - Wallet to use for operations. Defaults to WalletClient.
|
|
84
|
+
* @throws {Error} If the configuration contains invalid parameters.
|
|
85
|
+
*/
|
|
86
|
+
constructor (config: KVStoreConfig = {}) {
|
|
87
|
+
// Merge with defaults to create a fully resolved config
|
|
88
|
+
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
89
|
+
this.wallet = config.wallet ?? new WalletClient()
|
|
90
|
+
this.historian = new Historian<string, KVContext>(kvStoreInterpreter)
|
|
91
|
+
this.lookupResolver = new LookupResolver({
|
|
92
|
+
networkPreset: this.config.networkPreset
|
|
93
|
+
})
|
|
94
|
+
this.topicBroadcaster = new TopicBroadcaster(this.config.topics as string[], {
|
|
95
|
+
networkPreset: this.config.networkPreset
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Retrieves data from the KVStore.
|
|
101
|
+
* Can query by key+controller (single result), protocolID, controller, or key (multiple results).
|
|
102
|
+
*
|
|
103
|
+
* @param {KVStoreQuery} query - Query parameters sent to overlay
|
|
104
|
+
* @param {KVStoreGetOptions} [options={}] - Configuration options for the get operation
|
|
105
|
+
* @returns {Promise<KVStoreEntry | KVStoreEntry[] | undefined>} Single entry for key+controller queries, array for all other queries
|
|
106
|
+
*/
|
|
107
|
+
async get (query: KVStoreQuery, options: KVStoreGetOptions = {}): Promise<KVStoreEntry | KVStoreEntry[] | undefined> {
|
|
108
|
+
if (Object.keys(query).length === 0) {
|
|
109
|
+
throw new Error('Must specify either key, controller, or protocolID')
|
|
110
|
+
}
|
|
111
|
+
if (query.key != null && query.controller != null) {
|
|
112
|
+
// Specific key+controller query - return single entry
|
|
113
|
+
const entries = await this.queryOverlay(query, options)
|
|
114
|
+
return entries.length > 0 ? entries[0] : undefined
|
|
115
|
+
}
|
|
116
|
+
return await this.queryOverlay(query, options)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sets a key-value pair. The current user (wallet identity) becomes the controller.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} key - The key to set (user computes this however they want)
|
|
123
|
+
* @param {string} value - The value to store
|
|
124
|
+
* @param {KVStoreSetOptions} [options={}] - Configuration options for the set operation
|
|
125
|
+
* @returns {Promise<OutpointString>} The outpoint of the created token
|
|
126
|
+
*/
|
|
127
|
+
async set (key: string, value: string, options: KVStoreSetOptions = {}): Promise<OutpointString> {
|
|
128
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
129
|
+
throw new Error('Key must be a non-empty string.')
|
|
130
|
+
}
|
|
131
|
+
if (typeof value !== 'string') {
|
|
132
|
+
throw new Error('Value must be a string.')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const controller = await this.getIdentityKey()
|
|
136
|
+
const lockQueue = await this.queueOperationOnKey(key)
|
|
137
|
+
const protocolID = options.protocolID ?? this.config.protocolID
|
|
138
|
+
const tokenSetDescription = (options.tokenSetDescription != null && options.tokenSetDescription !== '') ? options.tokenSetDescription : `Create KVStore value for ${key}`
|
|
139
|
+
const tokenUpdateDescription = (options.tokenUpdateDescription != null && options.tokenUpdateDescription !== '') ? options.tokenUpdateDescription : `Update KVStore value for ${key}`
|
|
140
|
+
const tokenAmount = options.tokenAmount ?? this.config.tokenAmount
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Check for existing token to spend
|
|
144
|
+
const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
|
|
145
|
+
const existingToken = existingEntries.length > 0 ? existingEntries[0].token : undefined
|
|
146
|
+
|
|
147
|
+
// Create PushDrop locking script
|
|
148
|
+
const pushdrop = new PushDrop(this.wallet, this.config.originator)
|
|
149
|
+
const lockingScript = await pushdrop.lock(
|
|
150
|
+
[
|
|
151
|
+
Utils.toArray(JSON.stringify(protocolID), 'utf8'),
|
|
152
|
+
Utils.toArray(key, 'utf8'),
|
|
153
|
+
Utils.toArray(value, 'utf8'),
|
|
154
|
+
Utils.toArray(controller, 'hex')
|
|
155
|
+
],
|
|
156
|
+
protocolID ?? this.config.protocolID as WalletProtocol,
|
|
157
|
+
Utils.toUTF8(Utils.toArray(key, 'utf8')),
|
|
158
|
+
'anyone',
|
|
159
|
+
true
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
let inputs: CreateActionInput[] = []
|
|
163
|
+
let inputBEEF: Beef | undefined
|
|
164
|
+
|
|
165
|
+
if (existingToken != null) {
|
|
166
|
+
inputs = [{
|
|
167
|
+
outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
|
|
168
|
+
unlockingScriptLength: 74,
|
|
169
|
+
inputDescription: 'Previous KVStore token'
|
|
170
|
+
}]
|
|
171
|
+
inputBEEF = existingToken.beef
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (inputs.length > 0) {
|
|
175
|
+
// Update existing token
|
|
176
|
+
const { signableTransaction } = await this.wallet.createAction({
|
|
177
|
+
description: tokenUpdateDescription,
|
|
178
|
+
inputBEEF: inputBEEF?.toBinary(),
|
|
179
|
+
inputs,
|
|
180
|
+
outputs: [{
|
|
181
|
+
satoshis: tokenAmount ?? this.config.tokenAmount as number,
|
|
182
|
+
lockingScript: lockingScript.toHex(),
|
|
183
|
+
outputDescription: 'KVStore token'
|
|
184
|
+
}],
|
|
185
|
+
options: {
|
|
186
|
+
acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
|
|
187
|
+
randomizeOutputs: false
|
|
188
|
+
}
|
|
189
|
+
}, this.config.originator)
|
|
190
|
+
|
|
191
|
+
if (signableTransaction == null) {
|
|
192
|
+
throw new Error('Unable to create update transaction')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
|
|
196
|
+
const unlocker = pushdrop.unlock(
|
|
197
|
+
this.config.protocolID as WalletProtocol,
|
|
198
|
+
key,
|
|
199
|
+
'anyone'
|
|
200
|
+
)
|
|
201
|
+
const unlockingScript = await unlocker.sign(tx, 0)
|
|
202
|
+
|
|
203
|
+
const { tx: finalTx } = await this.wallet.signAction({
|
|
204
|
+
reference: signableTransaction.reference,
|
|
205
|
+
spends: { 0: { unlockingScript: unlockingScript.toHex() } }
|
|
206
|
+
}, this.config.originator)
|
|
207
|
+
|
|
208
|
+
if (finalTx == null) {
|
|
209
|
+
throw new Error('Unable to finalize update transaction')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const transaction = Transaction.fromAtomicBEEF(finalTx)
|
|
213
|
+
await this.submitToOverlay(transaction)
|
|
214
|
+
return `${transaction.id('hex')}.0`
|
|
215
|
+
} else {
|
|
216
|
+
// Create new token
|
|
217
|
+
const { tx } = await this.wallet.createAction({
|
|
218
|
+
description: tokenSetDescription,
|
|
219
|
+
outputs: [{
|
|
220
|
+
satoshis: tokenAmount ?? this.config.tokenAmount as number,
|
|
221
|
+
lockingScript: lockingScript.toHex(),
|
|
222
|
+
outputDescription: 'KVStore token'
|
|
223
|
+
}],
|
|
224
|
+
options: {
|
|
225
|
+
acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
|
|
226
|
+
randomizeOutputs: false
|
|
227
|
+
}
|
|
228
|
+
}, this.config.originator)
|
|
229
|
+
|
|
230
|
+
if (tx == null) {
|
|
231
|
+
throw new Error('Failed to create transaction')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const transaction = Transaction.fromAtomicBEEF(tx)
|
|
235
|
+
await this.submitToOverlay(transaction)
|
|
236
|
+
return `${transaction.id('hex')}.0`
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
if (lockQueue.length > 0) {
|
|
240
|
+
this.finishOperationOnKey(key, lockQueue)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Removes the key-value pair associated with the given key from the overlay service.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} key - The key to remove.
|
|
249
|
+
* @param {CreateActionOutput[] | undefined} [outputs=undefined] - Additional outputs to include in the removal transaction.
|
|
250
|
+
* @param {KVStoreRemoveOptions} [options=undefined] - Optional parameters for the removal operation.
|
|
251
|
+
* @returns {Promise<HexString>} A promise that resolves to the txid of the removal transaction if successful.
|
|
252
|
+
* @throws {Error} If the key is invalid.
|
|
253
|
+
* @throws {Error} If the key does not exist in the store.
|
|
254
|
+
* @throws {Error} If the overlay service is unreachable or the transaction fails.
|
|
255
|
+
* @throws {Error} If there are existing tokens that cannot be unlocked.
|
|
256
|
+
*/
|
|
257
|
+
async remove (key: string, outputs?: CreateActionOutput[], options: KVStoreRemoveOptions = {}): Promise<HexString> {
|
|
258
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
259
|
+
throw new Error('Key must be a non-empty string.')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const controller = await this.getIdentityKey()
|
|
263
|
+
const lockQueue = await this.queueOperationOnKey(key)
|
|
264
|
+
|
|
265
|
+
const protocolID = options.protocolID ?? this.config.protocolID
|
|
266
|
+
const tokenRemovalDescription = (options.tokenRemovalDescription != null && options.tokenRemovalDescription !== '') ? options.tokenRemovalDescription : `Remove KVStore value for ${key}`
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
|
|
270
|
+
|
|
271
|
+
if (existingEntries.length === 0 || existingEntries[0].token == null) {
|
|
272
|
+
throw new Error('The item did not exist, no item was deleted.')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const existingToken = existingEntries[0].token
|
|
276
|
+
const inputs: CreateActionInput[] = [{
|
|
277
|
+
outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
|
|
278
|
+
unlockingScriptLength: 74,
|
|
279
|
+
inputDescription: 'KVStore token to remove'
|
|
280
|
+
}]
|
|
281
|
+
|
|
282
|
+
const pushdrop = new PushDrop(this.wallet, this.config.originator)
|
|
283
|
+
const { signableTransaction } = await this.wallet.createAction({
|
|
284
|
+
description: tokenRemovalDescription,
|
|
285
|
+
inputBEEF: existingToken.beef.toBinary(),
|
|
286
|
+
inputs,
|
|
287
|
+
outputs,
|
|
288
|
+
options: {
|
|
289
|
+
acceptDelayedBroadcast: this.config.acceptDelayedBroadcast
|
|
290
|
+
}
|
|
291
|
+
}, this.config.originator)
|
|
292
|
+
|
|
293
|
+
if (signableTransaction == null) {
|
|
294
|
+
throw new Error('Unable to create removal transaction')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
|
|
298
|
+
const unlocker = pushdrop.unlock(
|
|
299
|
+
protocolID ?? this.config.protocolID as WalletProtocol,
|
|
300
|
+
key,
|
|
301
|
+
'anyone'
|
|
302
|
+
)
|
|
303
|
+
const unlockingScript = await unlocker.sign(tx, 0)
|
|
304
|
+
|
|
305
|
+
const { tx: finalTx } = await this.wallet.signAction({
|
|
306
|
+
reference: signableTransaction.reference,
|
|
307
|
+
spends: { 0: { unlockingScript: unlockingScript.toHex() } }
|
|
308
|
+
}, this.config.originator)
|
|
309
|
+
|
|
310
|
+
if (finalTx == null) {
|
|
311
|
+
throw new Error('Unable to finalize removal transaction')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const transaction = Transaction.fromAtomicBEEF(finalTx)
|
|
315
|
+
await this.submitToOverlay(transaction)
|
|
316
|
+
return transaction.id('hex')
|
|
317
|
+
} finally {
|
|
318
|
+
if (lockQueue.length > 0) {
|
|
319
|
+
this.finishOperationOnKey(key, lockQueue)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Queues an operation on a specific key to ensure atomic updates.
|
|
326
|
+
* Prevents concurrent operations on the same key from interfering with each other.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} key - The key to queue an operation for.
|
|
329
|
+
* @returns {Promise<Array<(value: void | PromiseLike<void>) => void>>} The lock queue for cleanup.
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
private async queueOperationOnKey (key: string): Promise<Array<(value: void | PromiseLike<void>) => void>> {
|
|
333
|
+
// Check if a lock exists for this key and wait for it to resolve
|
|
334
|
+
let lockQueue = this.keyLocks.get(key)
|
|
335
|
+
if (lockQueue == null) {
|
|
336
|
+
lockQueue = []
|
|
337
|
+
this.keyLocks.set(key, lockQueue)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let resolveNewLock: () => void = () => { }
|
|
341
|
+
const newLock = new Promise<void>((resolve) => {
|
|
342
|
+
resolveNewLock = resolve
|
|
343
|
+
if (lockQueue != null) { lockQueue.push(resolve) }
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// If we are the only request, resolve the lock immediately, queue remains at 1 item until request ends.
|
|
347
|
+
if (lockQueue.length === 1) {
|
|
348
|
+
resolveNewLock()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await newLock
|
|
352
|
+
return lockQueue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Finishes an operation on a key and resolves the next waiting operation.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} key - The key to finish the operation for.
|
|
359
|
+
* @param {Array<(value: void | PromiseLike<void>) => void>} lockQueue - The lock queue from queueOperationOnKey.
|
|
360
|
+
* @private
|
|
361
|
+
*/
|
|
362
|
+
private finishOperationOnKey (key: string, lockQueue: Array<(value: void | PromiseLike<void>) => void>): void {
|
|
363
|
+
lockQueue.shift() // Remove the current lock from the queue
|
|
364
|
+
if (lockQueue.length > 0) {
|
|
365
|
+
// If there are more locks waiting, resolve the next one
|
|
366
|
+
lockQueue[0]()
|
|
367
|
+
} else {
|
|
368
|
+
// Clean up empty queue to prevent memory leak
|
|
369
|
+
this.keyLocks.delete(key)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Helper function to fetch and cache user identity key
|
|
375
|
+
*
|
|
376
|
+
* @returns {Promise<PubKeyHex>} The identity key of the current user
|
|
377
|
+
* @private
|
|
378
|
+
*/
|
|
379
|
+
private async getIdentityKey (): Promise<PubKeyHex> {
|
|
380
|
+
if (this.cachedIdentityKey == null) {
|
|
381
|
+
this.cachedIdentityKey = (await this.wallet.getPublicKey({ identityKey: true }, this.config.originator)).publicKey
|
|
382
|
+
}
|
|
383
|
+
return this.cachedIdentityKey
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Queries the overlay service for KV entries.
|
|
388
|
+
*
|
|
389
|
+
* @param {KVStoreQuery} query - Query parameters sent to overlay
|
|
390
|
+
* @param {KVStoreGetOptions} options - Configuration options for the query
|
|
391
|
+
* @returns {Promise<KVStoreEntry[]>} Array of matching KV entries
|
|
392
|
+
* @private
|
|
393
|
+
*/
|
|
394
|
+
private async queryOverlay (query: KVStoreQuery, options: KVStoreGetOptions = {}): Promise<KVStoreEntry[]> {
|
|
395
|
+
const answer = await this.lookupResolver.query({
|
|
396
|
+
service: options.serviceName ?? this.config.serviceName as string,
|
|
397
|
+
query
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
if (answer.type !== 'output-list' || answer.outputs.length === 0) {
|
|
401
|
+
return []
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const entries: KVStoreEntry[] = []
|
|
405
|
+
|
|
406
|
+
for (const result of answer.outputs) {
|
|
407
|
+
try {
|
|
408
|
+
const tx = Transaction.fromBEEF(result.beef)
|
|
409
|
+
const output = tx.outputs[result.outputIndex]
|
|
410
|
+
const decoded = PushDrop.decode(output.lockingScript)
|
|
411
|
+
|
|
412
|
+
if (decoded.fields.length !== 5) {
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Verify signature
|
|
417
|
+
const anyoneWallet = new ProtoWallet('anyone')
|
|
418
|
+
const signature = decoded.fields.pop() as number[]
|
|
419
|
+
try {
|
|
420
|
+
await anyoneWallet.verifySignature({
|
|
421
|
+
data: decoded.fields.reduce((a, e) => [...a, ...e], []),
|
|
422
|
+
signature,
|
|
423
|
+
counterparty: Utils.toHex(decoded.fields[kvProtocol.controller]),
|
|
424
|
+
protocolID: JSON.parse(Utils.toUTF8(decoded.fields[kvProtocol.protocolID])),
|
|
425
|
+
keyID: Utils.toUTF8(decoded.fields[kvProtocol.key])
|
|
426
|
+
})
|
|
427
|
+
} catch (error) {
|
|
428
|
+
// Skip all outputs that fail signature verification
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const entry: KVStoreEntry = {
|
|
433
|
+
key: Utils.toUTF8(decoded.fields[kvProtocol.key]),
|
|
434
|
+
value: Utils.toUTF8(decoded.fields[kvProtocol.value]),
|
|
435
|
+
controller: Utils.toHex(decoded.fields[kvProtocol.controller]),
|
|
436
|
+
protocolID: JSON.parse(Utils.toUTF8(decoded.fields[kvProtocol.protocolID]))
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (options.includeToken === true) {
|
|
440
|
+
entry.token = {
|
|
441
|
+
txid: tx.id('hex'),
|
|
442
|
+
outputIndex: result.outputIndex,
|
|
443
|
+
beef: Beef.fromBinary(result.beef),
|
|
444
|
+
satoshis: output.satoshis ?? 1
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (options.history === true) {
|
|
449
|
+
entry.history = await this.historian.buildHistory(tx, {
|
|
450
|
+
key: entry.key,
|
|
451
|
+
protocolID: entry.protocolID
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
entries.push(entry)
|
|
456
|
+
} catch (error) {
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return entries
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Submits a transaction to an overlay service using TopicBroadcaster.
|
|
466
|
+
* Broadcasts the transaction to the configured topics for network propagation.
|
|
467
|
+
*
|
|
468
|
+
* @param {Transaction} transaction - The transaction to broadcast.
|
|
469
|
+
* @returns {Promise<BroadcastResponse | BroadcastFailure>} The broadcast result.
|
|
470
|
+
* @throws {Error} If the broadcast fails or the network is unreachable.
|
|
471
|
+
* @private
|
|
472
|
+
*/
|
|
473
|
+
private async submitToOverlay (transaction: Transaction): Promise<BroadcastResponse | BroadcastFailure> {
|
|
474
|
+
return await this.topicBroadcaster.broadcast(transaction)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export default GlobalKVStore
|
|
@@ -80,7 +80,7 @@ export default class LocalKVStore {
|
|
|
80
80
|
this.keyLocks.set(key, lockQueue)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
let resolveNewLock: () => void = () => {}
|
|
83
|
+
let resolveNewLock: () => void = () => { }
|
|
84
84
|
const newLock = new Promise<void>((resolve) => {
|
|
85
85
|
resolveNewLock = resolve
|
|
86
86
|
if (lockQueue != null) { lockQueue.push(resolve) }
|
|
@@ -166,8 +166,8 @@ export default class LocalKVStore {
|
|
|
166
166
|
throw new Error('Invalid token.')
|
|
167
167
|
}
|
|
168
168
|
field = decoded.fields[0]
|
|
169
|
-
} catch (
|
|
170
|
-
throw new Error(`Invalid value found. You need to call set to collapse the corrupted state (or relinquish the corrupted ${outputs[0].outpoint} output from the ${this.context} basket) before you can get this value again
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new Error(`Invalid value found. You need to call set to collapse the corrupted state (or relinquish the corrupted ${outputs[0].outpoint} output from the ${this.context} basket) before you can get this value again. Original error: ${error instanceof Error ? error.message : String(error)}`)
|
|
171
171
|
}
|
|
172
172
|
if (!this.encrypt) {
|
|
173
173
|
r.value = Utils.toUTF8(field)
|
|
@@ -288,8 +288,8 @@ export default class LocalKVStore {
|
|
|
288
288
|
})
|
|
289
289
|
outpoint = `${txid as string}.0`
|
|
290
290
|
}
|
|
291
|
-
} catch (
|
|
292
|
-
throw new Error(`There are ${outputs.length} outputs with tag ${key} that cannot be unlocked
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error(`There are ${outputs.length} outputs with tag ${key} that cannot be unlocked. Original error: ${error instanceof Error ? error.message : String(error)}`)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
return outpoint
|
|
@@ -337,8 +337,8 @@ export default class LocalKVStore {
|
|
|
337
337
|
})
|
|
338
338
|
if (txid === undefined) { throw new Error('signAction must return a valid txid') }
|
|
339
339
|
txids.push(txid)
|
|
340
|
-
} catch (
|
|
341
|
-
throw new Error(`There are ${totalOutputs} outputs with tag ${key} that cannot be unlocked
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new Error(`There are ${totalOutputs} outputs with tag ${key} that cannot be unlocked. Original error: ${error instanceof Error ? error.message : String(error)}`)
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
if (outputs.length === totalOutputs) { break }
|