@bsv/sdk 1.7.7 → 1.8.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 (94) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/GlobalKVStore.js +420 -0
  3. package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -0
  4. package/dist/cjs/src/kvstore/LocalKVStore.js +6 -6
  5. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
  6. package/dist/cjs/src/kvstore/kvStoreInterpreter.js +74 -0
  7. package/dist/cjs/src/kvstore/kvStoreInterpreter.js.map +1 -0
  8. package/dist/cjs/src/kvstore/types.js +11 -0
  9. package/dist/cjs/src/kvstore/types.js.map +1 -0
  10. package/dist/cjs/src/overlay-tools/Historian.js +153 -0
  11. package/dist/cjs/src/overlay-tools/Historian.js.map +1 -0
  12. package/dist/cjs/src/script/templates/PushDrop.js +2 -2
  13. package/dist/cjs/src/script/templates/PushDrop.js.map +1 -1
  14. package/dist/cjs/src/transaction/Transaction.js +4 -4
  15. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  16. package/dist/cjs/src/transaction/fee-models/LivePolicy.js +90 -0
  17. package/dist/cjs/src/transaction/fee-models/LivePolicy.js.map +1 -0
  18. package/dist/cjs/src/transaction/fee-models/index.js +3 -1
  19. package/dist/cjs/src/transaction/fee-models/index.js.map +1 -1
  20. package/dist/cjs/src/wallet/WalletClient.js +43 -52
  21. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  22. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +19 -0
  23. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  24. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  25. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/esm/src/kvstore/GlobalKVStore.js +416 -0
  28. package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -0
  29. package/dist/esm/src/kvstore/LocalKVStore.js +6 -6
  30. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
  31. package/dist/esm/src/kvstore/kvStoreInterpreter.js +47 -0
  32. package/dist/esm/src/kvstore/kvStoreInterpreter.js.map +1 -0
  33. package/dist/esm/src/kvstore/types.js +8 -0
  34. package/dist/esm/src/kvstore/types.js.map +1 -0
  35. package/dist/esm/src/overlay-tools/Historian.js +155 -0
  36. package/dist/esm/src/overlay-tools/Historian.js.map +1 -0
  37. package/dist/esm/src/script/templates/PushDrop.js +2 -2
  38. package/dist/esm/src/script/templates/PushDrop.js.map +1 -1
  39. package/dist/esm/src/transaction/Transaction.js +4 -4
  40. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  41. package/dist/esm/src/transaction/fee-models/LivePolicy.js +85 -0
  42. package/dist/esm/src/transaction/fee-models/LivePolicy.js.map +1 -0
  43. package/dist/esm/src/transaction/fee-models/index.js +1 -0
  44. package/dist/esm/src/transaction/fee-models/index.js.map +1 -1
  45. package/dist/esm/src/wallet/WalletClient.js +43 -52
  46. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  47. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +19 -0
  48. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  49. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  50. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  51. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  52. package/dist/types/src/kvstore/GlobalKVStore.d.ts +129 -0
  53. package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -0
  54. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts +22 -0
  55. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts.map +1 -0
  56. package/dist/types/src/kvstore/types.d.ts +106 -0
  57. package/dist/types/src/kvstore/types.d.ts.map +1 -0
  58. package/dist/types/src/overlay-tools/Historian.d.ts +92 -0
  59. package/dist/types/src/overlay-tools/Historian.d.ts.map +1 -0
  60. package/dist/types/src/script/templates/PushDrop.d.ts +6 -5
  61. package/dist/types/src/script/templates/PushDrop.d.ts.map +1 -1
  62. package/dist/types/src/transaction/Transaction.d.ts +2 -2
  63. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  64. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts +41 -0
  65. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts.map +1 -0
  66. package/dist/types/src/transaction/fee-models/index.d.ts +1 -0
  67. package/dist/types/src/transaction/fee-models/index.d.ts.map +1 -1
  68. package/dist/types/src/wallet/WalletClient.d.ts +1 -1
  69. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  70. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -1
  71. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -1
  72. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  73. package/dist/umd/bundle.js +3 -3
  74. package/dist/umd/bundle.js.map +1 -1
  75. package/docs/reference/script.md +7 -19
  76. package/docs/reference/transaction.md +75 -6
  77. package/docs/reference/wallet.md +1 -1
  78. package/package.json +1 -1
  79. package/src/kvstore/GlobalKVStore.ts +478 -0
  80. package/src/kvstore/LocalKVStore.ts +7 -7
  81. package/src/kvstore/__tests/GlobalKVStore.test.ts +965 -0
  82. package/src/kvstore/__tests/LocalKVStore.test.ts +72 -0
  83. package/src/kvstore/kvStoreInterpreter.ts +49 -0
  84. package/src/kvstore/types.ts +114 -0
  85. package/src/overlay-tools/Historian.ts +195 -0
  86. package/src/overlay-tools/__tests/Historian.test.ts +690 -0
  87. package/src/script/templates/PushDrop.ts +6 -5
  88. package/src/transaction/Transaction.ts +4 -4
  89. package/src/transaction/fee-models/LivePolicy.ts +97 -0
  90. package/src/transaction/fee-models/__tests/LivePolicy.test.ts +148 -0
  91. package/src/transaction/fee-models/index.ts +1 -0
  92. package/src/wallet/WalletClient.ts +50 -51
  93. package/src/wallet/substrates/WalletWireProcessor.ts +21 -0
  94. package/src/wallet/substrates/WalletWireTransceiver.ts +22 -10
