@bsv/sdk 1.4.15 → 1.4.18

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 (64) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +9 -9
  4. package/dist/cjs/src/kvstore/LocalKVStore.js +279 -0
  5. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -0
  6. package/dist/cjs/src/kvstore/index.js +9 -0
  7. package/dist/cjs/src/kvstore/index.js.map +1 -0
  8. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js +29 -0
  9. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  10. package/dist/cjs/src/wallet/WalletError.js +4 -3
  11. package/dist/cjs/src/wallet/WalletError.js.map +1 -1
  12. package/dist/cjs/src/wallet/index.js +4 -1
  13. package/dist/cjs/src/wallet/index.js.map +1 -1
  14. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  15. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/mod.js +1 -0
  18. package/dist/esm/mod.js.map +1 -1
  19. package/dist/esm/src/kvstore/LocalKVStore.js +273 -0
  20. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -0
  21. package/dist/esm/src/kvstore/index.js +2 -0
  22. package/dist/esm/src/kvstore/index.js.map +1 -0
  23. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js +31 -0
  24. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  25. package/dist/esm/src/wallet/WalletError.js +3 -2
  26. package/dist/esm/src/wallet/WalletError.js.map +1 -1
  27. package/dist/esm/src/wallet/index.js +2 -0
  28. package/dist/esm/src/wallet/index.js.map +1 -1
  29. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  30. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/mod.d.ts +1 -0
  33. package/dist/types/mod.d.ts.map +1 -1
  34. package/dist/types/src/kvstore/LocalKVStore.d.ts +85 -0
  35. package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -0
  36. package/dist/types/src/kvstore/index.d.ts +2 -0
  37. package/dist/types/src/kvstore/index.d.ts.map +1 -0
  38. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts +23 -0
  39. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts.map +1 -0
  40. package/dist/types/src/wallet/Wallet.interfaces.d.ts +22 -0
  41. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  42. package/dist/types/src/wallet/WalletError.d.ts +4 -3
  43. package/dist/types/src/wallet/WalletError.d.ts.map +1 -1
  44. package/dist/types/src/wallet/index.d.ts +1 -0
  45. package/dist/types/src/wallet/index.d.ts.map +1 -1
  46. package/dist/types/src/wallet/substrates/HTTPWalletJSON.d.ts.map +1 -1
  47. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  48. package/dist/umd/bundle.js +1 -1
  49. package/docs/identity.md +225 -0
  50. package/docs/kvstore.md +133 -0
  51. package/docs/registry.md +383 -0
  52. package/docs/transaction.md +3 -3
  53. package/docs/wallet.md +146 -38
  54. package/mod.ts +2 -1
  55. package/package.json +19 -9
  56. package/src/kvstore/LocalKVStore.ts +287 -0
  57. package/src/kvstore/__tests/LocalKVStore.test.ts +614 -0
  58. package/src/kvstore/index.ts +1 -0
  59. package/src/wallet/WERR_REVIEW_ACTIONS.ts +30 -0
  60. package/src/wallet/Wallet.interfaces.ts +24 -0
  61. package/src/wallet/WalletError.ts +4 -2
  62. package/src/wallet/index.ts +2 -0
  63. package/src/wallet/substrates/HTTPWalletJSON.ts +12 -6
  64. package/docs/wallet-substrates.md +0 -1194
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.4.15",
3
+ "version": "1.4.18",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -194,6 +194,16 @@
194
194
  "require": "./dist/cjs/src/storage/*.js",
195
195
  "types": "./dist/types/src/storage/*.d.ts"
196
196
  },
197
+ "./kvstore": {
198
+ "import": "./dist/esm/src/kvstore/index.js",
199
+ "require": "./dist/cjs/src/kvstore/index.js",
200
+ "types": "./dist/types/src/kvstore/index.d.ts"
201
+ },
202
+ "./kvstore/*": {
203
+ "import": "./dist/esm/src/kvstore/*.js",
204
+ "require": "./dist/cjs/src/kvstore/*.js",
205
+ "types": "./dist/types/src/kvstore/*.d.ts"
206
+ },
197
207
  "./umd": {
198
208
  "import": "./dist/umd/bundle.js"
199
209
  }
@@ -229,23 +239,23 @@
229
239
  },
230
240
  "homepage": "https://github.com/bitcoin-sv/ts-sdk#readme",
