Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/packages/wallet/wallet-toolbox-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: pkg-wallet-toolbox-client
title: '@bsv/wallet-toolbox-client'
kind: package
domain: wallet
version: '2.6.0'
version: '2.6.1'
last_updated: '2026-08-05'
last_verified: '2026-08-05'
review_cadence_days: 30
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/wallet/wallet-toolbox-mobile.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: pkg-wallet-toolbox-mobile
title: '@bsv/wallet-toolbox-mobile'
kind: package
domain: wallet
version: '2.6.0'
version: '2.6.1'
last_updated: '2026-08-05'
last_verified: '2026-08-05'
review_cadence_days: 30
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/wallet/wallet-toolbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: '@bsv/wallet-toolbox'
kind: package
domain: wallet
npm: '@bsv/wallet-toolbox'
version: '2.6.0'
version: '2.6.1'
last_updated: '2026-08-05'
last_verified: '2026-08-05'
review_cadence_days: 30
Expand Down
78 changes: 39 additions & 39 deletions docs/reference/package-api-migrations.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/reference/stack-facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ authorized release action.
| wallet | `@bsv/btms` | `1.1.4` | node-library | node-cjs, node-esm | node | `>=22` | [packages/wallet/btms](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms) |
| wallet | `@bsv/btms-permission-module` | `1.1.3` | node-library | node-esm | node | `>=22` | [packages/wallet/btms-permission-module](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms-permission-module) |
| wallet | `@bsv/wallet-relay` | `0.3.4` | cli-library | browser-bundler, browser-esm, cli, node-cjs, node-esm | browser, node | `>=22` | [packages/wallet/ts-wallet-relay](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/ts-wallet-relay) |
| wallet | `@bsv/wallet-toolbox` | `2.6.0` | node-library | node-cjs | node | `>=22` | [packages/wallet/wallet-toolbox](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox) |
| wallet | `@bsv/wallet-toolbox-client` | `2.6.0` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm | browser, node | `>=22` | [packages/wallet/wallet-toolbox/client](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox/client) |
| wallet | `@bsv/wallet-toolbox-mobile` | `2.6.0` | react-native-library | react-native-metro | react-native | `>=22` | [packages/wallet/wallet-toolbox/mobile](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox/mobile) |
| wallet | `@bsv/wallet-toolbox` | `2.6.1` | node-library | node-cjs | node | `>=22` | [packages/wallet/wallet-toolbox](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox) |
| wallet | `@bsv/wallet-toolbox-client` | `2.6.1` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm | browser, node | `>=22` | [packages/wallet/wallet-toolbox/client](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox/client) |
| wallet | `@bsv/wallet-toolbox-mobile` | `2.6.1` | react-native-library | react-native-metro | react-native | `>=22` | [packages/wallet/wallet-toolbox/mobile](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/wallet-toolbox/mobile) |

## Standalone infrastructure manifests

