Skip to content

Commit fc34338

Browse files
sirdeggenclaudeCopilot
authored
fix: rebroadcast tx on proof timeout instead of marking invalid (#8)
* fix: rebroadcast transactions instead of marking invalid on proof timeout When a transaction was successfully broadcast but not yet mined, the system previously marked it 'invalid' after max failed proof checks. This caused a double-spend: reviewStatus restored the inputs to spendable, they were spent again, and when the original tx was mined the wallet had two conflicting transactions. Fix 1 (TaskCheckForProofs): reset to 'unsent' + attempts=0 instead of 'invalid' when the proof-attempt limit is exceeded. TaskSendWaiting picks up 'unsent' reqs and rebroadcasts them automatically. Fix 2 (EntityProvenTx.fromReq): same rebroadcast logic for the secondary threshold (attempts > 8, age > 60 min) to prevent an earlier bypass of the main limit. Fix 3 (reviewStatus): add a guard to Step 2 so inputs are only restored to spendable when there is no live ProvenTxReq (in unmined/callback/unconfirmed/sending/unsent status) for the same txid. Defense-in-depth to prevent double-spend even if a req reaches 'failed'. Fix 4 (Monitor): increase unprovenAttemptsLimitTest from 10 to 100, giving ~24h of testnet blocks before a rebroadcast cycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add wasBroadcast flag and maxRebroadcastAttempts circuit breaker (Phases 2 & 4) Phase 2: Add `wasBroadcast` and `rebroadcastAttempts` schema columns to `proven_tx_reqs` (migration 2026-04-30-001). Set `wasBroadcast = true` when a req transitions to 'unmined' via broadcast success (attemptToPostReqsToNetwork) or ARC SSE event. Use the flag in TaskCheckForProofs and EntityProvenTx.fromReq so that proof-timeout resets only happen for genuinely broadcast transactions; unbroadcast reqs are still marked invalid. Phase 4: Add `maxRebroadcastAttempts: number` (default 0 = unlimited) to MonitorOptions. The proof-timeout path in TaskCheckForProofs increments `rebroadcastAttempts` each cycle; once the circuit-breaker limit is reached the req is marked invalid even for a broadcast tx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * testing plan Signed-off-by: Deggen <d.kellenschwiler@bsvassociation.org> * exclude dist Signed-off-by: Deggen <d.kellenschwiler@bsvassociation.org> * update pnpm Signed-off-by: Deggen <d.kellenschwiler@bsvassociation.org> * make sure they all pass Signed-off-by: Deggen <d.kellenschwiler@bsvassociation.org> * fix wallet proof timeout rebroadcast recovery * fix(docs-site): add path '/' to layout route so vite-react-ssg emits dist/index.html Without an explicit `path: '/'` on the root layout route, vite-react-ssg cannot discover the root pre-render target and never writes dist/index.html, causing pagefind and check-built-links to fail. Child paths converted to relative (no leading slash) and root index.md mapped to an index route per React Router v6 convention. Also adds `test -f dist/index.html` guard before pagefind to produce a clear error instead of a cryptic SSG crash. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix ci recursive workspace builds * chore: ignore .worktrees/ directory * test: allow slower mongo memory startup * ci: allow required native dependency builds * Update .github/workflows/conformance.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(wallet): harden input restoration guard * fix(ci): repair version and conformance checks * fix(wallet): rethrow unexpected purge beef errors * docs(wallet): summarize rebroadcast recovery PR --------- Signed-off-by: Deggen <d.kellenschwiler@bsvassociation.org> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 055fbe2 commit fc34338

31 files changed

Lines changed: 1603 additions & 324 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ jobs:
3232
run: node scripts/check-versions.mjs
3333

3434
- name: Build all packages
35-
# Exclude: example-paymail (example package, not production)
36-
run: pnpm -r --filter '!example-paymail' run build
35+
# Exclude: workspace root (delegating script) and example-paymail (example package, not production)
36+
run: pnpm -r --filter '!@bsv/ts-stack' --filter '!example-paymail' run build
3737

3838
- name: Lint all packages
3939
continue-on-error: true
40-
run: pnpm -r run lint --if-present
40+
run: pnpm -r --filter '!@bsv/ts-stack' run lint --if-present
4141

4242
- name: Test all packages
43-
# Exclude: example-paymail (example package, not production)
44-
run: pnpm -r --filter '!example-paymail' run test
43+
# Exclude: workspace root (delegating script) and example-paymail (example package, not production)
44+
run: pnpm -r --filter '!@bsv/ts-stack' --filter '!example-paymail' run test
4545

4646
conformance:
4747
name: Conformance Vectors

.github/workflows/conformance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
run: mkdir -p conformance/reports
2424
- name: Run TS conformance runner
2525
run: |
26-
pnpm --filter @bsv/conformance-runner-ts test -- \
26+
pnpm --filter @bsv/conformance-runner-ts test \
2727
--json --outputFile=../../reports/ts-results.json
2828
working-directory: ${{ github.workspace }}/conformance/runner/ts
2929
- name: Upload TS conformance reports

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: pnpm install --frozen-lockfile
2929

3030
- name: Build all packages
31-
run: pnpm -r run build
31+
run: pnpm -r --filter '!@bsv/ts-stack' run build
3232

3333
- name: Publish changed packages
3434
# No token needed — npm authenticates via GitHub OIDC.

docs-site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "node scripts/build-manifest.mjs && vite-react-ssg build && pagefind --site dist --output-path dist/_pagefind && node scripts/check-built-links.mjs",
8+
"build": "node scripts/build-manifest.mjs && vite-react-ssg build && test -f dist/index.html && pagefind --site dist --output-path dist/_pagefind && node scripts/check-built-links.mjs",
99
"preview": "vite preview",
1010
"validate": "node scripts/validate-frontmatter.mjs && node scripts/check-links.mjs"
1111
},

