Skip to content

Commit 50312d9

Browse files
authored
Merge pull request #135 from bsv-blockchain/infra/chaintracks-server-onboarding
infra(chaintracks-server): onboard with docs + v2 HTTP conformance vectors
2 parents 24c04a8 + 177b32e commit 50312d9

36 files changed

Lines changed: 7888 additions & 41 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ These deployable projects are not included in `pnpm-workspace.yaml`; they keep t
133133

134134
| Path | Package | Role |
135135
|---|---|---|
136+
| [`infra/chaintracks-server`](infra/chaintracks-server) | `chaintracks-server` private | Block header service exposing v1 + v2 REST API over `ChaintracksService` |
136137
| [`infra/message-box-server`](infra/message-box-server) | `@bsv/messagebox-server` private | Message box server |
137138
| [`infra/overlay-server`](infra/overlay-server) | `@bsv/overlay-express-examples` private | Overlay server deployment |
138139
| [`infra/uhrp-server-basic`](infra/uhrp-server-basic) | [`@bsv/uhrp-lite`](https://www.npmjs.com/package/@bsv/uhrp-lite) | Basic UHRP storage host |

conformance/META.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,15 @@
9797
],
9898
"message-box": [
9999
"messaging.messagebox-http"
100+
],
101+
"chaintracks-v2": [
102+
"sync.chaintracks-v2-http"
100103
]
101104
},
102105
"stats": {
103-
"total_files": 72,
104-
"total_vectors": 6625,
105-
"last_updated": "2026-05-14"
106+
"total_files": 73,
107+
"total_vectors": 6646,
108+
"last_updated": "2026-05-19"
106109
},
107110
"regression_index": {
108111
"beef-v2-txid-panic": "go-sdk#306",

conformance/PARITY_MATRIX.json

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
"source": "ts-stack conformance corpus",
55
"description": "Machine-readable parity status for cross-language SDK implementations (Go, Rust, Python). Use this to track and drive conformance.",
66
"summary": {
7-
"total_files": 72,
8-
"total_vectors": 6625,
9-
"fully_required_files": 53,
7+
"total_files": 73,
8+
"total_vectors": 6646,
9+
"fully_required_files": 54,
1010
"files_with_intended": 17,
1111
"files_with_mixed_status": 15,
1212
"vectors_by_status": {
13-
"required": 6421,
13+
"required": 6442,
1414
"intended": 204,
1515
"skipped": 8
1616
},
1717
"by_reason_category": {
18-
"fully_supported": 1283,
18+
"fully_supported": 1304,
1919
"historical_regression": 36,
2020
"partial_ts_behavioral_difference": 5116,
2121
"wallet_stateful_harness_required": 190
@@ -530,6 +530,18 @@
530530
"justification": "Runtime-state assertion; requires producer with seeded user table. Cannot be validated by shape-only dispatcher.",
531531
"categories": []
532532
},
533+
{
534+
"path": "sync/chaintracks-v2-http.json",
535+
"id": "sync.chaintracks-v2-http",
536+
"total_vectors": 21,
537+
"file_level_parity": "required",
538+
"effective_status": "required",
539+
"required_count": 21,
540+
"intended_count": 0,
541+
"skipped_count": 0,
542+
"reason_category": "fully_supported",
543+
"categories": []
544+
},
533545
{
534546
"path": "sync/gasp-protocol.json",
535547
"id": "sync.gasprotocol",

conformance/runner/ts/dispatchers/sync.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import { expect } from '@jest/globals'
3838

3939
export const categories: ReadonlyArray<string> = [
4040
'gasp-protocol',
41-
'brc40-user-state'
41+
'brc40-user-state',
42+
'chaintracks-v2-http'
4243
]
4344

4445
// ── Constants ──────────────────────────────────────────────────────────────────
@@ -704,6 +705,88 @@ function dispatchBRC40 (
704705
throw new Error(`sync dispatcher: unknown channel '${channel}' in brc40-user-state`)
705706
}
706707

708+
// ── Chaintracks v2 HTTP vector dispatcher ──────────────────────────────────────
709+
//
710+
// Structural validation of the chaintracks-server v2 REST contract. Cross-language
711+
// implementations replace this with real HTTP calls against a running server;
712+
// here we only assert that each vector's expected payload obeys the v2 envelope
713+
// rules so the corpus stays well-formed.
714+
715+
const V2_PATH_RE = /^\/v2\/(network|tip(?:\.bin)?|header\/height\/.+|header\/hash\/.+|headers(?:\.bin)?)$/
716+
const V2_ERROR_CODES: ReadonlySet<string> = new Set([
717+
'ERR_INVALID_PARAMS',
718+
'ERR_NOT_FOUND',
719+
'ERR_NO_TIP',
720+
'ERR_INTERNAL'
721+
])
722+
723+
function assertSuccessEnvelope (eb: Record<string, unknown>): void {
724+
expect(eb['status']).toBe('success')
725+
expect('value' in eb).toBe(true)
726+
}
727+
728+
function assertErrorEnvelope (eb: Record<string, unknown>): void {
729+
expect(eb['status']).toBe('error')
730+
const code = eb['code']
731+
expect(typeof code).toBe('string')
732+
expect(V2_ERROR_CODES.has(code as string)).toBe(true)
733+
expect(typeof eb['description']).toBe('string')
734+
}
735+
736+
function assertBinaryShape (shape: Record<string, unknown>): void {
737+
expect(shape['encoding']).toBe('binary')
738+
const len = shape['length_bytes']
739+
// length_bytes may be a fixed integer or a formula string for variable-length payloads.
740+
if (typeof len === 'number') {
741+
expect(Number.isInteger(len) && len >= 0).toBe(true)
742+
const stride = shape['stride']
743+
if (typeof stride === 'number') {
744+
expect(len % stride).toBe(0)
745+
}
746+
} else {
747+
expect(typeof len).toBe('string')
748+
}
749+
}
750+
751+
function dispatchChaintracksV2 (
752+
input: Record<string, unknown>,
753+
expected: Record<string, unknown>
754+
): void {
755+
const method = getString(input, 'method')
756+
const path = getString(input, 'path')
757+
expect(method).toBe('GET')
758+
expect(V2_PATH_RE.test(path)).toBe(true)
759+
760+
const status = getNumber(expected, 'status')
761+
expect([200, 400, 404].includes(status)).toBe(true)
762+
763+
if (status >= 400) {
764+
const body = expected['body']
765+
expect(body !== undefined).toBe(true)
766+
assertErrorEnvelope(body as Record<string, unknown>)
767+
return
768+
}
769+
770+
// Success path — either a JSON envelope (body) or a binary shape (body_shape).
771+
const hasJson = expected['body'] !== undefined
772+
const hasShape = expected['body_shape'] !== undefined
773+
expect(hasJson || hasShape).toBe(true)
774+
775+
if (hasJson) {
776+
assertSuccessEnvelope(expected['body'] as Record<string, unknown>)
777+
}
778+
if (hasShape) {
779+
const shape = expected['body_shape'] as Record<string, unknown>
780+
if (shape['encoding'] === 'binary') {
781+
assertBinaryShape(shape)
782+
} else {
783+
// JSON success shape — must describe a {status, value} envelope.
784+
expect(shape['status']).toBe('success')
785+
expect('value' in shape).toBe(true)
786+
}
787+
}
788+
}
789+
707790
// ── Main dispatch entry point ──────────────────────────────────────────────────
708791

709792
export function dispatch (
@@ -715,6 +798,10 @@ export function dispatch (
715798
dispatchBRC40(input, expected)
716799
return
717800
}
801+
if (category === 'chaintracks-v2-http') {
802+
dispatchChaintracksV2(input, expected)
803+
return
804+
}
718805
if (category !== 'gasp-protocol') {
719806
throw new Error(`sync dispatcher: unknown category '${category}'`)
720807
}

0 commit comments

Comments
 (0)