@bsv/sdk 1.9.2 → 1.9.4

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 (93) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/GlobalKVStore.js +116 -98
  3. package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -1
  4. package/dist/cjs/src/kvstore/types.js.map +1 -1
  5. package/dist/cjs/src/overlay-tools/index.js +1 -0
  6. package/dist/cjs/src/overlay-tools/index.js.map +1 -1
  7. package/dist/cjs/src/overlay-tools/withDoubleSpendRetry.js +55 -0
  8. package/dist/cjs/src/overlay-tools/withDoubleSpendRetry.js.map +1 -0
  9. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  10. package/dist/esm/src/kvstore/GlobalKVStore.js +117 -99
  11. package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -1
  12. package/dist/esm/src/kvstore/types.js.map +1 -1
  13. package/dist/esm/src/overlay-tools/index.js +1 -0
  14. package/dist/esm/src/overlay-tools/index.js.map +1 -1
  15. package/dist/esm/src/overlay-tools/withDoubleSpendRetry.js +48 -0
  16. package/dist/esm/src/overlay-tools/withDoubleSpendRetry.js.map +1 -0
  17. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  18. package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -1
  19. package/dist/types/src/kvstore/types.d.ts +2 -0
  20. package/dist/types/src/kvstore/types.d.ts.map +1 -1
  21. package/dist/types/src/overlay-tools/index.d.ts +1 -0
  22. package/dist/types/src/overlay-tools/index.d.ts.map +1 -1
  23. package/dist/types/src/overlay-tools/withDoubleSpendRetry.d.ts +14 -0
  24. package/dist/types/src/overlay-tools/withDoubleSpendRetry.d.ts.map +1 -0
  25. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  26. package/dist/umd/bundle.js +2 -2
  27. package/dist/umd/bundle.js.map +1 -1
  28. package/docs/fast-docs.png +0 -0
  29. package/docs/index.md +49 -44
  30. package/docs/reference/kvstore.md +9 -0
  31. package/docs/reference/overlay-tools.md +32 -0
  32. package/docs/swagger.png +0 -0
  33. package/package.json +1 -1
  34. package/src/kvstore/GlobalKVStore.ts +134 -114
  35. package/src/kvstore/__tests/GlobalKVStore.test.ts +11 -1
  36. package/src/kvstore/types.ts +2 -0
  37. package/src/overlay-tools/index.ts +1 -0
  38. package/src/overlay-tools/withDoubleSpendRetry.ts +71 -0
  39. package/docs/MARKDOWN_VALIDATION_GUIDE.md +0 -175
  40. package/docs/concepts/beef.md +0 -92
  41. package/docs/concepts/chain-tracking.md +0 -134
  42. package/docs/concepts/decentralized-identity.md +0 -221
  43. package/docs/concepts/fees.md +0 -249
  44. package/docs/concepts/identity-certificates.md +0 -307
  45. package/docs/concepts/index.md +0 -77
  46. package/docs/concepts/key-management.md +0 -185
  47. package/docs/concepts/script-templates.md +0 -176
  48. package/docs/concepts/sdk-philosophy.md +0 -80
  49. package/docs/concepts/signatures.md +0 -194
  50. package/docs/concepts/spv-verification.md +0 -118
  51. package/docs/concepts/transaction-encoding.md +0 -167
  52. package/docs/concepts/transaction-structure.md +0 -67
  53. package/docs/concepts/trust-model.md +0 -139
  54. package/docs/concepts/verification.md +0 -250
  55. package/docs/concepts/wallet-integration.md +0 -101
  56. package/docs/guides/development-wallet-setup.md +0 -374
  57. package/docs/guides/direct-transaction-creation.md +0 -147
  58. package/docs/guides/http-client-configuration.md +0 -488
  59. package/docs/guides/index.md +0 -138
  60. package/docs/guides/large-transactions.md +0 -448
  61. package/docs/guides/multisig-transactions.md +0 -792
  62. package/docs/guides/security-best-practices.md +0 -494
  63. package/docs/guides/transaction-batching.md +0 -132
  64. package/docs/guides/transaction-signing-methods.md +0 -419
  65. package/docs/reference/arc-config.md +0 -698
  66. package/docs/reference/brc-100.md +0 -33
  67. package/docs/reference/configuration.md +0 -835
  68. package/docs/reference/debugging.md +0 -705
  69. package/docs/reference/errors.md +0 -597
  70. package/docs/reference/index.md +0 -111
  71. package/docs/reference/network-config.md +0 -914
  72. package/docs/reference/op-codes.md +0 -325
  73. package/docs/reference/transaction-signatures.md +0 -95
  74. package/docs/tutorials/advanced-transaction.md +0 -572
  75. package/docs/tutorials/aes-encryption.md +0 -949
  76. package/docs/tutorials/authfetch-tutorial.md +0 -986
  77. package/docs/tutorials/ecdh-key-exchange.md +0 -549
  78. package/docs/tutorials/elliptic-curve-fundamentals.md +0 -606
  79. package/docs/tutorials/error-handling.md +0 -1216
  80. package/docs/tutorials/first-transaction-low-level.md +0 -205
  81. package/docs/tutorials/first-transaction.md +0 -275
  82. package/docs/tutorials/hashes-and-hmacs.md +0 -788
  83. package/docs/tutorials/identity-management.md +0 -729
  84. package/docs/tutorials/index.md +0 -219
  85. package/docs/tutorials/key-management.md +0 -538
  86. package/docs/tutorials/protowallet-development.md +0 -743
  87. package/docs/tutorials/script-construction.md +0 -690
  88. package/docs/tutorials/spv-merkle-proofs.md +0 -685
  89. package/docs/tutorials/testnet-transactions-low-level.md +0 -359
  90. package/docs/tutorials/transaction-broadcasting.md +0 -538
  91. package/docs/tutorials/transaction-types.md +0 -420
  92. package/docs/tutorials/type-42.md +0 -568
  93. package/docs/tutorials/uhrp-storage.md +0 -599