Expand Down
24 changes: 12 additions & 12 deletions governance/package-release-notes.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,24 +194,24 @@
},
{
"name": "@bsv/wallet-toolbox",
"publishedVersion": "2.5.0",
"releaseType": "minor",
"summary": "Adds resource-profile-aware Wallet Storage RPC pagination, array and response budgets, bounded authenticated response capture, durable payment replay and session schemas, and double-slash compatibility; makes fragmented createAction atomic and set-based with batched proof reads, validation, output writes, shared derivation work, coalesced session touches, and complete privacy-safe phase timings; makes UMP account lookup resilient to partial overlay failure; and refreshes ChainTracks with credential-free Arcade/go-chaintracks bulk and SSE sources, five-network genesis correctness, prioritized failover, local last-good operation, source health, browser-safe reconnects, and a rate-limited keyless WhatsOnChain fallback.",
"migration": "List/find RPC calls that omit a limit now receive the profile default (1,000 in standard), and larger caller limits are rejected above the operator maximum. Existing calls and storage data remain compatible; migrations add auth_sessions, payment_replays, and createAction indexes automatically, and storage-provider additions retain backward-compatible fallbacks. Applications may now enter new-user flow when at least one overlay host returns a clean empty result despite malformed or unavailable peers; one verified token still establishes an existing account, multiple unresolved verified tokens remain an error, and WAB existing-account continuity still blocks replacement-wallet onboarding. The default ChainTracks client for mainnet, testnet, and TerraTestNet now uses each public Arcade v2 endpoint without a credential; explicit ChaintracksClientApi injection and legacy v1 URLs remain supported. STN and Terra Scaling TestNet require an explicit STN_CHAINTRACKS_URL/TSTN_CHAINTRACKS_URL or corresponding Arcade URL. A WhatsOnChain key is optional, anonymous fallback stays below the documented public rate, and ChainTracks header/info requests retry anonymously when a configured key is rejected. Browser/mobile CORS expectations, wallet results, BRC-103/104, AuthFetch, JSON-RPC, and wallet wire behavior are otherwise unchanged."
"publishedVersion": "2.6.0",
"releaseType": "patch",
"summary": "Fixes credential-free Arcade/go-chaintracks initialization when ChainTracks bulk storage is empty while retaining continuity, proof-of-work, file-integrity, chainwork, and exact-network genesis validation.",
"migration": "No consumer migration is required. Fresh keyless ChainTracks deployments can now bootstrap from a valid height-zero Arcade batch; existing CDN-backed, persisted, browser, mobile, explicit-client, and legacy v1 configurations remain compatible and keep the same trust checks."
},
{
"name": "@bsv/wallet-toolbox-client",
"publishedVersion": "2.5.0",
"releaseType": "minor",
"summary": "Carries the lockstep browser build with the bounded remote-storage contract, batched proof assembly, linear funding and signing work, canonical P2PKH verification, expired-reservation filtering, complete privacy-safe createAction timings, resilient UMP account lookup, and the browser-safe credential-free ChainTracks v2 client with reconnecting SSE and five-network header support.",
"migration": "No browser consumer migration is required. Remote storage callers may provide explicit list limits when they need a value other than the operator profile default, and browser wallets receive the resilient partial-host UMP lookup behavior described for @bsv/wallet-toolbox. Mainnet, testnet, and TerraTestNet ChainTracks defaults now use public credential-free Arcade v2 endpoints; explicit clients and legacy v1 URLs remain compatible, while STN and Terra Scaling TestNet require an explicit endpoint. Older compatible SDK peers retain the validated sequential proof fallback, IndexedDB upgrades automatically, and CORS, BRC-103/104, AuthFetch, browser entry points, JSON-RPC, and remote storage wire behavior remain compatible."
"publishedVersion": "2.6.0",
"releaseType": "patch",
"summary": "Carries the lockstep browser build with the corrected credential-free Arcade/go-chaintracks empty-store bootstrap and unchanged validation guarantees.",
"migration": "No browser consumer migration is required. Browser entry points, CORS behavior, explicit clients, legacy v1 URLs, persisted storage, and wire contracts remain compatible."
},
{
"name": "@bsv/wallet-toolbox-mobile",
"publishedVersion": "2.5.0",
"releaseType": "minor",
"summary": "Carries the lockstep mobile build with the bounded remote-storage contract, batched proof assembly, linear funding and signing work, canonical P2PKH verification, expired-reservation filtering, complete privacy-safe createAction timings, resilient UMP account lookup, and the mobile-safe credential-free ChainTracks v2 client with reconnecting SSE and five-network header support.",
"migration": "No mobile consumer migration is required. Remote storage callers may provide explicit list limits when they need a value other than the operator profile default, and mobile wallets receive the resilient partial-host UMP lookup behavior described for @bsv/wallet-toolbox. Mainnet, testnet, and TerraTestNet ChainTracks defaults now use public credential-free Arcade v2 endpoints; explicit clients and legacy v1 URLs remain compatible, while STN and Terra Scaling TestNet require an explicit endpoint. Older compatible SDK peers retain the validated sequential proof fallback, and BRC-103/104, AuthFetch, React Native, the mobile bridge, JSON-RPC, and remote storage wire behavior remain compatible."
"publishedVersion": "2.6.0",
"releaseType": "patch",
"summary": "Carries the lockstep mobile build with the corrected credential-free Arcade/go-chaintracks empty-store bootstrap and unchanged validation guarantees.",
"migration": "No mobile consumer migration is required. React Native entry points, the mobile bridge, explicit clients, legacy v1 URLs, persisted storage, and wire contracts remain compatible."
},
{
"name": "create-bsv-app",
Expand Down
6 changes: 3 additions & 3 deletions governance/repository-health/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@
"@bsv/btms": "1.1.4",
"@bsv/btms-permission-module": "1.1.3",
"@bsv/wallet-relay": "0.3.4",
"@bsv/wallet-toolbox-client": "2.6.0",
"@bsv/wallet-toolbox-mobile": "2.6.0",
"@bsv/wallet-toolbox": "2.6.0"
"@bsv/wallet-toolbox-client": "2.6.1",
"@bsv/wallet-toolbox-mobile": "2.6.1",
"@bsv/wallet-toolbox": "2.6.1"
}
}
4 changes: 4 additions & 0 deletions packages/wallet/wallet-toolbox/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ attention to changes that materially alter behavior or extend functionality.

