@bsv/wallet-toolbox 1.2.30 → 1.2.31

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 (39) hide show
  1. package/docs/client.md +159 -69
  2. package/docs/services.md +32 -2
  3. package/docs/storage.md +0 -15
  4. package/docs/wallet.md +159 -69
  5. package/out/src/sdk/WalletServices.interfaces.d.ts +37 -0
  6. package/out/src/sdk/WalletServices.interfaces.d.ts.map +1 -1
  7. package/out/src/sdk/WalletStorage.interfaces.d.ts +0 -7
  8. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  9. package/out/src/services/Services.d.ts +2 -0
  10. package/out/src/services/Services.d.ts.map +1 -1
  11. package/out/src/services/Services.js +24 -0
  12. package/out/src/services/Services.js.map +1 -1
  13. package/out/src/services/providers/WhatsOnChain.d.ts +22 -0
  14. package/out/src/services/providers/WhatsOnChain.d.ts.map +1 -1
  15. package/out/src/services/providers/WhatsOnChain.js +60 -0
  16. package/out/src/services/providers/WhatsOnChain.js.map +1 -1
  17. package/out/src/storage/methods/attemptToPostReqsToNetwork.d.ts +0 -7
  18. package/out/src/storage/methods/attemptToPostReqsToNetwork.d.ts.map +1 -1
  19. package/out/src/storage/methods/attemptToPostReqsToNetwork.js +27 -34
  20. package/out/src/storage/methods/attemptToPostReqsToNetwork.js.map +1 -1
  21. package/out/src/storage/methods/processAction.js +1 -1
  22. package/out/src/storage/methods/processAction.js.map +1 -1
  23. package/out/src/storage/schema/entities/__tests/ProvenTxTests.test.js +5 -0
  24. package/out/src/storage/schema/entities/__tests/ProvenTxTests.test.js.map +1 -1
  25. package/out/test/Wallet/local/localWallet2.man.test.js +1 -2
  26. package/out/test/Wallet/local/localWallet2.man.test.js.map +1 -1
  27. package/out/test/services/Services.test.js +21 -0
  28. package/out/test/services/Services.test.js.map +1 -1
  29. package/out/tsconfig.all.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/sdk/WalletServices.interfaces.ts +41 -0
  32. package/src/sdk/WalletStorage.interfaces.ts +0 -4
  33. package/src/services/Services.ts +29 -0
  34. package/src/services/providers/WhatsOnChain.ts +74 -0
  35. package/src/storage/methods/attemptToPostReqsToNetwork.ts +28 -39
  36. package/src/storage/methods/processAction.ts +1 -1
  37. package/src/storage/schema/entities/__tests/ProvenTxTests.test.ts +6 -0
  38. package/test/Wallet/local/localWallet2.man.test.ts +0 -1
  39. package/test/services/Services.test.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/wallet-toolbox",
3
- "version": "1.2.30",
3
+ "version": "1.2.31",
4
4
  "description": "BRC100 conforming wallet, wallet storage and wallet signer components",
5
5
  "main": "./out/src/index.js",
6
6
  "types": "./out/src/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@bsv/auth-express-middleware": "^1.1.2",
34
34
  "@bsv/payment-express-middleware": "^1.0.4",
35
- "@bsv/sdk": "^1.4.12",
35
+ "@bsv/sdk": "^1.4.15",
36
36
  "express": "^4.21.2",
37
37
  "knex": "^3.1.0",
38
38
  "mysql2": "^3.12.0",