Binary file
package/docs/index.md CHANGED
@@ -2,71 +2,76 @@
2
2
 
3
3
  ## SDK Overview
4
4
 
5
- The BSV TypeScript SDK is designed to provide an updated and unified layer for developing scalable applications on the BSV Blockchain. It addresses the limitations of previous tools by offering a fresh, peer-to-peer approach, adhering to SPV principles, and ensuring privacy and scalability.
5
+ The BSV TypeScript SDK is designed to provide an updated and unified layer for developing scalable applications on the BSV Blockchain. It addresses the limitations of previous tools by offering a Direct Instant Payments (DIP) approach, ensuring privacy and scalability.
6
6
 
7
- This documentation is organized to help you learn, solve problems, understand concepts, and find technical references.
7
+ ## Installation
8
8
 
9
- ## Documentation Categories
9
+ To install the SDK, run:
10
10
 
11
- ### [🚀 Tutorials](./tutorials/index.md)
11
+ ```bash
12
+ npm install @bsv/sdk
13
+ ```
12
14
 
13
- Learn step-by-step with practical, guided examples:
15
+ You can then import modules from the SDK as follows:
14
16
 
15
- - [Your First BSV Transaction](./tutorials/first-transaction.md)
16
- - [Key Management and Cryptography](./tutorials/key-management.md)
17
- - [Transaction Broadcasting and ARC](./tutorials/transaction-broadcasting.md)
18
- - [View all tutorials →](./tutorials/index.md)
17
+ ```ts
18
+ import { WalletClient, PrivateKey, Transaction } from "@bsv/sdk";
19
+ ```
19
20
 
20
- ### [🔧 How-To Guides](./guides/index.md)
21
+ Or using require syntax:
21
22
 
22
- Problem-oriented guides for specific tasks:
23
+ ```js
24
+ const { WalletClient, PrivateKey, Transaction } = require("@bsv/sdk");
25
+ ```
23
26
 