## wallet-toolbox (unreleased)

- Fix credential-free Arcade/go-chaintracks bootstrap when ChainTracks bulk
storage is empty. A first batch is now accepted only from height zero and is
still checked for continuity, proof of work, file integrity, and the exact
configured network genesis before it becomes available.
- Make ChainTracks credential-free by default on mainnet, testnet, and
TerraTestNet through the public Arcade/go-chaintracks v2 HTTP and SSE APIs.
Add explicit STN and Terra Scaling TestNet support, exact per-network genesis
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wallet-toolbox/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/wallet-toolbox-client",
"version": "2.6.0",
"version": "2.6.1",
"type": "module",
"sideEffects": false,
"engines": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"esbuild": {
"raw": 1205000,
"gzip": 330000,
"gzip": 330100,
"brotli": 268000
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wallet-toolbox/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/wallet-toolbox-mobile",
"version": "2.6.0",
"version": "2.6.1",
"type": "module",
"sideEffects": false,
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wallet-toolbox/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/wallet-toolbox",
"version": "2.6.0",
"version": "2.6.1",
"sideEffects": false,
"type": "commonjs",
"engines": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ChaintracksClientApi } from '../../Api/ChaintracksClientApi'
import { ChaintracksStorageBase } from '../../Storage/ChaintracksStorageBase'
import { ChaintracksStorageNoDb } from '../../Storage/ChaintracksStorageNoDb'
import { BulkFileDataManager } from '../../util/BulkFileDataManager'
import { ChaintracksFs } from '../../util/ChaintracksFs'
import { HeightRange } from '../../util/HeightRange'
import { serializeBaseBlockHeader } from '../../util/blockHeaderUtilities'
import { BulkIngestorChaintracks } from '../BulkIngestorChaintracks'
Expand Down Expand Up @@ -79,6 +82,48 @@ describe('BulkIngestorChaintracks', () => {
expect(result.map(header => header.height)).toEqual([0, 1, 2])
})