@@ -218,21 +218,15 @@ export default class PushDrop implements ScriptTemplate {
218
218
  fields: number[][];
219
219
  }
220
220
  constructor(wallet: WalletInterface, originator?: string)
221
- async lock(fields: number[][], protocolID: [
222
- SecurityLevel,
223
- string
224
- ], keyID: string, counterparty: string, forSelf = false, includeSignature = true, lockPosition: "before" | "after" = "before"): Promise<LockingScript>
225
- unlock(protocolID: [
226
- SecurityLevel,
227
- string
228
- ], keyID: string, counterparty: string, signOutputs: "all" | "none" | "single" = "all", anyoneCanPay = false, sourceSatoshis?: number, lockingScript?: LockingScript): {
221
+ async lock(fields: number[][], protocolID: WalletProtocol, keyID: string, counterparty: string, forSelf = false, includeSignature = true, lockPosition: "before" | "after" = "before"): Promise<LockingScript>
222
+ unlock(protocolID: WalletProtocol, keyID: string, counterparty: string, signOutputs: "all" | "none" | "single" = "all", anyoneCanPay = false, sourceSatoshis?: number, lockingScript?: LockingScript): {
229
223
  sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
230
224
  estimateLength: () => Promise<73>;
231
225
  }
232
226
  }
233
227
  ```
234
228
 
235
- See also: [LockingScript](./script.md#class-lockingscript), [PublicKey](./primitives.md#class-publickey), [ScriptTemplate](./script.md#interface-scripttemplate), [SecurityLevel](./wallet.md#type-securitylevel), [Transaction](./transaction.md#class-transaction), [UnlockingScript](./script.md#class-unlockingscript), [WalletInterface](./wallet.md#interface-walletinterface), [sign](./compat.md#variable-sign)
229
+ See also: [LockingScript](./script.md#class-lockingscript), [PublicKey](./primitives.md#class-publickey), [ScriptTemplate](./script.md#interface-scripttemplate), [Transaction](./transaction.md#class-transaction), [UnlockingScript](./script.md#class-unlockingscript), [WalletInterface](./wallet.md#interface-walletinterface), [WalletProtocol](./wallet.md#type-walletprotocol), [sign](./compat.md#variable-sign)
236
230
 
237
231
  #### Constructor
238
232
 
@@ -277,12 +271,9 @@ Argument Details
277
271
  Creates a PushDrop locking script with arbitrary data fields and a public key lock.
278
272
 
279
273
  ```ts