docs-site/src/routes.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@ function mdRoute(docPath: string): string {
2424
.replace(/\.md$/, '/')
2525
}
2626

27-
const docRoutes: RouteObject[] = Object.keys(pages).map(key => ({
28-
path: mdRoute(key),
29-
element: page(pages[key]),
30-
}))
27+
// vite-react-ssg requires parent route to have path '/' so it can discover
28+
// the root route for pre-rendering. Child paths must be relative (no leading /).
29+
const docRoutes: RouteObject[] = Object.keys(pages).map(key => {
30+
const absPath = mdRoute(key)
31+
if (absPath === '/') {
32+
// Root index.md → React Router index route so RootLayout renders at /
33+
return { index: true, element: page(pages[key]) } as RouteObject
34+
}
35+
// Strip leading / to make path relative to the '/' parent
36+
return { path: absPath.slice(1), element: page(pages[key]) }
37+
})
3138

3239
export const routes: RouteObject[] = [
3340
{
41+
path: '/',
3442
element: <RootLayout />,
3543
children: [
3644
...docRoutes,

package.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"private": true,
44
"description": "BSV TypeScript monorepo — SDK, Wallet, Network, Overlay, Messaging, Middleware, Helpers",
55
"scripts": {
6-
"build": "pnpm -r run build",
7-
"test": "pnpm -r run test",
8-
"lint": "pnpm -r run lint",
9-
"clean": "pnpm -r run clean",
6+
"build": "pnpm -r --filter '!@bsv/ts-stack' run build",
7+
"test": "pnpm -r --filter '!@bsv/ts-stack' run test",
8+
"lint": "pnpm -r --filter '!@bsv/ts-stack' run lint",
9+
"clean": "pnpm -r --filter '!@bsv/ts-stack' run clean",
1010
"sync-versions": "node scripts/sync-versions.mjs",
1111
"check-versions": "node scripts/check-versions.mjs",
1212
"conformance": "node conformance/runner/src/runner.js",
@@ -23,8 +23,13 @@
2323
"node": ">=22",
2424
"pnpm": ">=9"
2525
},
26-
"packageManager": "pnpm@9.0.0",
26+
"packageManager": "pnpm@10.33.2",
2727
"pnpm": {
28+
"onlyBuiltDependencies": [
29+
"better-sqlite3",
30+
"mongodb-memory-server",
31+
"sqlite3"
32+
],
2833
"overrides": {
2934
"jest-environment-node": "^30.3.0",
3035
"jest-mock": "^30.3.0",

packages/overlays/topics/src/__tests__/any.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { AnyLookupService } from '../any/AnyLookupService.js'
1414
import { AnyStorage } from '../any/AnyStorage.js'
1515
import { OutputAdmittedByTopic, LookupQuestion } from '@bsv/overlay'
1616

17+
const mongoMemoryServerOptions = { instance: { launchTimeout: 60000 } }
18+
1719
// ---------------------------------------------------------------------------
1820
// Helpers
1921
// ---------------------------------------------------------------------------
@@ -86,8 +88,8 @@ describe('AnyLookupService (MongoDB)', () => {
8688
let service: AnyLookupService
8789

8890
beforeAll(async () => {
89-
jest.setTimeout(30000)
90-
mongod = await MongoMemoryServer.create()
91+
jest.setTimeout(60000)
92+
mongod = await MongoMemoryServer.create(mongoMemoryServerOptions)
9193
client = new MongoClient(mongod.getUri())
9294
await client.connect()
9395
db = client.db('test_any')

packages/overlays/topics/src/__tests__/hello.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { HelloWorldLookupService } from '../hello/HelloWorldLookupService.js'
3131
import { HelloWorldStorage } from '../hello/HelloWorldStorage.js'
3232
import { OutputAdmittedByTopic, LookupQuestion } from '@bsv/overlay'
3333

34+
const mongoMemoryServerOptions = { instance: { launchTimeout: 60000 } }
35+
3436
// ---------------------------------------------------------------------------
3537
// Helpers
3638
// ---------------------------------------------------------------------------
@@ -185,8 +187,8 @@ describe('HelloWorldLookupService (MongoDB)', () => {
185187
let service: HelloWorldLookupService
186188

187189
beforeAll(async () => {
188-
jest.setTimeout(30000)
189-
mongod = await MongoMemoryServer.create()
190+
jest.setTimeout(60000)
191+
mongod = await MongoMemoryServer.create(mongoMemoryServerOptions)
190192
client = new MongoClient(mongod.getUri())
191193
await client.connect()
192194
db = client.db('test_hello')

packages/overlays/topics/src/__tests__/kvstore.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import KVStoreLookupServiceFactory from '../kvstore/KVStoreLookupService.js'
3030
import { KVStoreStorageManager } from '../kvstore/KVStoreStorageManager.js'
3131
import { OutputAdmittedByTopic, LookupQuestion } from '@bsv/overlay'
3232

33+
const mongoMemoryServerOptions = { instance: { launchTimeout: 60000 } }
34+
3335
// ---------------------------------------------------------------------------
3436
// Helpers
3537
// ---------------------------------------------------------------------------
@@ -263,8 +265,8 @@ describe('KVStoreLookupService (MongoDB)', () => {
263265
let service: ReturnType<typeof KVStoreLookupServiceFactory>
264266

265267
beforeAll(async () => {
266-
jest.setTimeout(30000)
267-
mongod = await MongoMemoryServer.create()
268+
jest.setTimeout(60000)
269+
mongod = await MongoMemoryServer.create(mongoMemoryServerOptions)
268270
client = new MongoClient(mongod.getUri())
269271
await client.connect()
270272
db = client.db('test_kvstore')

0 commit comments

Comments
 (0)