231
241
  "devDependencies": {
232
- "@eslint/js": "^9.19.0",
242
+ "@eslint/js": "^9.23.0",
233
243
  "@jest/globals": "^29.7.0",
234
244
  "@types/jest": "^29.5.14",
235
245
  "@types/node": "^22.13.14",
236
- "eslint": "^8.57.1",
237
- "globals": "^15.14.0",
246
+ "eslint": "^9.23.0",
247
+ "globals": "^16.0.0",
238
248
  "jest": "^29.7.0",
239
249
  "jest-environment-jsdom": "^29.7.0",
240
- "ts-jest": "^29.1.1",
241
- "ts-loader": "^9.5.1",
250
+ "ts-jest": "^29.3.1",
251
+ "ts-loader": "^9.5.2",
242
252
  "ts-standard": "^12.0.2",
243
253
  "ts2md": "^0.2.8",
244
254
  "tsconfig-to-dual-package": "^1.2.0",
245
255
  "typescript": "5.1",
246
- "typescript-eslint": "^8.22.0",
247
- "webpack": "^5.95.0",
248
- "webpack-cli": "^5.1.4"
256
+ "typescript-eslint": "^8.29.0",
257
+ "webpack": "^5.98.0",
258
+ "webpack-cli": "^6.0.1"
249
259
  },
