@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.
- package/dist/cjs/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +9 -9
- package/dist/cjs/src/kvstore/LocalKVStore.js +279 -0
- package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -0
- package/dist/cjs/src/kvstore/index.js +9 -0
- package/dist/cjs/src/kvstore/index.js.map +1 -0
- package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js +29 -0
- package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
- package/dist/cjs/src/wallet/WalletError.js +4 -3
- package/dist/cjs/src/wallet/WalletError.js.map +1 -1
- package/dist/cjs/src/wallet/index.js +4 -1
- package/dist/cjs/src/wallet/index.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js +13 -6
- package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/kvstore/LocalKVStore.js +273 -0
- package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -0
- package/dist/esm/src/kvstore/index.js +2 -0
- package/dist/esm/src/kvstore/index.js.map +1 -0
- package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js +31 -0
- package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
- package/dist/esm/src/wallet/WalletError.js +3 -2
- package/dist/esm/src/wallet/WalletError.js.map +1 -1
- package/dist/esm/src/wallet/index.js +2 -0
- package/dist/esm/src/wallet/index.js.map +1 -1
- package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js +13 -6
- package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/kvstore/LocalKVStore.d.ts +85 -0
- package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -0
- package/dist/types/src/kvstore/index.d.ts +2 -0
- package/dist/types/src/kvstore/index.d.ts.map +1 -0
- package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts +23 -0
- package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts.map +1 -0
- package/dist/types/src/wallet/Wallet.interfaces.d.ts +22 -0
- package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
- package/dist/types/src/wallet/WalletError.d.ts +4 -3
- package/dist/types/src/wallet/WalletError.d.ts.map +1 -1
- package/dist/types/src/wallet/index.d.ts +1 -0
- package/dist/types/src/wallet/index.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/HTTPWalletJSON.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/identity.md +225 -0
- package/docs/kvstore.md +133 -0
- package/docs/registry.md +383 -0
- package/docs/transaction.md +3 -3
- package/docs/wallet.md +146 -38
- package/mod.ts +2 -1
- package/package.json +19 -9
- package/src/kvstore/LocalKVStore.ts +287 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +614 -0
- package/src/kvstore/index.ts +1 -0
- package/src/wallet/WERR_REVIEW_ACTIONS.ts +30 -0
- package/src/wallet/Wallet.interfaces.ts +24 -0
- package/src/wallet/WalletError.ts +4 -2
- package/src/wallet/index.ts +2 -0
- package/src/wallet/substrates/HTTPWalletJSON.ts +12 -6
- 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.
|
|
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.
|
|
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": "^
|
|
237
|
-
"globals": "^
|
|
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.
|
|
241
|
-
"ts-loader": "^9.5.
|
|
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.
|
|
247
|
-
"webpack": "^5.
|
|
248
|
-
"webpack-cli": "^
|
|
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
|
+
}
|