@bsv/wallet-toolbox 1.6.26 → 1.6.28
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/CHANGELOG.md +8 -0
- package/docs/client.md +12 -7
- package/docs/services.md +5 -5
- package/docs/storage.md +7 -2
- package/docs/wallet.md +12 -7
- package/mobile/out/src/services/Services.js +4 -4
- package/mobile/out/src/signer/methods/internalizeAction.d.ts.map +1 -1
- package/mobile/out/src/signer/methods/internalizeAction.js +9 -1
- package/mobile/out/src/signer/methods/internalizeAction.js.map +1 -1
- package/mobile/out/src/storage/methods/internalizeAction.d.ts.map +1 -1
- package/mobile/out/src/storage/methods/internalizeAction.js +92 -18
- package/mobile/out/src/storage/methods/internalizeAction.js.map +1 -1
- package/mobile/out/src/storage/methods/processAction.d.ts +2 -1
- package/mobile/out/src/storage/methods/processAction.d.ts.map +1 -1
- package/mobile/out/src/storage/methods/processAction.js +4 -3
- package/mobile/out/src/storage/methods/processAction.js.map +1 -1
- package/mobile/package-lock.json +2 -2
- package/mobile/package.json +1 -1
- package/out/src/services/Services.js +4 -4
- package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js +5 -0
- package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js.map +1 -1
- package/out/src/signer/methods/internalizeAction.d.ts.map +1 -1
- package/out/src/signer/methods/internalizeAction.js +9 -1
- package/out/src/signer/methods/internalizeAction.js.map +1 -1
- package/out/src/storage/methods/internalizeAction.d.ts.map +1 -1
- package/out/src/storage/methods/internalizeAction.js +92 -18
- package/out/src/storage/methods/internalizeAction.js.map +1 -1
- package/out/src/storage/methods/processAction.d.ts +2 -1
- package/out/src/storage/methods/processAction.d.ts.map +1 -1
- package/out/src/storage/methods/processAction.js +4 -3
- package/out/src/storage/methods/processAction.js.map +1 -1
- package/out/test/Wallet/get/getHeaderForHeight.test.js +7 -0
- package/out/test/Wallet/get/getHeaderForHeight.test.js.map +1 -1
- package/out/tsconfig.all.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/services/Services.ts +4 -4
- package/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.ts +3 -0
- package/src/signer/methods/internalizeAction.ts +9 -1
- package/src/storage/methods/internalizeAction.ts +100 -22
- package/src/storage/methods/processAction.ts +5 -3
- package/test/Wallet/get/getHeaderForHeight.test.ts +10 -0
package/package.json
CHANGED
package/src/services/Services.ts
CHANGED
|
@@ -574,12 +574,12 @@ export function validateScriptHash(output: string, outputFormat?: GetUtxoStatusO
|
|
|
574
574
|
*/
|
|
575
575
|
export function toBinaryBaseBlockHeader(header: BaseBlockHeader): number[] {
|
|
576
576
|
const writer = new Utils.Writer()
|
|
577
|
-
writer.
|
|
577
|
+
writer.writeUInt32LE(header.version)
|
|
578
578
|
writer.writeReverse(asArray(header.previousHash))
|
|
579
579
|
writer.writeReverse(asArray(header.merkleRoot))
|
|
580
|
-
writer.
|
|
581
|
-
writer.
|
|
582
|
-
writer.
|
|
580
|
+
writer.writeUInt32LE(header.time)
|
|
581
|
+
writer.writeUInt32LE(header.bits)
|
|
582
|
+
writer.writeUInt32LE(header.nonce)
|
|
583
583
|
const r = writer.toArray()
|
|
584
584
|
return r
|
|
585
585
|
}
|
|
@@ -5,6 +5,7 @@ import { Chain } from '../../../../sdk'
|
|
|
5
5
|
import { createDefaultNoDbChaintracksOptions } from '../createDefaultNoDbChaintracksOptions'
|
|
6
6
|
import { ChaintracksFs } from '../util/ChaintracksFs'
|
|
7
7
|
import { LocalCdnServer } from './LocalCdnServer'
|
|
8
|
+
import { _tu } from '../../../../../test/utils/TestUtilsWalletStorage'
|
|
8
9
|
|
|
9
10
|
const rootFolder = './src/services/chaintracker/chaintracks/__tests/data'
|
|
10
11
|
|
|
@@ -25,10 +26,12 @@ describe('Chaintracks tests', () => {
|
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
test('1 NoDb mainnet', async () => {
|
|
29
|
+
if (_tu.noEnv('main')) return
|
|
28
30
|
await NoDbBody('main')
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
test('2 NoDb testnet', async () => {
|
|
34
|
+
if (_tu.noEnv('main')) return
|
|
32
35
|
await NoDbBody('test')
|
|
33
36
|
})
|
|
34
37
|
|
|
@@ -79,10 +79,18 @@ export async function internalizeAction(
|
|
|
79
79
|
*/
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Verifies that the `tx` argument passed to internalizeAction is a valid AtomicBEEF,
|
|
84
|
+
* and the proofs are valid according to the wallet's configured chainTracker.
|
|
85
|
+
* THIS DOES NOT GUARANTEE:
|
|
86
|
+
* 1. That the transaction has been broadcast. (Is known to the network).
|
|
87
|
+
* 2. That the proofs are for the same block as recorded in the wallet's configured storage in the event of a reorg.
|
|
88
|
+
*/
|
|
82
89
|
async function validateAtomicBeef() {
|
|
83
90
|
const ab = Beef.fromBinary(vargs.tx)
|
|
84
91
|
|
|
85
|
-
// TODO: Add support for known txids...
|
|
92
|
+
// TODO: Add support for known txids...which would speed up processing by avoiding a network call,
|
|
93
|
+
// unless a local chaintracker is used.
|
|
86
94
|
|
|
87
95
|
const txValid = await ab.verify(await wallet.getServices().getChainTracker(), false)
|
|
88
96
|
if (!txValid || !ab.atomicTxid) {
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
TransactionOutput,
|
|
7
7
|
Beef
|
|
8
8
|
} from '@bsv/sdk'
|
|
9
|
-
import { shareReqsWithWorld } from './processAction'
|
|
9
|
+
import { GetReqsAndBeefResult, shareReqsWithWorld } from './processAction'
|
|
10
10
|
import { StorageProvider } from '../StorageProvider'
|
|
11
|
-
import { AuthId, StorageInternalizeActionResult } from '../../sdk/WalletStorage.interfaces'
|
|
11
|
+
import { AuthId, StorageInternalizeActionResult, StorageProvenOrReq } from '../../sdk/WalletStorage.interfaces'
|
|
12
12
|
import { TableOutput } from '../schema/tables/TableOutput'
|
|
13
13
|
import { TableOutputBasket } from '../schema/tables/TableOutputBasket'
|
|
14
14
|
import { TableTransaction } from '../schema/tables/TableTransaction'
|
|
@@ -17,6 +17,8 @@ import { WERR_INTERNAL, WERR_INVALID_PARAMETER } from '../../sdk/WERR_errors'
|
|
|
17
17
|
import { randomBytesBase64, verifyId, verifyOne, verifyOneOrNone } from '../../utility/utilityHelpers'
|
|
18
18
|
import { TransactionStatus } from '../../sdk/types'
|
|
19
19
|
import { EntityProvenTxReq } from '../schema/entities/EntityProvenTxReq'
|
|
20
|
+
import { blockHash } from '../../services/chaintracker/chaintracks/util/blockHeaderUtilities'
|
|
21
|
+
import { TableProvenTx } from '../index.client'
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Internalize Action allows a wallet to take ownership of outputs in a pre-existing transaction.
|
|
@@ -252,6 +254,21 @@ class InternalizeActionContext {
|
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
/**
|
|
258
|
+
* This is the second time the atomic beef is validated against a chaintracker.
|
|
259
|
+
* The first validation used the originating wallet's configured chaintracker.
|
|
260
|
+
* Now the chaintracker configured for this storage is used.
|
|
261
|
+
* These may be the same, or different.
|
|
262
|
+
*
|
|
263
|
+
* THIS DOES NOT GUARANTEE:
|
|
264
|
+
* 1. That the transaction has been broadcast. (Is known to the network).
|
|
265
|
+
* 2. That the proof(s) are for the same block as recorded in this storage in the event of a reorg.
|
|
266
|
+
*
|
|
267
|
+
* In the event of a reorg, we CAN assume that the proof contained in this beef should replace the proof in storage.
|
|
268
|
+
*
|
|
269
|
+
* @param atomicBeef
|
|
270
|
+
* @returns
|
|
271
|
+
*/
|
|
255
272
|
async validateAtomicBeef(atomicBeef: number[]) {
|
|
256
273
|
const ab = Beef.fromBinary(atomicBeef)
|
|
257
274
|
const txValid = await ab.verify(await this.storage.getServices().getChainTracker(), false)
|
|
@@ -277,14 +294,18 @@ class InternalizeActionContext {
|
|
|
277
294
|
return { ab, tx, txid }
|
|
278
295
|
}
|
|
279
296
|
|
|
280
|
-
async findOrInsertTargetTransaction(satoshis: number,
|
|
297
|
+
async findOrInsertTargetTransaction(satoshis: number, provenTx?: TableProvenTx): Promise<TableTransaction> {
|
|
281
298
|
const now = new Date()
|
|
299
|
+
const provenTxId = provenTx?.provenTxId
|
|
300
|
+
const status: TransactionStatus = provenTx ? 'completed' : 'unproven'
|
|
282
301
|
const newTx: TableTransaction = {
|
|
283
302
|
created_at: now,
|
|
284
303
|
updated_at: now,
|
|
285
304
|
transactionId: 0,
|
|
286
305
|
|
|
306
|
+
provenTxId,
|
|
287
307
|
status,
|
|
308
|
+
|
|
288
309
|
satoshis,
|
|
289
310
|
|
|
290
311
|
version: this.tx.version,
|
|
@@ -301,10 +322,14 @@ class InternalizeActionContext {
|
|
|
301
322
|
const tr = await this.storage.findOrInsertTransaction(newTx)
|
|
302
323
|
if (!tr.isNew) {
|
|
303
324
|
if (!this.isMerge)
|
|
325
|
+
// For now, only allow transaction record to pre-exist if it was there at the start.
|
|
304
326
|
throw new WERR_INVALID_PARAMETER('tx', `target transaction of internalizeAction is undergoing active changes.`)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
327
|
+
const update: Partial<TableTransaction> = { satoshis: tr.tx.satoshis + satoshis }
|
|
328
|
+
if (provenTx) {
|
|
329
|
+
update.provenTxId = provenTxId
|
|
330
|
+
update.status = status
|
|
331
|
+
}
|
|
332
|
+
await this.storage.updateTransaction(tr.tx.transactionId!, update)
|
|
308
333
|
}
|
|
309
334
|
return tr.tx
|
|
310
335
|
}
|
|
@@ -325,30 +350,83 @@ class InternalizeActionContext {
|
|
|
325
350
|
}
|
|
326
351
|
}
|
|
327
352
|
|
|
353
|
+
/**
|
|
354
|
+
* internalize output(s) from a transaction with txid unknown to storage.
|
|
355
|
+
*/
|
|
328
356
|
async newInternalize() {
|
|
329
|
-
|
|
357
|
+
// Check if the transaction has a merkle path proof (BUMP)
|
|
358
|
+
const btx = this.ab.findTxid(this.txid)
|
|
359
|
+
if (!btx) throw new WERR_INTERNAL(`Could not find transaction ${this.txid} in AtomicBEEF`)
|
|
360
|
+
const bump = this.ab.findBump(this.txid)
|
|
361
|
+
|
|
362
|
+
let pr: StorageProvenOrReq = { isNew: false, proven: undefined, req: undefined }
|
|
363
|
+
|
|
364
|
+
if (bump) {
|
|
365
|
+
// The presence bump indicates the transaction has already been mined.
|
|
366
|
+
// Verify a provenTx record exist before creating a new transaction with completed status...
|
|
367
|
+
// Which normally means creating a new provenTx record.
|
|
368
|
+
const now = new Date()
|
|
369
|
+
const merkleRoot = bump.computeRoot(this.txid)
|
|
370
|
+
const indexEntry = bump.path[0].find(p => p.hash === this.txid)
|
|
371
|
+
if (!indexEntry) {
|
|
372
|
+
throw new WERR_INTERNAL(
|
|
373
|
+
`Could not determine transaction index for txid ${this.txid} in bump path. Expected to find txid in bump.path[0]: ${JSON.stringify(bump.path[0])}`
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
const index = indexEntry.offset
|
|
377
|
+
const header = await this.storage.getServices().getHeaderForHeight(bump.blockHeight)
|
|
378
|
+
if (!header) {
|
|
379
|
+
throw new WERR_INTERNAL(`Block header not found for height ${bump.blockHeight}`)
|
|
380
|
+
}
|
|
381
|
+
const hash = blockHash(header)
|
|
382
|
+
const provenTxR = await this.storage.findOrInsertProvenTx({
|
|
383
|
+
created_at: now,
|
|
384
|
+
updated_at: now,
|
|
385
|
+
provenTxId: 0,
|
|
386
|
+
txid: this.txid,
|
|
387
|
+
height: bump.blockHeight,
|
|
388
|
+
index,
|
|
389
|
+
merklePath: bump.toBinary(),
|
|
390
|
+
rawTx: btx.rawTx!,
|
|
391
|
+
blockHash: hash,
|
|
392
|
+
merkleRoot: merkleRoot
|
|
393
|
+
})
|
|
394
|
+
pr.proven = provenTxR.proven
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.etx = await this.findOrInsertTargetTransaction(this.satoshis, pr.proven)
|
|
330
398
|
|
|
331
399
|
const transactionId = this.etx!.transactionId!
|
|
332
400
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
401
|
+
if (!pr.proven) {
|
|
402
|
+
// beef doesn't include proof of mining for the transaction (etx).
|
|
403
|
+
// the new transaction record has been added to storage, but (baring race conditions)
|
|
404
|
+
// there should be no provenTx or provenTxReq records for this txid.
|
|
405
|
+
//
|
|
406
|
+
// Attempt to create a provenTxReq record for the txid to obtain a proof,
|
|
407
|
+
// while allowing for possible race conditions...
|
|
408
|
+
const newReq = EntityProvenTxReq.fromTxid(this.txid, this.tx.toBinary(), this.args.tx)
|
|
409
|
+
newReq.status = 'unsent'
|
|
410
|
+
// this history and notify will be merged into an existing req if it exists.
|
|
411
|
+
newReq.addHistoryNote({ what: 'internalizeAction', userId: this.userId })
|
|
412
|
+
newReq.addNotifyTransactionId(transactionId)
|
|
413
|
+
pr = await this.storage.getProvenOrReq(this.txid, newReq.toApi())
|
|
414
|
+
}
|
|
342
415
|
|
|
343
416
|
if (pr.isNew) {
|
|
344
|
-
// This storage
|
|
345
|
-
|
|
346
|
-
// TODO Can we immediately prove this txid?
|
|
347
|
-
// TODO Do full validation on the transaction?
|
|
348
|
-
|
|
417
|
+
// This storage didn't know about this txid and the beef didn't include a mining proof.
|
|
418
|
+
// Assume the transaction has never been broadcast.
|
|
349
419
|
// Attempt to broadcast it to the network, throwing an error if it fails.
|
|
350
420
|
|
|
351
|
-
|
|
421
|
+
// Skip looking up txids and building an aggregate beef,
|
|
422
|
+
// just this one txid and the already validated atomic beef.
|
|
423
|
+
// The beef may contain additional unbroadcast transactions which
|
|
424
|
+
// we don't care about.
|
|
425
|
+
const r: GetReqsAndBeefResult = {
|
|
426
|
+
beef: Beef.fromBinary(this.args.tx),
|
|
427
|
+
details: [{ txid: this.txid, status: 'readyToSend', req: pr.req }]
|
|
428
|
+
}
|
|
429
|
+
const { swr, ndr } = await shareReqsWithWorld(this.storage, this.userId, [], false, r)
|
|
352
430
|
if (ndr![0].status !== 'success') {
|
|
353
431
|
this.r.sendWithResults = swr
|
|
354
432
|
this.r.notDelayedResults = ndr
|
|
@@ -107,20 +107,22 @@ export interface PostBeefResultForTxidApi {
|
|
|
107
107
|
* @param userId
|
|
108
108
|
* @param txids
|
|
109
109
|
* @param isDelayed
|
|
110
|
+
* @param r Optional. Ignores txids and allows ProvenTxReqs and merged beef to be passed in.
|
|
110
111
|
*/
|
|
111
112
|
export async function shareReqsWithWorld(
|
|
112
113
|
storage: StorageProvider,
|
|
113
114
|
userId: number,
|
|
114
115
|
txids: string[],
|
|
115
|
-
isDelayed: boolean
|
|
116
|
+
isDelayed: boolean,
|
|
117
|
+
r?: GetReqsAndBeefResult
|
|
116
118
|
): Promise<{ swr: SendWithResult[]; ndr: ReviewActionResult[] | undefined }> {
|
|
117
119
|
let swr: SendWithResult[] = []
|
|
118
120
|
let ndr: ReviewActionResult[] | undefined = undefined
|
|
119
121
|
|
|
120
|
-
if (txids.length < 1) return { swr, ndr }
|
|
122
|
+
if (!r && txids.length < 1) return { swr, ndr }
|
|
121
123
|
|
|
122
124
|
// Collect what we know about these sendWith transaction txids from storage.
|
|
123
|
-
|
|
125
|
+
r ||= await storage.getReqsAndBeefToShareWithWorld(txids, [])
|
|
124
126
|
|
|
125
127
|
const readyToSendReqs: EntityProvenTxReq[] = []
|
|
126
128
|
for (const getReq of r.details) {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { Utils } from '@bsv/sdk'
|
|
2
|
+
import {
|
|
3
|
+
blockHash,
|
|
4
|
+
deserializeBaseBlockHeader
|
|
5
|
+
} from '../../../src/services/chaintracker/chaintracks/util/blockHeaderUtilities'
|
|
1
6
|
import { _tu, TestWalletNoSetup } from '../../utils/TestUtilsWalletStorage'
|
|
2
7
|
|
|
3
8
|
const includeTestChaintracks = false
|
|
@@ -38,6 +43,11 @@ describe('getHeaderForHeight tests', () => {
|
|
|
38
43
|
// Query an existing valid block height
|
|
39
44
|
const height = 1 // Ensure this height exists in the test database
|
|
40
45
|
const result = await wallet.getHeaderForHeight({ height })
|
|
46
|
+
const headerHex = result.header
|
|
47
|
+
const headerA = Utils.toArray(headerHex, 'hex')
|
|
48
|
+
const hash = blockHash(headerA)
|
|
49
|
+
expect(hash).toBe('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206')
|
|
50
|
+
const header = deserializeBaseBlockHeader(headerA)
|
|
41
51
|
|
|
42
52
|
expect(result).toHaveProperty('header')
|
|
43
53
|
expect(typeof result.header).toBe('string')
|