@@ -97,6 +97,18 @@ export interface WalletServices {
97
97
  */
98
98
  hashOutputScript(script: string): string
99
99
 
100
+ /**
101
+ * For an array of one or more txids, returns for each wether it is a 'known', 'mined', or 'unknown' transaction.
102
+ *
103
+ * Primarily useful for determining if a recently broadcast transaction is known to the processing network.
104
+ *
105
+ * Also returns the current depth from chain tip if 'mined'.
106
+ *
107
+ * @param txids
108
+ * @param useNext
109
+ */
110
+ getStatusForTxids(txids: string[], useNext?: boolean): Promise<sdk.GetStatusForTxidsResult>
111
+
100
112
  /**
101
113
  * Attempts to determine the UTXO status of a transaction output.
102
114
  *
@@ -166,6 +178,33 @@ export interface WalletServicesOptions {
166
178
  arcConfig: ArcConfig
167
179
  }
168
180
 
181
+ export interface GetStatusForTxidsResult {
182
+ /**
183
+ * The name of the service returning these results.
184
+ */
185
+ name: string
186
+ status: 'success' | 'error'
187
+ /**
188
+ * The first exception error that occurred during processing, if any.
189
+ */
190
+ error?: sdk.WalletError
191
+ results: sdk.StatusForTxidResult[]
192
+ }
193
+
194
+ export interface StatusForTxidResult {
195
+ txid: string
196
+ /**
197
+ * roughly depth of block containing txid from chain tip.
198
+ */
199
+ depth: number | undefined
200
+ /**
201
+ * 'mined' if depth > 0
202
+ * 'known' if depth === 0
203
+ * 'unknown' if depth === undefined, txid may be old an purged or never processed.
204
+ */
205
+ status: 'mined' | 'known' | 'unknown'
206
+ }
207
+
169
208
  /**
170
209
  * Properties on result returned from `WalletServices` function `getRawTx`.
171
210
  */
@@ -416,6 +455,8 @@ export type GetUtxoStatusService = (
416
455
  outpoint?: string
417
456
  ) => Promise<GetUtxoStatusResult>
418
457
 
458
+ export type GetStatusForTxidsService = (txids: string[]) => Promise<sdk.GetStatusForTxidsResult>
459
+
419
460
  export type GetScriptHashHistoryService = (hash: string) => Promise<GetScriptHashHistoryResult>
420
461
 
421
462
  export type GetMerklePathService = (txid: string, services: WalletServices) => Promise<GetMerklePathResult>
@@ -264,10 +264,6 @@ export interface ReviewActionResult {
264
264
  * Merged beef of competingTxs, valid when status is 'doubleSpend'.
265
265
  */
266
266
  competingBeef?: number[]
267
- /**
268
- * Transaction input indices that have been spent, valid when status is 'doubleSpend'.
269
- */
270
- spentInputs?: { vin: number; scriptHash: string }[]
271
267
  }
272
268
 