280
- async lock(fields: number[][], protocolID: [
281
- SecurityLevel,
282
- string
283
- ], keyID: string, counterparty: string, forSelf = false, includeSignature = true, lockPosition: "before" | "after" = "before"): Promise<LockingScript>
274
+ async lock(fields: number[][], protocolID: WalletProtocol, keyID: string, counterparty: string, forSelf = false, includeSignature = true, lockPosition: "before" | "after" = "before"): Promise<LockingScript>
284
275
  ```
285
- See also: [LockingScript](./script.md#class-lockingscript), [SecurityLevel](./wallet.md#type-securitylevel)
276
+ See also: [LockingScript](./script.md#class-lockingscript), [WalletProtocol](./wallet.md#type-walletprotocol)
286
277
 
287
278
  Returns
288
279
 
@@ -308,15 +299,12 @@ Argument Details
308
299
  Creates an unlocking script for spending a PushDrop token output.
309
300
 
310
301
  ```ts
311
- unlock(protocolID: [
312
- SecurityLevel,
313
- string
314
- ], keyID: string, counterparty: string, signOutputs: "all" | "none" | "single" = "all", anyoneCanPay = false, sourceSatoshis?: number, lockingScript?: LockingScript): {
302
+ unlock(protocolID: WalletProtocol, keyID: string, counterparty: string, signOutputs: "all" | "none" | "single" = "all", anyoneCanPay = false, sourceSatoshis?: number, lockingScript?: LockingScript): {
315
303
  sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
316
304
  estimateLength: () => Promise<73>;
317
305
  }
318
306
  ```
319
- See also: [LockingScript](./script.md#class-lockingscript), [SecurityLevel](./wallet.md#type-securitylevel), [Transaction](./transaction.md#class-transaction), [UnlockingScript](./script.md#class-unlockingscript), [sign](./compat.md#variable-sign)
307
+ See also: [LockingScript](./script.md#class-lockingscript), [Transaction](./transaction.md#class-transaction), [UnlockingScript](./script.md#class-unlockingscript), [WalletProtocol](./wallet.md#type-walletprotocol), [sign](./compat.md#variable-sign)
320
308
 
321
309
  Returns
322
310
 
@@ -456,6 +456,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
456
456
  | [BeefParty](#class-beefparty) |
457
457
  | [BeefTx](#class-beeftx) |
458
458
  | [FetchHttpClient](#class-fetchhttpclient) |
459
+ | [LivePolicy](#class-livepolicy) |
459
460
  | [MerklePath](#class-merklepath) |
460
461
  | [NodejsHttpClient](#class-nodejshttpclient) |
461
462
  | [SatoshisPerKilobyte](#class-satoshisperkilobyte) |
@@ -1198,6 +1199,74 @@ See also: [Fetch](./transaction.md#type-fetch), [HttpClient](./transaction.md#in
1198
1199
 
1199
1200
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1200
1201
 
1202
+ ---
1203
+ ### Class: LivePolicy
1204
+
1205
+ Represents a live fee policy that fetches current rates from ARC GorillaPool.
1206
+ Extends SatoshisPerKilobyte to reuse transaction size calculation logic.
1207
+
1208
+ ```ts
1209
+ export default class LivePolicy extends SatoshisPerKilobyte {
1210
+ constructor(cacheValidityMs: number = 5 * 60 * 1000)
1211
+ static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
1212
+ async computeFee(tx: Transaction): Promise<number>
1213
+ }
1214
+ ```
1215
+
1216
+ See also: [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte), [Transaction](./transaction.md#class-transaction)
1217
+
1218
+ #### Constructor
1219
+
1220
+ Constructs an instance of the live policy fee model.
1221
+
1222
+ ```ts
1223
+ constructor(cacheValidityMs: number = 5 * 60 * 1000)
1224
+ ```
1225
+
1226
+ Argument Details
1227
+
1228
+ + **cacheValidityMs**
1229
+ + How long to cache the fee rate in milliseconds (default: 5 minutes)
1230
+
1231
+ #### Method computeFee
1232
+
1233
+ Computes the fee for a given transaction using the current live rate.
1234
+ Overrides the parent method to use dynamic rate fetching.
1235
+
1236
+ ```ts
1237
+ async computeFee(tx: Transaction): Promise<number>
1238
+ ```
1239
+ See also: [Transaction](./transaction.md#class-transaction)
1240
+
1241
+ Returns
1242
+
1243
+ The fee in satoshis for the transaction.
1244
+
1245
+ Argument Details
1246
+
1247
+ + **tx**
1248
+ + The transaction for which a fee is to be computed.
1249
+
1250
+ #### Method getInstance
1251
+
1252
+ Gets the singleton instance of LivePolicy to ensure cache sharing across the application.
1253
+
1254
+ ```ts
1255
+ static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
1256
+ ```
1257
+ See also: [LivePolicy](./transaction.md#class-livepolicy)
1258
+
1259
+ Returns
1260
+
1261
+ The singleton LivePolicy instance
1262
+
1263
+ Argument Details
1264
+
1265
+ + **cacheValidityMs**
1266
+ + How long to cache the fee rate in milliseconds (default: 5 minutes)
1267
+
1268
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1269
+
1201
1270
  ---
1202
1271
  ### Class: MerklePath
1203
1272
 
@@ -1520,7 +1589,7 @@ export default class Transaction {
1520
1589
  addOutput(output: TransactionOutput): void
1521
1590
  addP2PKHOutput(address: number[] | string, satoshis?: number): void
1522
1591
  updateMetadata(metadata: Record<string, any>): void
1523
- async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1592
+ async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1524
1593
  getFee(): number
1525
1594
  async sign(): Promise<void>
1526
1595
  async broadcast(broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure>
@@ -1540,7 +1609,7 @@ export default class Transaction {
1540
1609
  }
1541
1610
  ```
1542
1611
 
1543
- See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)
1612
+ See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)
1544
1613
 