test('bootstraps real empty storage from a validated Arcade genesis batch', async () => {
const data = await ChaintracksFs.readFile(
'./src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_0.headers'
)
const getHeaders = jest.fn(async () => toHex([...data]))
const remote = {
getChain: jest.fn(async () => 'main'),
getHeaders
} as unknown as ChaintracksClientApi
const storageOptions = ChaintracksStorageBase.createStorageBaseOptions('main')
storageOptions.bulkFileDataManager = new BulkFileDataManager({
chain: 'main',
maxPerFile: 100,
maxRetained: 2
})
const storage = new ChaintracksStorageNoDb(storageOptions)
const ingestor = new BulkIngestorChaintracks({
chain: 'main',
jsonResource: 'mainNetBlockHeaders.json',
chaintracks: remote,
maxHeadersPerRequest: 100
})
await ingestor.setStorage(storage, () => {})

await expect(
ingestor.fetchHeaders(
{ bulk: HeightRange.empty, live: HeightRange.empty },
new HeightRange(0, 99),
new HeightRange(0, 99),
[]
)
).resolves.toEqual([])

expect(getHeaders).toHaveBeenCalledWith(0, 100)
await expect(storage.getAvailableHeightRanges()).resolves.toMatchObject({
bulk: { minHeight: 0, maxHeight: 99 }
})
await expect(storage.bulkManager.findHeaderForHeightOrUndefined(0)).resolves.toMatchObject({
hash: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
})
})