273
269
  export interface StorageProcessActionResults {
@@ -22,6 +22,7 @@ export class Services implements sdk.WalletServices {
22
22
  getRawTxServices: ServiceCollection<sdk.GetRawTxService>
23
23
  postBeefServices: ServiceCollection<sdk.PostBeefService>
24
24
  getUtxoStatusServices: ServiceCollection<sdk.GetUtxoStatusService>
25
+ getStatusForTxidsServices: ServiceCollection<sdk.GetStatusForTxidsService>
25
26
  getScriptHashHistoryServices: ServiceCollection<sdk.GetScriptHashHistoryService>
26
27
  updateFiatExchangeRateServices: ServiceCollection<sdk.UpdateFiatExchangeRateService>
27
28
 
@@ -57,6 +58,10 @@ export class Services implements sdk.WalletServices {
57
58
  this.getUtxoStatusServices = new ServiceCollection<sdk.GetUtxoStatusService>()
58
59
  .add({ name: 'WhatsOnChain', service: this.whatsonchain.getUtxoStatus.bind(this.whatsonchain) })
59
60
 
61
+ //prettier-ignore
62
+ this.getStatusForTxidsServices = new ServiceCollection<sdk.GetStatusForTxidsService>()
63
+ .add({ name: 'WhatsOnChain', service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain) })
64
+
60
65
  //prettier-ignore
61
66
  this.getScriptHashHistoryServices = new ServiceCollection<sdk.GetScriptHashHistoryService>()
62
67
  .add({ name: 'WhatsOnChain', service: this.whatsonchain.getScriptHashHistory.bind(this.whatsonchain) })
@@ -105,6 +110,30 @@ export class Services implements sdk.WalletServices {
105
110
  return this.getUtxoStatusServices.count
106
111
  }
107
112
 
113
+ async getStatusForTxids(txids: string[], useNext?: boolean): Promise<sdk.GetStatusForTxidsResult> {
114
+ const services = this.getStatusForTxidsServices
115
+ if (useNext) services.next()
116
+
117
+ let r0: sdk.GetStatusForTxidsResult = {
118
+ name: '<noservices>',
119
+ status: 'error',
120
+ error: new sdk.WERR_INTERNAL('No services available.'),
121
+ results: []
122
+ }
123
+
124
+ for (let tries = 0; tries < services.count; tries++) {
125
+ const service = services.service
126
+ const r = await service(txids)
127
+ if (r.status === 'success') {
128
+ r0 = r
129
+ break
130
+ }
131
+ services.next()
132
+ }
133
+
134
+ return r0
135
+ }
136
+
108
137
  /**
109
138
  * @param script Output script to be hashed for `getUtxoStatus` default `outputFormat`
110
139
  * @returns script hash in 'hashLE' format, which is the default.
@@ -15,6 +15,68 @@ export class WhatsOnChain extends SdkWhatsOnChain {
15
15
  this.services = services || new Services(chain)
16
16
  }
17
17
 
18
+ /**
19
+ * POST
20
+ * https://api.whatsonchain.com/v1/bsv/main/txs/status
21
+ * Content-Type: application/json
22
+ * data: "{\"txids\":[\"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62b\"]}"
23
+ *
24
+ * result for a mined txid:
25
+ * [{
26
+ * "txid":"294cd1ebd5689fdee03509f92c32184c0f52f037d4046af250229b97e0c8f1aa",
27
+ * "blockhash":"000000000000000004b5ce6670f2ff27354a1e87d0a01bf61f3307f4ccd358b5",
28
+ * "blockheight":612251,
29
+ * "blocktime":1575841517,
30
+ * "confirmations":278272
31
+ * }]
32
+ *
33
+ * result for a valid recent txid:
34
+ * [{"txid":"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62b"}]
35
+ *
36
+ * result for an unknown txid:
37
+ * [{"txid":"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62c","error":"unknown"}]
38
+ */
39
+ async getStatusForTxids(txids: string[]): Promise<sdk.GetStatusForTxidsResult> {
40
+ const r: sdk.GetStatusForTxidsResult = {
41
+ name: 'WoC',
42
+ status: 'error',
43
+ error: undefined,
44
+ results: []
45
+ }
46
+
47
+ const requestOptions = {
48
+ method: 'POST',
49
+ headers: this.getHttpHeaders(),
50
+ data: { txids }
51
+ }
52
+
53
+ const url = `${this.URL}/txs/status`
54
+
55
+ try {
56
+ const response = await this.httpClient.request<WhatsOnChainTxsStatusData[]>(url, requestOptions)
57
+
58
+ if (!response.data || !response.ok || response.status !== 200)
59
+ throw new sdk.WERR_INVALID_OPERATION(`Unable to get status for txids at this timei.`)
60
+
61
+ const data = response.data
62
+ for (const txid of txids) {
63
+ const d = data.find(d => d.txid === txid)
64
+ if (!d || d.error === 'unknown') r.results.push({ txid, status: 'unknown', depth: undefined })
65
+ else if (d.error !== undefined) {
66
+ console.log(`WhatsOnChain getStatusForTxids unexpected error ${d.error} ${txid}`)
67
+ r.results.push({ txid, status: 'unknown', depth: undefined })
68
+ } else if (d.confirmations === undefined) r.results.push({ txid, status: 'known', depth: 0 })
69
+ else r.results.push({ txid, status: 'mined', depth: d.confirmations })
70
+ }
71
+ r.status = 'success'
72
+ } catch (eu: unknown) {
73
+ const e = sdk.WalletError.fromUnknown(eu)
74
+ r.error = e
75
+ }
76
+
77
+ return r
78
+ }
79
+
18
80
  /**
19
81
  * 2025-02-16 throwing internal server error 500.
20
82
  * @param txid
@@ -617,3 +679,15 @@ interface WhatsOnChainScriptHashHistoryData {
617
679
  error?: string
618
680
  nextPageToken?: string
619
681
  }
682
+
683
+ interface WhatsOnChainTxsStatusData {
684
+ txid: string
685
+ blockhash?: string
686
+ blockheight?: number
687
+ blocktime?: number
688
+ confirmations?: number
689
+ /**
690
+ * 'unknown' if txid isn't known
691
+ */
692
+ error?: string
693
+ }
@@ -1,7 +1,7 @@
1
1
  import { Beef, Transaction } from '@bsv/sdk'
2
2
  import { StorageProvider } from '../StorageProvider'
3
3
  import { EntityProvenTxReq } from '../schema/entities'
4
- import { sdk } from '../../index.client'
4
+ import { sdk, wait } from '../../index.client'
5
5
  import { ReqHistoryNote } from '../../sdk'
6
6
 
7
7
  /**
@@ -136,8 +136,7 @@ function aggregatePostBeefResultsByTxid(
136
136
  doubleSpendCount: 0,
137
137
  statusErrorCount: 0,
138
138
  serviceErrorCount: 0,
139
- competingTxs: [],
140
- spentInputs: []
139
+ competingTxs: []
141
140
  }
142
141
  r[txid] = ar
143
142
  for (const pbr of pbrs) {
@@ -263,7 +262,6 @@ async function updateReqsFromAggregateResults(
263
262
  const details = r.details.find(d => d.txid === txid)!
264
263
  details.status = ar.status
265
264
  details.competingTxs = ar.competingTxs
266
- details.spentInputs = ar.spentInputs
267
265
  }
268
266
  }
269
267
 
@@ -286,43 +284,42 @@ async function confirmDoubleSpend(
286
284
  ): Promise<void> {
287
285
  const req = ar.vreq.req
288
286
  const note: ReqHistoryNote = { when: new Date().toISOString(), what: 'confirmDoubleSpend' }
289
- const tx = Transaction.fromBinary(req.rawTx)
290
- ar.spentInputs = []
291
- let vin = -1
292
- for (const input of tx.inputs) {
293
- vin++
294
- const sourceTx = beef.findTxid(input.sourceTXID!)?.tx
295
- if (!sourceTx) throw new sdk.WERR_INTERNAL(`beef lacks tx for ${input.sourceTXID}`)
296
- const lockingScript = sourceTx.outputs[input.sourceOutputIndex].lockingScript.toHex()
297
- const hash = services.hashOutputScript(lockingScript)
298
- const usr = await services.getUtxoStatus(hash, undefined, `${input.sourceTXID}.${input.sourceOutputIndex}`)
299
- if (usr.isUtxo === false) {
300
- ar.spentInputs.push({
301
- vin,
302
- scriptHash: hash,
303
- sourceTXID: input.sourceTXID!,
304
- sourceIndex: input.sourceOutputIndex
305
- })
287
+
288
+ let known = false
289
+
290
+ for (let retry = 0; retry < 3; retry++) {
291
+ const gsr = await services.getStatusForTxids([req.txid])
292
+ note[`getStatus${retry}`] = `${gsr.status}${gsr.error ? `${gsr.error.code}` : ''},${gsr.results[0]?.status}`
293
+ if (gsr.status === 'success' && gsr.results[0].status !== 'unknown') {
294
+ known = true
295
+ break
296
+ } else {
297
+ await wait(1000)
306
298
  }
307
299
  }
308
- note.vins = ar.spentInputs.map(si => si.vin.toString()).join(',')
309
- if (ar.spentInputs.length === 0) {
310
- // Possibly NOT a double spend...
311
- if (ar.successCount > 0) ar.status = 'success'
312
- else ar.status = 'serviceError'
300
+
301
+ if (known) {
302
+ // doubleSpend -> success
303
+ ar.status = 'success'
313
304
  note.newStatus = ar.status
314
305
  } else {
315
- // Confirmed double spend.
306
+ // Confirmed double spend, get txids of possible competing transactions.
307
+ const tx = Transaction.fromBinary(req.rawTx)
316
308
  const competingTxids = new Set(ar.competingTxs)
317
- for (const si of ar.spentInputs) {
318
- const shhrs = await services.getScriptHashHistory(si.scriptHash)
309
+ for (const input of tx.inputs) {
310
+ const sourceTx = beef.findTxid(input.sourceTXID!)?.tx
311
+ if (!sourceTx) throw new sdk.WERR_INTERNAL(`beef lacks tx for ${input.sourceTXID}`)
312
+ const lockingScript = sourceTx.outputs[input.sourceOutputIndex].lockingScript.toHex()
313
+ const hash = services.hashOutputScript(lockingScript)
314
+ const shhrs = await services.getScriptHashHistory(hash)
319
315
  if (shhrs.status === 'success') {
320
316
  for (const h of shhrs.history) {
321
- if (h.txid !== si.sourceTXID) competingTxids.add(h.txid)
317
+ // Neither the source of the input nor the current transaction are competition.
318
+ if (h.txid !== input.sourceTXID && h.txid !== ar.txid) competingTxids.add(h.txid)
322
319
  }
323
320
  }
324
321
  }
325
- ar.competingTxs = [...competingTxids].slice(0, 24) // keep at most 24, if they were sorted by time, keep oldest
322
+ ar.competingTxs = [...competingTxids].slice(-1, 24) // keep at most 24, if they were sorted by time, keep newest
326
323
  note.competingTxs = ar.competingTxs.join(',')
327
324
  }
328
325
  req.addHistoryNote(note)
@@ -343,10 +340,6 @@ interface AggregatePostBeefTxResult {
343
340
  * Any competing double spend txids reported for this txid
344
341
  */
345
342
  competingTxs: string[]
346
- /**
347
- * Input indices that have been spent, valid when status is 'doubleSpend'
348
- */
349
- spentInputs: { vin: number; scriptHash: string; sourceTXID: string; sourceIndex: number }[]
350
343
  }