1545
1614
  #### Method addInput
1546
1615
 
@@ -1610,13 +1679,13 @@ Argument Details
1610
1679
  #### Method fee
1611
1680
 
1612
1681
  Computes fees prior to signing.
1613
- If no fee model is provided, uses a SatoshisPerKilobyte fee model that pays 1 sat/kb.
1682
+ If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
1614
1683
  If fee is a number, the transaction uses that value as fee.
1615
1684
 
1616
1685
  ```ts
1617
- async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1686
+ async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1618
1687
  ```
1619
- See also: [FeeModel](./transaction.md#interface-feemodel), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte)
1688
+ See also: [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy)
1620
1689
 
1621
1690
  Argument Details
1622
1691
 
@@ -2037,7 +2106,7 @@ Argument Details
2037
2106
  Example
2038
2107
 
2039
2108
  ```ts
2040
- tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
2109
+ tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
2041
2110
  ```
2042
2111
 
2043
2112
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
@@ -2629,7 +2629,7 @@ The SDK is how applications communicate with wallets over a communications subst
2629
2629
  export default class WalletClient implements WalletInterface {
2630
2630
  public substrate: "auto" | WalletInterface;
2631
2631
  originator?: OriginatorDomainNameStringUnder250Bytes;
2632
- constructor(substrate: "auto" | "Cicada" | "XDM" | "window.CWI" | "json-api" | "react-native" | WalletInterface = "auto", originator?: OriginatorDomainNameStringUnder250Bytes)
2632
+ constructor(substrate: "auto" | "Cicada" | "XDM" | "window.CWI" | "json-api" | "react-native" | "secure-json-api" | WalletInterface = "auto", originator?: OriginatorDomainNameStringUnder250Bytes)
2633
2633
  async connectToSubstrate(): Promise<void>
2634
2634
  async createAction(args: CreateActionArgs): Promise<CreateActionResult>
2635
2635
  async signAction(args: SignActionArgs): Promise<SignActionResult>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.7.7",
3
+ "version": "1.8.1",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -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