250
260
  "ts-standard": {
251
261
  "project": "tsconfig.eslint.json",
@@ -0,0 +1,287 @@
1
+ import LockingScript from '../script/LockingScript.js'
2
+ import PushDrop from '../script/templates/PushDrop.js'
3
+ import * as Utils from '../primitives/utils.js'
4
+ import { WalletInterface, OutpointString, CreateActionInput, SignActionSpend, WalletProtocol, ListOutputsResult, WalletOutput, AtomicBEEF } from '../wallet/Wallet.interfaces.js'
5
+ import WalletClient from '../wallet/WalletClient.js'
6
+ import Transaction from '../transaction/Transaction.js'
7
+ import { Beef } from '../transaction/Beef.js'
8
+
9
+ /**
10
+ * Implements a key-value storage system backed by transaction outputs managed by a wallet.
11
+ * Each key-value pair is represented by a PushDrop token output in a specific context (basket).
12
+ * Allows setting, getting, and removing key-value pairs, with optional encryption.
13
+ */
14
+ export default class LocalKVStore {
15
+ /**
16
+ * The wallet interface used to manage outputs and perform cryptographic operations.
17
+ * @private
18
+ * @readonly
19
+ */
20
+ private readonly wallet: WalletInterface
21
+ /**
22
+ * The context (basket name) used to namespace the key-value pairs within the wallet.
23
+ * @private
24
+ * @readonly
25
+ */
26
+ private readonly context: string
27
+ /**
28
+ * Flag indicating whether values should be encrypted before storing.
29
+ * @private
30
+ * @readonly
31
+ */
32
+ private readonly encrypt: boolean
33
+
34
+ /**
35
+ * An originator to use with PushDrop and the wallet.
36
+ * @private
37
+ * @readonly
38
+ */
39
+ private readonly originator?: string
40
+
41
+ /**
42
+ * Creates an instance of the localKVStore.
43
+ *
44
+ * @param {WalletInterface} [wallet=new WalletClient()] - The wallet interface to use. Defaults to a new WalletClient instance.
45
+ * @param {string} [context='kvstoredefault'] - The context (basket) for namespacing keys. Defaults to 'kvstore default'.
46
+ * @param {boolean} [encrypt=true] - Whether to encrypt values. Defaults to true.
47
+ * @param {string} [originator] — An originator to use with PushDrop and the wallet, if provided.
48
+ * @throws {Error} If the context is missing or empty.
49
+ */
50
+ constructor (
51
+ wallet: WalletInterface = new WalletClient(),
52
+ context = 'kvstore default',
53
+ encrypt = true,
54
+ originator?: string
55
+ ) {
56
+ if (typeof context !== 'string' || context.length < 1) {
57
+ throw new Error('A context in which to operate is required.')
58
+ }
59
+ this.wallet = wallet
60
+ this.context = context
61
+ this.encrypt = encrypt
62
+ this.originator = originator
63
+ }
64
+
65
+ private getProtocol (key: string): { protocolID: WalletProtocol, keyID: string } {
66
+ return { protocolID: [2, this.context], keyID: key }
67
+ }
68
+
69
+ private async getOutputs (key: string, limit?: number): Promise<ListOutputsResult> {
70
+ const results = await this.wallet.listOutputs({
71
+ basket: this.context,
72
+ tags: [key],
73
+ tagQueryMode: 'all',
74
+ include: 'entire transactions',
75
+ limit
76
+ })
77
+ return results
78
+ }
79
+
80
+ /**
81
+ * Retrieves the value associated with a given key.
82
+ *
83
+ * @param {string} key - The key to retrieve the value for.
84
+ * @param {string | undefined} [defaultValue=undefined] - The value to return if the key is not found.
85
+ * @returns {Promise<string | undefined>} A promise that resolves to the value as a string,
86
+ * the defaultValue if the key is not found, or undefined if no defaultValue is provided.
87
+ * @throws {Error} If too many outputs are found for the key (ambiguous state).
88
+ * @throws {Error} If the found output's locking script cannot be decoded or represents an invalid token format.
89
+ */
90
+ async get (key: string, defaultValue: string | undefined = undefined): Promise<string | undefined> {
91
+ const r = await this.lookupValue(key, defaultValue, 5)
92
+ return r.value
93
+ }
94
+
95
+ private getLockingScript (output: WalletOutput, beef: Beef): LockingScript {
96
+ const [txid, vout] = output.outpoint.split('.')
97
+ const tx = beef.findTxid(txid)?.tx
98
+ if (tx == null) { throw new Error(`beef must contain txid ${txid}`) }
99
+ const lockingScript = tx.outputs[Number(vout)].lockingScript
100
+ return lockingScript
101
+ }
102
+
103
+ private async lookupValue (key: string, defaultValue: string | undefined, limit?: number): Promise<LookupValueResult> {
104
+ const lor = await this.getOutputs(key, limit)
105
+ const r: LookupValueResult = { value: defaultValue, outpoint: undefined, lor }
106
+ const { outputs } = lor
107
+ if (outputs.length === 0) {
108
+ return r
109
+ }
110
+ const output = outputs.slice(-1)[0]
111
+ r.outpoint = output.outpoint
112
+ let field: number[]
113
+ try {
114
+ if (lor.BEEF === undefined) { throw new Error('entire transactions listOutputs option must return valid BEEF') }
115
+ const lockingScript = this.getLockingScript(output, Beef.fromBinary(lor.BEEF))
116
+ const decoded = PushDrop.decode(lockingScript)
117
+ if (decoded.fields.length < 1 || decoded.fields.length > 2) {
118
+ throw new Error('Invalid token.')
119
+ }
120
+ field = decoded.fields[0]
121
+ } catch (_) {
122
+ 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.`)
123
+ }
124
+ if (!this.encrypt) {
125
+ r.value = Utils.toUTF8(field)
126
+ } else {
127
+ const { plaintext } = await this.wallet.decrypt({
128
+ ...this.getProtocol(key),
129
+ ciphertext: field
130
+ })
131
+ r.value = Utils.toUTF8(plaintext)
132
+ }
133
+ return r
134
+ }
135
+
136
+ private getInputs (outputs: WalletOutput[]): CreateActionInput[] {
137
+ const inputs: CreateActionInput[] = []
138
+ for (let i = 0; i < outputs.length; i++) {
139
+ inputs.push({
140
+ outpoint: outputs[i].outpoint,
141
+ unlockingScriptLength: 74,
142
+ inputDescription: 'Previous key-value token'
143
+ })
144
+ }
145
+ return inputs
146
+ }
147
+
148
+ private async getSpends (key: string, outputs: WalletOutput[], pushdrop: PushDrop, atomicBEEF: AtomicBEEF): Promise<Record<number, SignActionSpend>> {
149
+ const p = this.getProtocol(key)
150
+ const tx = Transaction.fromAtomicBEEF(atomicBEEF)
151
+ const spends: Record<number, SignActionSpend> = {}
152
+ for (let i = 0; i < outputs.length; i++) {
153
+ const unlocker = pushdrop.unlock(p.protocolID, p.keyID, 'self')
154
+ const unlockingScript = await unlocker.sign(tx, i)
155
+ spends[i] = {
156
+ unlockingScript: unlockingScript.toHex()
157
+ }
158
+ }
159
+ return spends
160
+ }
161
+
162
+ /**
163
+ * Sets or updates the value associated with a given key.
164
+ * If the key already exists (one or more outputs found), it spends the existing output(s)
165
+ * and creates a new one with the updated value. If multiple outputs exist for the key,
166
+ * they are collapsed into a single new output.
167
+ * If the key does not exist, it creates a new output.
168
+ * Handles encryption if enabled.
169
+ * If signing the update/collapse transaction fails, it relinquishes the original outputs and starts over with a new chain.
170
+ *
171
+ * @param {string} key - The key to set or update.
172
+ * @param {string} value - The value to associate with the key.
173
+ * @returns {Promise<OutpointString>} A promise that resolves to the outpoint string (txid.vout) of the new or updated token output.
174
+ */
175
+ async set (key: string, value: string): Promise<OutpointString> {
176
+ const current = await this.lookupValue(key, undefined, 10)
177
+ if (current.value === value) {
178
+ if (current.outpoint === undefined) { throw new Error('outpoint must be valid when value is valid and unchanged') }
179
+ // Don't create a new transaction if the value doesn't need to change...
180
+ return current.outpoint
181
+ }
182
+ const protocol = this.getProtocol(key)
183
+ let valueAsArray = Utils.toArray(value, 'utf8')
184
+ if (this.encrypt) {
185
+ const { ciphertext } = await this.wallet.encrypt({
186
+ ...protocol,
187
+ plaintext: valueAsArray
188
+ })
189
+ valueAsArray = ciphertext
190
+ }
191
+ const pushdrop = new PushDrop(this.wallet, this.originator)
192
+ const lockingScript = await pushdrop.lock(
193
+ [valueAsArray],
194
+ protocol.protocolID,
195
+ protocol.keyID,
196
+ 'self'
197
+ )
198
+ const { outputs, BEEF: inputBEEF } = current.lor
199
+ let outpoint: OutpointString
200
+ try {
201
+ const inputs = this.getInputs(outputs)
202
+ const { txid, signableTransaction } = await this.wallet.createAction({
203
+ description: `Update ${key} in ${this.context}`,
204
+ inputBEEF,
205
+ inputs,
206
+ outputs: [{
207
+ basket: this.context,
208
+ tags: [key],
209
+ lockingScript: lockingScript.toHex(),
210
+ satoshis: 1,
211
+ outputDescription: 'Key-value token'
212
+ }],
213
+ options: {
214
+ acceptDelayedBroadcast: false,
215
+ randomizeOutputs: false
216
+ }
217
+ })
218
+ if (outputs.length > 0 && typeof signableTransaction !== 'object') {
219
+ throw new Error('Wallet did not return a signable transaction when expected.')
220
+ }
221
+ if (signableTransaction == null) {
222
+ outpoint = `${txid as string}.0`
223
+ } else {
224
+ const spends = await this.getSpends(key, outputs, pushdrop, signableTransaction.tx)
225
+ const { txid } = await this.wallet.signAction({
226
+ reference: signableTransaction.reference,
227
+ spends
228
+ })
229
+ outpoint = `${txid as string}.0`
230
+ }
231
+ } catch (_) {
232
+ throw new Error(`There are ${outputs.length} outputs with tag ${key} that cannot be unlocked.`)
233
+ }
234
+ return outpoint
235
+ }
236
+
237
+ /**
238
+ * Removes the key-value pair associated with the given key.
239
+ * It finds the existing output(s) for the key and spends them without creating a new output.
240
+ * If multiple outputs exist, they are all spent in the same transaction.
241
+ * If the key does not exist, it does nothing.
242
+ * If signing the removal transaction fails, it relinquishes the original outputs instead of spending.
243
+ *
244
+ * @param {string} key - The key to remove.
245
+ * @returns {Promise<string[]>} A promise that resolves to the txids of the removal transactions if successful.
246
+ */
247
+ async remove (key: string): Promise<string[]> {
248
+ const txids: string[] = []
249
+ for (; ;) {
250
+ const { outputs, BEEF: inputBEEF, totalOutputs } = await this.getOutputs(key)
251
+ if (outputs.length > 0) {
252
+ const pushdrop = new PushDrop(this.wallet, this.originator)
253
+ try {
254
+ const inputs = this.getInputs(outputs)
255
+ const { signableTransaction } = await this.wallet.createAction({
256
+ description: `Remove ${key} in ${this.context}`,
257
+ inputBEEF,
258
+ inputs,
259
+ options: {
260
+ acceptDelayedBroadcast: false
261
+ }
262
+ })
263
+ if (typeof signableTransaction !== 'object') {
264
+ throw new Error('Wallet did not return a signable transaction when expected.')
265
+ }
266
+ const spends = await this.getSpends(key, outputs, pushdrop, signableTransaction.tx)
267
+ const { txid } = await this.wallet.signAction({
268
+ reference: signableTransaction.reference,
269
+ spends
270
+ })
271
+ if (txid === undefined) { throw new Error('signAction must return a valid txid') }
272
+ txids.push(txid)
273
+ } catch (_) {
274
+ throw new Error(`There are ${totalOutputs} outputs with tag ${key} that cannot be unlocked.`)
275
+ }
276
+ }
277
+ if (outputs.length === totalOutputs) { break }
278
+ }
279
+ return txids
280
+ }
281
+ }
282
+
283
+ interface LookupValueResult {
284
+ value: string | undefined
285
+ outpoint: OutpointString | undefined
286
+ lor: ListOutputsResult
287
+ }