24
- - [Creating Multi-signature Transactions](./guides/multisig-transactions.md)
25
- - [Implementing Transaction Batching](./guides/transaction-batching.md)
26
- - [Configuring Custom ARC Endpoints](./guides/custom-arc-endpoints.md)
27
- - [View all guides →](./guides/index.md)
28
27
 
29
- ### [📚 Reference](./reference/index.md)
28
+ ## BRC-100 Application to Wallet Interface
30
29
 
31
- Complete technical specifications and API documentation:
30
+ This interface is what most application developers will use to interact with the BSV blockchain.
32
31
 
33
- - [BRC-100 Wallet Interface (Swagger)](./reference/brc-100.md)
34
- - [Primitives](./reference/primitives.md)
35
- - [Script](./reference/script.md)
36
- - [Transaction](./reference/transaction.md)
37
- - [View all references →](./reference/index.md)
32
+ 🚀 **[WalletClient Quickstart](https://fast.brc.dev/)**
38
33
 
39
- ### [🏗️ Concepts & Explanations](./concepts/index.md)
34
+ - Run SDK code examples without any setup
35
+ - Experiment with transactions, keys, and scripts in real-time
36
+ - Learn by doing with immediate feedback
37
+ - Test concepts from our tutorials interactively
40
38
 
41
- Understanding the architecture and design principles:
39
+ [![alt text](fast-docs.png)](https://fast.brc.dev/)
42
40
 
43
- - [SDK Design Philosophy](./concepts/sdk-philosophy.md)
44
- - [Transaction Structure](./concepts/transaction-structure.md)
45
- - [SPV Verification](./concepts/spv-verification.md)
46
- - [BEEF Format](./concepts/beef.md)
47
- - [View all concepts →](./concepts/index.md)
41
+ Perfect for getting started quickly or experimenting with new ideas.
48
42
 
49
- ## Getting Started
43
+ Another way to familiarize yourself with the Application to Wallet Interface is to checkout this Swagger UI.
50
44
 
51
- If you're new to the BSV TypeScript SDK, we recommend starting with our [Getting Started Tutorial](./tutorials/first-transaction.md).
45
+ [![Swagger](swagger.png)](./swagger/index.html)
52
46
 
53
- ## Try It Interactive
47
+ Finally, you can deep dive into the details of the interface and types in the reference material below.
54
48
 
55
- 🚀 **[Interactive BSV Coding Environment](https://fast.brc.dev/)**
49
+ ## Reference Material
56
50
 
57
- Experience the BSV TypeScript SDK directly in your browser! Our interactive coding environment lets you:
51
+ - [Wallet](./reference/wallet.md)
52
+ - [Primitives](./reference/primitives.md)
53
+ - [Script](./reference/script.md)
54
+ - [Transaction](./reference/transaction.md)
55
+ - [Mutual Authenitcation](./reference/auth.md)
56
+ - [Identity](./reference/identity.md)
57
+ - [Overlay Tools](./reference/overlay-tools.md)
58
+ - [Registry](./reference/registry.md)
59
+ - [Storage](./reference/storage.md)
60
+ - [KV Store](./reference/kvstore.md)
61
+ - [Messages](./reference/messages.md)
62
+ - [TOTP](./reference/totp.md)
63
+ - [Compatibility](./reference/compat.md)
58
64
 
59
- - Run SDK code examples without any setup
60
- - Experiment with transactions, keys, and scripts in real-time
61
- - Learn by doing with immediate feedback
62
- - Test concepts from our tutorials interactively
63
65
 
64
- Perfect for getting started quickly or experimenting with new ideas.
66
+ ## Coming Soon™
65
67
 
66
- ## Installation
68
+ - [Manual Transaction Creation](#manual-transaction-creation)
69
+ - [Broadcasting Transactions](#broadcasting-transactions)
70
+ - [Deriving Keys](#deriving-keys)
71
+ - [Overlays](#overlays)
72
+ - [MessageBox](#message-box)
67
73
 
68
- To install the SDK, run:
69
74
 
70
- ```bash
71
- npm install @bsv/sdk
72
- ```
75
+ ## Performance Reports
76
+
77
+ - [Benchmarks](./performance.md)
@@ -50,6 +50,7 @@ export interface KVStoreConfig {
50
50
  wallet?: WalletInterface;
51
51
  networkPreset?: "mainnet" | "testnet" | "local";
52
52
  acceptDelayedBroadcast?: boolean;
53
+ overlayBroadcast?: boolean;
53
54
  tokenSetDescription?: string;
54
55
  tokenUpdateDescription?: string;
55
56
  tokenRemovalDescription?: string;
@@ -82,6 +83,14 @@ Originator
82
83
  originator?: string
83
84
  ```
84
85
 
86
+ #### Property overlayBroadcast
87
+
88
+ Whether to let overlay handle broadcasting (prevents UTXO spending on rejection)
89
+
90
+ ```ts
91
+ overlayBroadcast?: boolean
92
+ ```
93
+
85
94
  #### Property overlayHost
86
95
 
87
96
  The overlay service host URL
@@ -564,6 +564,38 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
564
564
  ---
565
565
  ## Functions
566
566
 
567
+ ### Function: withDoubleSpendRetry
568
+
569
+ Executes an operation with automatic retry logic for double-spend errors.
570
+ When a double-spend is detected, broadcasts the competing transaction to
571
+ update the overlay with missing state, then retries the operation.
572
+
573
+ ```ts
574
+ export async function withDoubleSpendRetry<T>(operation: () => Promise<T>, broadcaster: TopicBroadcaster, maxRetries: number = MAX_DOUBLE_SPEND_RETRIES): Promise<T>
575
+ ```
576
+
577
+ See also: [TopicBroadcaster](./overlay-tools.md#class-topicbroadcaster)
578
+
579
+ Returns
580
+
581
+ The result of the successful operation
582
+
583
+ Argument Details
584
+
585
+ + **operation**
586
+ + The async operation to execute (e.g., createAction + signAction)
587
+ + **broadcaster**
588
+ + The TopicBroadcaster to use for syncing missing state
589
+ + **maxRetries**
590
+ + Maximum number of retry attempts (default: MAX_DOUBLE_SPEND_RETRIES)
591
+
592
+ Throws
593
+
594
+ If max retries exceeded or non-double-spend error occurs
595
+
596
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
597
+
598
+ ---
567
599
  ## Types
568
600
 
569
601
  | |
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -1,6 +1,6 @@
1
1
  import Transaction from '../transaction/Transaction.js'
2
2
  import * as Utils from '../primitives/utils.js'
3
- import { TopicBroadcaster, LookupResolver } from '../overlay-tools/index.js'
3
+ import { TopicBroadcaster, LookupResolver, withDoubleSpendRetry } from '../overlay-tools/index.js'
4
4
  import { BroadcastResponse, BroadcastFailure } from '../transaction/Broadcaster.js'
5
5
  import { WalletInterface, WalletProtocol, CreateActionInput, OutpointString, PubKeyHex, CreateActionOutput, HexString } from '../wallet/Wallet.interfaces.js'
6
6
  import { PushDrop } from '../script/index.js'
@@ -22,6 +22,7 @@ const DEFAULT_CONFIG: KVStoreConfig = {
22
22
  topics: ['tm_kvstore'],
23
23
  networkPreset: 'mainnet',
24
24
  acceptDelayedBroadcast: false,
25
+ overlayBroadcast: false, // Use overlay broadcasting to prevent UTXO spending on broadcast rejection.
25
26
  tokenSetDescription: '', // Will be set dynamically
26
27
  tokenUpdateDescription: '', // Will be set dynamically
27
28
  tokenRemovalDescription: '' // Will be set dynamically
@@ -141,11 +142,7 @@ export class GlobalKVStore {
141
142
  const tags = options.tags ?? []
142
143
 
143
144
  try {
144
- // Check for existing token to spend
145
- const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
146
- const existingToken = existingEntries.length > 0 ? existingEntries[0].token : undefined
147
-
148
- // Create PushDrop locking script
145
+ // Create PushDrop locking script (reusable across retries)
149
146
  const pushdrop = new PushDrop(this.wallet, this.config.originator)
150
147
  const lockingScriptFields = [
151
148
  Utils.toArray(JSON.stringify(protocolID), 'utf8'),
@@ -167,82 +164,92 @@ export class GlobalKVStore {
167
164
  true
168
165
  )
169
166
 
170
- let inputs: CreateActionInput[] = []
171
- let inputBEEF: Beef | undefined
172
-
173
- if (existingToken != null) {
174
- inputs = [{
175
- outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
176
- unlockingScriptLength: 74,
177
- inputDescription: 'Previous KVStore token'
178
- }]
179
- inputBEEF = existingToken.beef
180
- }
181
-
182
- if (inputs.length > 0) {
183
- // Update existing token
184
- const { signableTransaction } = await this.wallet.createAction({
185
- description: tokenUpdateDescription,
186
- inputBEEF: inputBEEF?.toBinary(),
187
- inputs,
188
- outputs: [{
189
- satoshis: tokenAmount ?? this.config.tokenAmount as number,
190
- lockingScript: lockingScript.toHex(),
191
- outputDescription: 'KVStore token'
192
- }],
193
- options: {
194
- acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
195
- randomizeOutputs: false
167
+ // Wrap entire operation in double-spend retry, including overlay query
168
+ const outpoint = await withDoubleSpendRetry(async () => {
169
+ // Re-query overlay on each attempt to get fresh token state
170
+ const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
171
+ const existingToken = existingEntries.length > 0 ? existingEntries[0].token : undefined
172
+
173
+ if (existingToken != null) {
174
+ // Update existing token
175
+ const inputs: CreateActionInput[] = [{
176
+ outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
177
+ unlockingScriptLength: 74,
178
+ inputDescription: 'Previous KVStore token'
179
+ }]
180
+ const inputBEEF = existingToken.beef
181
+
182
+ const { signableTransaction } = await this.wallet.createAction({
183
+ description: tokenUpdateDescription,
184
+ inputBEEF: inputBEEF.toBinary(),
185
+ inputs,
186
+ outputs: [{
187
+ satoshis: tokenAmount ?? this.config.tokenAmount as number,
188
+ lockingScript: lockingScript.toHex(),
189
+ outputDescription: 'KVStore token'
190
+ }],
191
+ options: {
192
+ acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
193
+ noSend: this.config.overlayBroadcast,
194
+ randomizeOutputs: false
195
+ }
196
+ }, this.config.originator)
197
+
198
+ if (signableTransaction == null) {
199
+ throw new Error('Unable to create update transaction')
196
200
  }
197
- }, this.config.originator)
198
-
199
- if (signableTransaction == null) {
200
- throw new Error('Unable to create update transaction')
201
- }
202
-
203
- const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
204
- const unlocker = pushdrop.unlock(
205
- this.config.protocolID as WalletProtocol,
206
- key,
207
- 'anyone'
208
- )
209
- const unlockingScript = await unlocker.sign(tx, 0)
210
201
 
211
- const { tx: finalTx } = await this.wallet.signAction({
212
- reference: signableTransaction.reference,
213
- spends: { 0: { unlockingScript: unlockingScript.toHex() } }
214
- }, this.config.originator)
215
-
216
- if (finalTx == null) {
217
- throw new Error('Unable to finalize update transaction')
218
- }
202
+ const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
203
+ const unlocker = pushdrop.unlock(
204
+ this.config.protocolID as WalletProtocol,
205
+ key,
206
+ 'anyone'
207
+ )
208
+ const unlockingScript = await unlocker.sign(tx, 0)
209
+
210
+ const { tx: finalTx } = await this.wallet.signAction({
211
+ reference: signableTransaction.reference,
212
+ spends: { 0: { unlockingScript: unlockingScript.toHex() } },
213
+ options: {
214
+ acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
215
+ noSend: this.config.overlayBroadcast
216
+ }
217
+ }, this.config.originator)
218
+
219
+ if (finalTx == null) {
220
+ throw new Error('Unable to finalize update transaction')
221
+ }
219
222
 
220
- const transaction = Transaction.fromAtomicBEEF(finalTx)
221
- await this.submitToOverlay(transaction)
222
- return `${transaction.id('hex')}.0`
223
- } else {
224
- // Create new token
225
- const { tx } = await this.wallet.createAction({
226
- description: tokenSetDescription,
227
- outputs: [{
228
- satoshis: tokenAmount ?? this.config.tokenAmount as number,
229
- lockingScript: lockingScript.toHex(),
230
- outputDescription: 'KVStore token'
231
- }],
232
- options: {
233
- acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
234
- randomizeOutputs: false
223
+ const transaction = Transaction.fromAtomicBEEF(finalTx)
224
+ await this.submitToOverlay(transaction)
225
+ return `${transaction.id('hex')}.0`
226
+ } else {
227
+ // Create new token
228
+ const { tx } = await this.wallet.createAction({
229
+ description: tokenSetDescription,
230
+ outputs: [{
231
+ satoshis: tokenAmount ?? this.config.tokenAmount as number,
232
+ lockingScript: lockingScript.toHex(),
233
+ outputDescription: 'KVStore token'
234
+ }],
235
+ options: {
236
+ acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
237
+ noSend: this.config.overlayBroadcast,
238
+ randomizeOutputs: false
239
+ }
240
+ }, this.config.originator)
241
+
242
+ if (tx == null) {
243
+ throw new Error('Failed to create transaction')
235
244
  }
236
- }, this.config.originator)
237
245
 
238
- if (tx == null) {
239
- throw new Error('Failed to create transaction')
246
+ const transaction = Transaction.fromAtomicBEEF(tx)
247
+ await this.submitToOverlay(transaction)
248
+ return `${transaction.id('hex')}.0`
240
249
  }
250
+ }, this.topicBroadcaster)
241
251
 
242
- const transaction = Transaction.fromAtomicBEEF(tx)
243
- await this.submitToOverlay(transaction)
244
- return `${transaction.id('hex')}.0`
245
- }
252
+ return outpoint
246
253
  } finally {
247
254
  if (lockQueue.length > 0) {
248
255
  this.finishOperationOnKey(key, lockQueue)
@@ -274,54 +281,67 @@ export class GlobalKVStore {
274
281
  const tokenRemovalDescription = (options.tokenRemovalDescription != null && options.tokenRemovalDescription !== '') ? options.tokenRemovalDescription : `Remove KVStore value for ${key}`
275
282
 
276
283
  try {
277
- const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
284
+ const pushdrop = new PushDrop(this.wallet, this.config.originator)
278
285
 
279
- if (existingEntries.length === 0 || existingEntries[0].token == null) {
280
- throw new Error('The item did not exist, no item was deleted.')
281
- }
286
+ // Remove token with double-spend retry
287
+ const txid = await withDoubleSpendRetry(async () => {
288
+ // Re-query overlay on each attempt to get fresh token state
289
+ const existingEntries = await this.queryOverlay({ key, controller }, { includeToken: true })
282
290
 
283
- const existingToken = existingEntries[0].token
284
- const inputs: CreateActionInput[] = [{
285
- outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
286
- unlockingScriptLength: 74,
287
- inputDescription: 'KVStore token to remove'
288
- }]
291
+ if (existingEntries.length === 0 || existingEntries[0].token == null) {
292
+ throw new Error('The item did not exist, no item was deleted.')
293
+ }
289
294
 
290
- const pushdrop = new PushDrop(this.wallet, this.config.originator)
291
- const { signableTransaction } = await this.wallet.createAction({
292
- description: tokenRemovalDescription,
293
- inputBEEF: existingToken.beef.toBinary(),
294
- inputs,
295
- outputs,
296
- options: {
297
- acceptDelayedBroadcast: this.config.acceptDelayedBroadcast
295
+ const existingToken = existingEntries[0].token
296
+ const inputs: CreateActionInput[] = [{
297
+ outpoint: `${existingToken.txid}.${existingToken.outputIndex}`,
298
+ unlockingScriptLength: 74,
299
+ inputDescription: 'KVStore token to remove'
300
+ }]
301
+
302
+ const { signableTransaction } = await this.wallet.createAction({
303
+ description: tokenRemovalDescription,
304
+ inputBEEF: existingToken.beef.toBinary(),
305
+ inputs,
306
+ outputs,
307
+ options: {
308
+ acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
309
+ randomizeOutputs: false,
310
+ noSend: this.config.overlayBroadcast
311
+ }
312
+ }, this.config.originator)
313
+
314
+ if (signableTransaction == null) {
315
+ throw new Error('Unable to create removal transaction')
298
316
  }
299
- }, this.config.originator)
300
317
 
301
- if (signableTransaction == null) {
302
- throw new Error('Unable to create removal transaction')
303
- }
318
+ const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
319
+ const unlocker = pushdrop.unlock(
320
+ protocolID ?? this.config.protocolID as WalletProtocol,
321
+ key,
322
+ 'anyone'
323
+ )
324
+ const unlockingScript = await unlocker.sign(tx, 0)
304
325
 
305
- const tx = Transaction.fromAtomicBEEF(signableTransaction.tx)
306
- const unlocker = pushdrop.unlock(
307
- protocolID ?? this.config.protocolID as WalletProtocol,
308
- key,
309
- 'anyone'
310
- )
311
- const unlockingScript = await unlocker.sign(tx, 0)
326
+ const { tx: finalTx } = await this.wallet.signAction({
327
+ reference: signableTransaction.reference,
328
+ spends: { 0: { unlockingScript: unlockingScript.toHex() } },
329
+ options: {
330
+ acceptDelayedBroadcast: this.config.acceptDelayedBroadcast,
331
+ noSend: this.config.overlayBroadcast
332
+ }
333
+ }, this.config.originator)
312
334
 
313
- const { tx: finalTx } = await this.wallet.signAction({
314
- reference: signableTransaction.reference,
315
- spends: { 0: { unlockingScript: unlockingScript.toHex() } }
316
- }, this.config.originator)
335
+ if (finalTx == null) {
336
+ throw new Error('Unable to finalize removal transaction')
337
+ }
317
338
 
318
- if (finalTx == null) {
319
- throw new Error('Unable to finalize removal transaction')
320
- }
339
+ const transaction = Transaction.fromAtomicBEEF(finalTx)
340
+ await this.submitToOverlay(transaction)
341
+ return transaction.id('hex')
342
+ }, this.topicBroadcaster)
321
343
 
322
- const transaction = Transaction.fromAtomicBEEF(finalTx)
323
- await this.submitToOverlay(transaction)
324
- return transaction.id('hex')
344
+ return txid
325
345
  } finally {
326
346
  if (lockQueue.length > 0) {
327
347
  this.finishOperationOnKey(key, lockQueue)
@@ -18,7 +18,17 @@ jest.mock('../../overlay-tools/Historian.js')
18
18
  jest.mock('../kvStoreInterpreter.js')
19
19
  jest.mock('../../script/index.js')
20
20
  jest.mock('../../primitives/utils.js')
21
- jest.mock('../../overlay-tools/index.js')
21
+ jest.mock('../../overlay-tools/index.js', () => {
22
+ const actual = jest.requireActual('../../overlay-tools/index.js')
23
+ return {
24
+ ...actual,
25
+ // Keep withDoubleSpendRetry as the real implementation
26
+ withDoubleSpendRetry: actual.withDoubleSpendRetry,
27
+ // Mock the classes
28
+ TopicBroadcaster: jest.fn(),
29
+ LookupResolver: jest.fn()
30
+ }
31
+ })
22
32
  jest.mock('../../wallet/ProtoWallet.js')
23
33
  jest.mock('../../wallet/WalletClient.js')
24
34
 
@@ -25,6 +25,8 @@ export interface KVStoreConfig {
25
25
  networkPreset?: 'mainnet' | 'testnet' | 'local'
26
26
  /** Whether to accept delayed broadcast */
27
27
  acceptDelayedBroadcast?: boolean
28
+ /** Whether to let overlay handle broadcasting (prevents UTXO spending on rejection) */
29
+ overlayBroadcast?: boolean
28
30
  /** Description for token set */
29
31
  tokenSetDescription?: string
30
32
  /** Description for token update */
@@ -1,5 +1,6 @@
1
1
  export * from './LookupResolver.js'
2
2
  export * from './SHIPBroadcaster.js'
3
+ export * from './withDoubleSpendRetry.js'
3
4
  export { default as OverlayAdminTokenTemplate } from './OverlayAdminTokenTemplate.js'
4
5
  export { default as LookupResolver } from './LookupResolver.js'
5
6
 
@@ -0,0 +1,71 @@
1
+ import Transaction from '../transaction/Transaction.js'
2
+ import { WERR_REVIEW_ACTIONS } from '../wallet/WERR_REVIEW_ACTIONS.js'
3
+ import { ReviewActionResult } from '../wallet/Wallet.interfaces.js'
4
+ import TopicBroadcaster from './SHIPBroadcaster.js'
5
+
6
+ /**
7
+ * Maximum number of retry attempts for double-spend resolution
8
+ */
9
+ const MAX_DOUBLE_SPEND_RETRIES = 5
10
+
11
+ /**
12
+ * Executes an operation with automatic retry logic for double-spend errors.
13
+ * When a double-spend is detected, broadcasts the competing transaction to
14
+ * update the overlay with missing state, then retries the operation.
15
+ *
16
+ * @param operation - The async operation to execute (e.g., createAction + signAction)
17
+ * @param broadcaster - The TopicBroadcaster to use for syncing missing state
18
+ * @param maxRetries - Maximum number of retry attempts (default: MAX_DOUBLE_SPEND_RETRIES)
19
+ * @returns The result of the successful operation
20
+ * @throws If max retries exceeded or non-double-spend error occurs
21
+ */
22
+ export async function withDoubleSpendRetry<T> (
23
+ operation: () => Promise<T>,
24
+ broadcaster: TopicBroadcaster,
25
+ maxRetries: number = MAX_DOUBLE_SPEND_RETRIES
26
+ ): Promise<T> {
27
+ let attempts = 0
28
+
29
+ while (attempts < maxRetries) {
30
+ attempts++
31
+
32
+ try {
33
+ return await operation()
34
+ } catch (error) {
35
+ // Only handle double-spend errors on non-final attempts
36
+ if (attempts < maxRetries && error.name === 'WERR_REVIEW_ACTIONS') {
37
+ const reviewError = error as WERR_REVIEW_ACTIONS
38
+
39
+ // Check if any action in the batch has a double-spend
40
+ const doubleSpendResult = reviewError.reviewActionResults.find(
41
+ (result: ReviewActionResult) => result.status === 'doubleSpend'
42
+ )
43
+
44
+ if (doubleSpendResult?.competingBeef != null &&
45
+ doubleSpendResult?.competingTxs != null &&
46
+ doubleSpendResult?.competingTxs.length > 0) {
47
+ const competingTx = Transaction.fromBEEF(
48
+ doubleSpendResult.competingBeef,
49
+ doubleSpendResult.competingTxs[0]
50
+ )
51
+
52
+ // Recursively handle double-spend errors during broadcast
53
+ // The broadcast itself might fail if the competing tx depends on another missing tx
54
+ await withDoubleSpendRetry(
55
+ async () => await broadcaster.broadcast(competingTx),
56
+ broadcaster,
57
+ maxRetries - attempts // Reduce max retries based on attempts already used
58
+ )
59
+
60
+ continue
61
+ }
62
+ }
63
+
64
+ // Non-double-spend error or max retries exceeded - rethrow
65
+ throw error
66
+ }
67
+ }
68
+
69
+ // This should never be reached, but TypeScript requires it
70
+ throw new Error('Unexpected end of retry loop')
71
+ }