test('rejects an upstream configured for another network', async () => {
const remote = {
getChain: jest.fn(async () => 'test')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,18 @@ export class BulkFileDataManager {
))

if (newBulkHeaders.length === 0) return
if (lbf == null || nextHeight !== newBulkHeaders[0].height) {
if (nextHeight !== newBulkHeaders[0].height) {
throw new WERR_INVALID_PARAMETER('newBulkHeaders', 'an extension of existing bulk headers')
}
if (!lbf.lastHash) throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`)
if (lbf != null && !lbf.lastHash) {
throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`)
}

const prevChainWork = lbf?.lastChainWork ?? '00'.repeat(32)
const prevHash = lbf?.lastHash ?? '00'.repeat(32)

const lastChainWork = incrementalChainWork
? addWork(incrementalChainWork, lbf.lastChainWork)
? addWork(incrementalChainWork, prevChainWork)
: computeChainWorkFromHeaders(newBulkHeaders, lbf)

const data = serializeBaseBlockHeaders(newBulkHeaders)
Expand All @@ -308,9 +313,9 @@ export class BulkFileDataManager {
fileName: 'incremental',
firstHeight: newBulkHeaders[0].height,
count: newBulkHeaders.length,
prevChainWork: lbf.lastChainWork,
prevChainWork,
lastChainWork,
prevHash: lbf.lastHash,
prevHash,
lastHash: newBulkHeaders.at(-1)!.hash,
fileHash,
data
Expand Down Expand Up @@ -952,12 +957,13 @@ function trimAlreadyStoredHeaders(

/**
* Computes `lastChainWork` for a sequence of new bulk headers extending `lbf`,
* validating that the sequence is contiguous.
* or beginning at genesis when bulk storage is empty, validating that the
* sequence is contiguous.
*/
function computeChainWorkFromHeaders(headers: BlockHeader[], lbf: BulkHeaderFileInfo): string {
let lastHeight = lbf.firstHeight + lbf.count - 1
let lastHash = lbf.lastHash!
let lastChainWork = lbf.lastChainWork
function computeChainWorkFromHeaders(headers: BlockHeader[], lbf?: BulkHeaderFileInfo): string {
let lastHeight = lbf != null ? lbf.firstHeight + lbf.count - 1 : -1
let lastHash = lbf?.lastHash ?? '00'.repeat(32)
let lastChainWork = lbf?.lastChainWork ?? '00'.repeat(32)
for (const h of headers) {
if (h.height !== lastHeight + 1 || h.previousHash !== lastHash) {
throw new WERR_INVALID_PARAMETER(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('BulkFileDataManager tests', () => {
const chain: Chain = 'main'
const fs = ChaintracksFs
const rootFolder = './src/services/chaintracker/chaintracks/__tests/data'
let headers0_99: BlockHeader[] = []
let headers300_399: BlockHeader[] = []
let headers400_499: BlockHeader[] = []
let server349: LocalCdnServer | undefined
Expand All @@ -25,8 +26,10 @@ describe('BulkFileDataManager tests', () => {
let server499: LocalCdnServer | undefined

beforeAll(async () => {
const data0_99 = await ChaintracksFs.readFile(fs.pathJoin(rootFolder, 'cdnTest499/mainNet_0.headers'))
const data300_399 = await ChaintracksFs.readFile(fs.pathJoin(rootFolder, 'cdnTest499/mainNet_3.headers'))
const data400_499 = await ChaintracksFs.readFile(fs.pathJoin(rootFolder, 'cdnTest499/mainNet_4.headers'))
headers0_99 = deserializeBlockHeaders(0, data0_99)
headers300_399 = deserializeBlockHeaders(300, data300_399)
headers400_499 = deserializeBlockHeaders(400, data400_499)

Expand Down Expand Up @@ -264,7 +267,49 @@ describe('BulkFileDataManager tests', () => {
await storage.destroy()
})

test('6 validates last-file update transitions', async () => {
test('6 bootstraps empty bulk storage from a validated genesis batch', async () => {
const computedWorkManager = createEmptyManager()

await computedWorkManager.mergeIncrementalBlockHeaders(headers0_99)
await computedWorkManager.ReValidate()

const range = await computedWorkManager.getHeightRange()
expect(range.minHeight).toBe(0)
expect(range.maxHeight).toBe(99)
const [file] = await computedWorkManager.getBulkFiles(true)
expect(file).toMatchObject({
firstHeight: 0,
count: 100,
fileName: 'incremental',
prevChainWork: '00'.repeat(32),
prevHash: '00'.repeat(32),
lastHash: headers0_99.at(-1)!.hash,
validated: true
})
expect(await computedWorkManager.findHeaderForHeightOrUndefined(0)).toEqual(headers0_99[0])

const suppliedWorkManager = createEmptyManager()
await expect(
suppliedWorkManager.mergeIncrementalBlockHeaders(headers0_99, file.lastChainWork)
).resolves.toBeUndefined()
await suppliedWorkManager.ReValidate()

const nonGenesisManager = createEmptyManager()
await expect(nonGenesisManager.mergeIncrementalBlockHeaders(headers300_399)).rejects.toThrow(
'an extension of existing bulk headers'
)
expect((await nonGenesisManager.getHeightRange()).isEmpty).toBe(true)

const missingLastHashManager = createEmptyManager()
const incompleteFile = makeBulkFile(0, 99, 'incremental')
incompleteFile.lastHash = undefined
missingLastHashManager['bfds'] = [incompleteFile] as any
await expect(
missingLastHashManager.mergeIncrementalBlockHeaders(headers0_99.slice(99))
).rejects.toThrow('lastHash is not defined for the last bulk file incremental')
})

test('7 validates last-file update transitions', async () => {
const manager = createEmptyManager()
const cdn = makeBulkFile(0, 10, 'mainNet_0.headers', 'https://cdn.example')
manager['bfds'] = [cdn] as any
Expand Down Expand Up @@ -302,7 +347,7 @@ describe('BulkFileDataManager tests', () => {
).resolves.toEqual({ index: 0, truncate: undefined })
})

test('7 validates penultimate-file update transitions', async () => {
test('8 validates penultimate-file update transitions', async () => {
const manager = createEmptyManager()
const cdn = makeBulkFile(0, 10, 'mainNet_0.headers', 'https://cdn.example')
cdn.fileId = 7
Expand Down Expand Up @@ -345,7 +390,7 @@ describe('BulkFileDataManager tests', () => {
).rejects.toThrow('CDN file update followed by an incremental file')
})

test('8 preserves or inherits file identity for valid penultimate updates', async () => {
test('9 preserves or inherits file identity for valid penultimate updates', async () => {
const manager = createEmptyManager()
const cdn = makeBulkFile(0, 10, 'mainNet_0.headers', 'https://cdn.example')
cdn.fileId = 7
Expand Down
Loading