351
344
 
352
345
  /**
@@ -378,10 +371,6 @@ export interface PostReqsToNetworkDetails {
378
371
  * Any competing double spend txids reported for this txid
379
372
  */
380
373
  competingTxs?: string[]
381
- /**
382
- * Input indices that have been spent, valid when status is 'doubleSpend'
383
- */
384
- spentInputs?: { vin: number; scriptHash: string }[]
385
374
  }
386
375
 
387
376
  export interface PostReqsToNetworkResult {
@@ -191,7 +191,7 @@ async function shareReqsWithWorld(
191
191
  const txid = ar.txid
192
192
  const d = prtn.details.find(d => d.txid === txid)
193
193
  if (!d) throw new sdk.WERR_INTERNAL(`missing details for ${txid}`)
194
- ar.ndr = { txid: d.txid, status: 'success', competingTxs: d.competingTxs, spentInputs: d.spentInputs }
194
+ ar.ndr = { txid: d.txid, status: 'success', competingTxs: d.competingTxs }
195
195
  switch (d.status) {
196
196
  case 'success':
197
197
  // processing network has accepted this transaction
@@ -103,6 +103,12 @@ describe('ProvenTx class method tests', () => {
103
103
  getFiatExchangeRate: async () => 1,
104
104
  postBeef: async () => [],
105
105
 
106
+ getStatusForTxids: async () => ({
107
+ name: 'mock-service',
108
+ status: 'success',
109
+ results: []
110
+ }),
111
+
106
112
  getUtxoStatus: async () => ({
107
113
  name: 'mock-service',
108
114
  status: 'success',
@@ -77,7 +77,6 @@ describe('localWallet2 tests', () => {
77
77
  const rar = e.reviewActionResults![0]!
78
78
  expect(rar.status).toBe('doubleSpend')
79
79
  expect(rar.competingTxs?.length).toBe(1)
80
- expect(rar.spentInputs?.length).toBe(1)
81
80
  }
82
81
  await setup.wallet.destroy()
83
82
  })
@@ -233,4 +233,25 @@ describe('Wallet services tests', () => {
233
233
  }
234
234
  }
235
235
  })
236
+
237
+ test('7 getStatusForTxids', async () => {
238
+ for (const { chain, services } of ctxs) {
239
+ {
240
+ const txids = ['32c691a077b0ce46051aa7a45fa3b131c71ff85950264575a32171086b02ad98']
241
+ const r = await services.getStatusForTxids(txids)
242
+ expect(r.results.length).toBe(1)
243
+ expect(r.results[0].txid).toBe(txids[0])
244
+ expect(r.name).toBeTruthy()
245
+ expect(r.status).toBe('success')
246
+ expect(r.error).toBe(undefined)
247
+ if (chain === 'main') {
248
+ expect(r.results[0].status).toBe('mined')
249
+ expect(r.results[0].depth).toBeGreaterThan(146)
250
+ } else {
251
+ expect(r.results[0].status).toBe('unknown')
252
+ expect(r.results[0].depth).toBe(undefined)
253
+ }
254
+ }
255
+ }
256
+ })
236
257
  })