@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
505 lines (436 loc) • 16.1 kB
text/typescript
import * as bsv from '@bsv/sdk'
import { createSyncMap, sdk, sha256Hash, SyncMap } from '../../../../../src'
import { TestUtilsWalletStorage as _tu, TestWalletNoSetup } from '../../../../../test/utils/TestUtilsWalletStorage'
import { EntityProvenTx } from '../EntityProvenTx'
describe('ProvenTx class method tests', () => {
jest.setTimeout(99999999)
const env = _tu.getEnv('test')
const ctxs: TestWalletNoSetup[] = []
const ctxs2: TestWalletNoSetup[] = []
beforeAll(async () => {
if (env.runMySQL) {
ctxs.push(await _tu.createLegacyWalletMySQLCopy('ProvenTxTests'))
ctxs2.push(await _tu.createLegacyWalletMySQLCopy('ProvenTxTests2'))
}
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('ProvenTxTests'))
ctxs2.push(await _tu.createLegacyWalletSQLiteCopy('ProvenTxTests2'))
})
afterAll(async () => {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
for (const ctx of ctxs2) {
await ctx.storage.destroy()
}
})
test('0_fromTxid: valid txid with rawTx and Merkle proof (real database)', async () => {
const ctx = ctxs[0]
const txid = '2795b293c698b2244147aaba745db887a632d21990c474df46d842ec3e52f122' // Using a valid txid from the table
// Fetch the rawTx and Merkle proof data directly from the database
const provenTxRecord = await ctx.activeStorage.findProvenTxs({
partial: { txid }
})
expect(provenTxRecord.length).toBeGreaterThan(0) // Ensure the record exists in the database
const rawTx = provenTxRecord[0]?.rawTx
const height = provenTxRecord[0]?.height
const blockHash = provenTxRecord[0]?.blockHash
const merkleRoot = provenTxRecord[0]?.merkleRoot
const merklePathBinary = provenTxRecord[0]?.merklePath || []
const services: sdk.WalletServices = {
chain: 'test',
hashOutputScript: (script: string): string => {
const hash = bsv.Utils.toHex(sha256Hash(bsv.Utils.toArray(script, 'hex')))
return hash
},
getRawTx: async (requestedTxid: string) => {
if (requestedTxid === txid) {
return { txid: requestedTxid, rawTx }
}
throw new Error('Unexpected txid')
},
getMerklePath: async (requestedTxid: string) => {
if (requestedTxid === txid) {
return {
merklePath: {
path: [[{ hash: txid, offset: 0 }]],
blockHeight: height,
toBinary: () => merklePathBinary,
computeRoot: () => merkleRoot,
verifyProof: () => true,
toHex: () => Buffer.from(merklePathBinary).toString('hex'),
indexOf: () => 0,
findOrComputeLeaf: () => ({ hash: txid, offset: 0 }),
verify: () => true,
combine: () => ({}) as bsv.MerklePath,
trim: () => ({}) as bsv.MerklePath
} as unknown as bsv.MerklePath,
header: {
version: 1,
previousHash: 'prev-hash',
merkleRoot: merkleRoot,
time: 1610000000,
bits: 123456,
nonce: 78910,
height: height,
hash: blockHash
},
name: 'mock-service'
}
}
throw new Error('Unexpected txid')
},
getChainTracker: () =>
Promise.resolve({
isValidRootForHeight: async (root: string, height: number) => true,
currentHeight: async () => height
}),
getHeaderForHeight: async () => Promise.resolve([1, 2, 3, 4]),
getHeight: async () => height,
getBsvExchangeRate: async () => 0,
getFiatExchangeRate: async () => 1,
postBeef: async () => [],
getStatusForTxids: async () => ({
name: 'mock-service',
status: 'success',
results: []
}),
isUtxo: async () => true,
getUtxoStatus: async () => ({
name: 'mock-service',
status: 'success',
isUtxo: true,
details: []
}),
getScriptHashHistory: async () => ({
name: 'mock-service',
status: 'success',
history: []
}),
hashToHeader: async () => ({
version: 1,
previousHash: 'prev-hash',
merkleRoot: merkleRoot,
time: 1610000000,
bits: 123456,
nonce: 78910,
height: height,
hash: blockHash
}),
nLockTimeIsFinal: async () => true,
getBeefForTxid: async () => new bsv.Beef(),
getServicesCallHistory: () => ({
version: 1,
getMerklePath: { serviceName: '', historyByProvider: {} },
getRawTx: { serviceName: '', historyByProvider: {} },
postBeef: { serviceName: '', historyByProvider: {} },
getStatusForTxids: { serviceName: '', historyByProvider: {} },
getUtxoStatus: { serviceName: '', historyByProvider: {} },
getScriptHashHistory: { serviceName: '', historyByProvider: {} },
updateFiatExchangeRates: { serviceName: '', historyByProvider: {} }
})
}
// Call the method under test
const result = await EntityProvenTx.fromTxid(txid, services)
// Validate the ProvenTx result
expect(result.proven).toBeDefined()
expect(result.proven!.txid).toBe(txid)
// Validate Merkle proof details
expect(result.proven!.height).toBe(height)
expect(Buffer.from(result.proven!.merklePath).toString('hex')).toEqual(
Buffer.from(merklePathBinary).toString('hex')
)
expect(result.proven!.blockHash).toBe(blockHash)
expect(result.proven!.merkleRoot).toBe(merkleRoot)
expect(result.rawTx).toEqual(rawTx)
})
test('1_fromTxid: txid with no rawTx available', async () => {
const ctx = ctxs[0]
const txid = 'missing-txid'
const services: sdk.WalletServices = ctx.services
// Call the method under test
const result = await EntityProvenTx.fromTxid(txid, services)
// Validate that ProvenTx could not be created
expect(result.proven).toBeUndefined()
expect(result.rawTx).toBeUndefined()
})
test('2_fromTxid: txid with no Merkle proof available', async () => {
const ctx = ctxs[0]
const txid = 'no-merkle-proof-txid'
const services: sdk.WalletServices = ctx.services
// Verify the rawTx and Merkle proof
const rawTx = await services.getRawTx(txid)
const merkleProof = await services.getMerklePath(txid)
// Call the method under test
const result = await EntityProvenTx.fromTxid(txid, services)
// Validate the ProvenTx result
expect(result.proven).toBeUndefined()
expect(result.rawTx).toEqual(rawTx.rawTx)
})
test('3_ProvenTx getters and setters', () => {
// Mock data to initialize the ProvenTx entity
const mockData = {
provenTxId: 1,
created_at: new Date('2025-01-01T00:00:00Z'),
updated_at: new Date('2025-01-02T00:00:00Z'),
txid: '2795b293c698b2244147aaba745db887a632d21990c474df46d842ec3e52f122',
height: 123,
index: 0,
merklePath: [0x04, 0x05, 0x06],
rawTx: [0x01, 0x02, 0x03],
blockHash: 'mock-block-hash',
merkleRoot: 'mock-merkle-root'
}
// Initialize the ProvenTx entity with mock data
const provenTx = new EntityProvenTx(mockData)
// Validate getters
expect(provenTx.provenTxId).toBe(mockData.provenTxId)
expect(provenTx.created_at).toEqual(mockData.created_at)
expect(provenTx.updated_at).toEqual(mockData.updated_at)
expect(provenTx.txid).toBe(mockData.txid)
expect(provenTx.height).toBe(mockData.height)
expect(provenTx.index).toBe(mockData.index)
expect(provenTx.merklePath).toEqual(mockData.merklePath)
expect(provenTx.rawTx).toEqual(mockData.rawTx)
expect(provenTx.blockHash).toBe(mockData.blockHash)
expect(provenTx.merkleRoot).toBe(mockData.merkleRoot)
// Validate setters
provenTx.provenTxId = 2
provenTx.created_at = new Date('2025-02-01T00:00:00Z')
provenTx.updated_at = new Date('2025-02-02T00:00:00Z')
provenTx.txid = 'a3b2f0935c7b5bb7a841a09e535c13be86f4df0e7a91cebdc33812bfcc0eb9d7'
provenTx.height = 456
provenTx.index = 1
provenTx.merklePath = [0x07, 0x08, 0x09]
provenTx.rawTx = [0x0a, 0x0b, 0x0c]
provenTx.blockHash = 'new-block-hash'
provenTx.merkleRoot = 'new-merkle-root'
// Validate updated values
expect(provenTx.provenTxId).toBe(2)
expect(provenTx.created_at).toEqual(new Date('2025-02-01T00:00:00Z'))
expect(provenTx.updated_at).toEqual(new Date('2025-02-02T00:00:00Z'))
expect(provenTx.txid).toBe('a3b2f0935c7b5bb7a841a09e535c13be86f4df0e7a91cebdc33812bfcc0eb9d7')
expect(provenTx.height).toBe(456)
expect(provenTx.index).toBe(1)
expect(provenTx.merklePath).toEqual([0x07, 0x08, 0x09])
expect(provenTx.rawTx).toEqual([0x0a, 0x0b, 0x0c])
expect(provenTx.blockHash).toBe('new-block-hash')
expect(provenTx.merkleRoot).toBe('new-merkle-root')
// Validate overridden methods
expect(provenTx.id).toBe(2)
expect(provenTx.entityName).toBe('provenTx')
expect(provenTx.entityTable).toBe('proven_txs')
// Update id via overridden setter
provenTx.id = 3
expect(provenTx.provenTxId).toBe(3)
})
// Test: equals identifies matching ProvenTx entities
test('4_equals: identifies matching ProvenTx entities', async () => {
const ctx1 = ctxs[0]
const ctx2 = ctxs2[0]
// Insert a ProvenTx into the first database
const provenTx1 = new EntityProvenTx({
provenTxId: 401,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
await ctx1.activeStorage.insertProvenTx(provenTx1.toApi())
// Insert a matching ProvenTx into the second database
const provenTx2 = new EntityProvenTx({
provenTxId: 401,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
await ctx2.activeStorage.insertProvenTx(provenTx2.toApi())
const syncMap = createSyncMap()
syncMap.provenTx.idMap = { [provenTx1.provenTxId]: provenTx2.provenTxId }
// Verify the ProvenTx entities match
expect(provenTx1.equals(provenTx2.toApi(), syncMap)).toBe(true)
})
// Test: equals identifies non-matching txid
test('5_equals: identifies non-matching txid', async () => {
const provenTx1 = new EntityProvenTx({
provenTxId: 102,
txid: 'txid1',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
const provenTx2 = new EntityProvenTx({
provenTxId: 103,
txid: 'txid2',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
expect(provenTx1.equals(provenTx2.toApi())).toBe(false)
})
// Test: equals identifies non-matching height
test('6_equals: identifies non-matching height', async () => {
const provenTx1 = new EntityProvenTx({
provenTxId: 104,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
const provenTx2 = new EntityProvenTx({
provenTxId: 105,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588741,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
expect(provenTx1.equals(provenTx2.toApi())).toBe(false)
})
// Test: equals identifies non-matching merklePath
test('7_equals: identifies non-matching merklePath', async () => {
const provenTx1 = new EntityProvenTx({
provenTxId: 106,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
const provenTx2 = new EntityProvenTx({
provenTxId: 107,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 4],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
expect(provenTx1.equals(provenTx2.toApi())).toBe(false)
})
// Test: equals identifies non-matching syncMap
test('8_equals: identifies non-matching syncMap', async () => {
const provenTx1 = new EntityProvenTx({
provenTxId: 108,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
const provenTx2 = new EntityProvenTx({
provenTxId: 109,
txid: 'valid-txid',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 1588740,
index: 0,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash',
merkleRoot: 'merkle-root'
})
const syncMap = createSyncMap()
syncMap.provenTx.idMap = { 108: 999 }
expect(provenTx1.equals(provenTx2.toApi(), syncMap)).toBe(false)
})
test('9_equals: provenTxId mismatch without syncMap', async () => {
const ctx1 = ctxs[0]
const ctx2 = ctxs2[0]
// Insert a ProvenTx record into the first database
const tx1 = new EntityProvenTx({
provenTxId: 405,
txid: 'txid1',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 100,
index: 1,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash-1',
merkleRoot: 'merkle-root-1'
})
await ctx1.activeStorage.insertProvenTx(tx1.toApi())
// Insert a different ProvenTx record into the second database with a mismatched provenTxId
const tx2 = new EntityProvenTx({
provenTxId: 406,
txid: 'txid1',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 100,
index: 1,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash-1',
merkleRoot: 'merkle-root-1'
})
await ctx2.activeStorage.insertProvenTx(tx2.toApi())
// Verify that `equals` returns false because provenTxId mismatch without a syncMap
expect(tx1.equals(tx2.toApi())).toBe(false)
})
test('10_mergeExisting: always returns false', async () => {
const ctx = ctxs[0]
// Create a ProvenTx entity
const provenTx = new EntityProvenTx({
provenTxId: 101,
txid: 'txid1',
created_at: new Date('2023-01-01'),
updated_at: new Date('2023-01-02'),
height: 100,
index: 1,
merklePath: [1, 2, 3],
rawTx: [4, 5, 6],
blockHash: 'block-hash-1',
merkleRoot: 'merkle-root-1'
})
const mockSyncMap = createSyncMap()
mockSyncMap.provenTx.idMap = { [provenTx.provenTxId]: provenTx.provenTxId }
// Create mock storage, syncMap, and trx token
const mockStorage = ctx.activeStorage
const mockTrx: sdk.TrxToken = {}
// Call the mergeExisting method
const result = await provenTx.mergeExisting(mockStorage, new Date(), provenTx.toApi(), mockSyncMap, mockTrx)
// Assert that it always returns false
expect(result).toBe(false)
})
})