@velocity-chain/schemas
v0.6.0
Published
Shared Zod 4 schemas for the velocity-node read API. Consumed by velocity-bridge-ui and velocity-explorer (and any future velocity-node consumer).
Downloads
2,428
Maintainers
Readme
@velocity-chain/schemas
Shared Zod 4 schemas for the velocity-node read API.
Consumed by velocity-bridge-ui and velocity-explorer (and any future velocity-node consumer). Single source of truth for response-shape validators so the two repos can't drift.
Why this exists
bridge-ui and explorer both consume the same velocity-node REST + SSE endpoints. Each was hand-rolling Zod schemas in their own repo. After both teams converged on Zod 4 (2026-04-26), extracting the shared schemas into one package was the obvious next step — agreed in siblings-comms/explorer/bridge-ui-zod-shipped-and-shared-package-offer.md and acked in siblings-comms/bridge-ui/explorer-yes-shared-schemas-and-yes-account-history.md.
Install
pnpm add @velocity-chain/schemas zod
# zod is a peer dep so consumers control the versionRequirements
- Node 22+ for any consumer running outside a bundler.
- Zod 4 (peer dep, range
^4.3). Bothvelocity-bridge-uiandvelocity-explorerare on Zod 4 since 2026-04-26; new consumers should align. - ESM-only. No CJS build is published. Vite, esbuild, Webpack 5, and modern Node all import this package directly. CJS-only environments are not supported — file an issue if that becomes a real constraint.
Use
import {
blockResponseSchema,
bridgeEventStreamPayloadSchema,
nonNegBigInt,
} from "@velocity-chain/schemas";
const block = blockResponseSchema.parse(await fetch("/block/42").then(r => r.json()));
// ^ typed as BlockResponse with bigint heights, etc.Inventory
28 schemas + 11 helpers, all landed and exported from the package root. Each schema pairs a <name>ResponseSchema const with an inferred <Name>Response type.
Schema-shape changes since v0.2.0:
- v0.6.0 — architectural transition: source of truth for
blockActionSchemamoved from hand-mirroredsrc/schemas/block-action.tsto velocity-codegen's Rust-driven emitter. The action-internal Zod schemas now live insrc/generated/_block-action.ts(do-not-edit; regenerated from canonical chain artifacts). The oldsrc/schemas/block-action.tsbecomes a ~50-line re-export shim. Public API stays identical — all existing exports preserved (blockActionSchema,chainActionSchema,BlockActiontype, sub-enums, polymorphic enums,committeeQuorumSigSchema); plus 8 additive exports (bridgeAttestationSchema,proofSignerSchema,changesetPathSchema, and const-array companionsBRIDGE_ORIGINS/REFUND_REASONS/CHANGESET_PATHS/NAMESPACE_CAPABILITIES). The single drop-from-shim distinction:committeeMemberSchemaaction-internal version is not re-exported (response-levelcommittee.tsversion owns the name; action-internal is reachable viablockActionSchemanarrowing). 12 silent wire-shape drift bugs caught and corrected across 3 review iterations between codegen and chain-schemas pre-vendor — Pattern 2 (hand-mirror as second-source) paid off. Per codegen handoff atsiblings-comms/codegen/handoff.mdand review thread atsiblings-comms/codegen/chain-schemas-phase-1b-*. Tests unchanged (225 passing); drift verification unchanged. Forward changes to action wire shapes land via codegen regen + re-vendor, not hand-edit. - v0.5.7 — additive registry-widen:
SIGNED_ENDPOINT_REGISTRYextended from 20 → 26 entries. Added 4 wallet-coverage/...-signedendpoints (/deposit-fuel-signed→DepositFuel,/namespace-admin-signed→NamespaceAdmin,/player-manage-signed→PlayerManage,/purchase-capability-signed→PurchaseCapability) per blockchain commit97af20d, and 2 dual-shape legacy paths (/account/register-signed→RegisterAccount,/account/add-key-signed→AddAccountKey) per commit5d33d5d— these now accept both wrapped-JSON envelope (canonical) and the legacy hex-blob envelope used by velocity-identity's existing flow. All 6 are single-sig (usewrappedSignedActionSchema); §18 multi-sig partition unchanged. Per blockchain heads-up atchain-schemas/v0-5-7-widen-heads-up-6-new-endpoints.md. Two new field-rename pairs (PurchaseCapability→namespace_admin_*;AddAccountKey→added_by/signature) follow the same envelope-canonicalization pattern as cross-ns grants — consumers don't need to surface the rename. - v0.5.6 — additive: wrapped-JSON signed-action envelope schemas for the 20
/...-signedendpoints blockchain shipped today. Six new exports:wrappedSignedActionSchema(single-sig, 18 endpoints),committeeQuorumSignedActionSchema+committeeQuorumHttpSigSchema(multi-sig §18 quorum, 2 endpoints),signedActionResponseSchema+committeeQuorumSignedActionResponseSchema(200 OK shapes),SIGNED_ENDPOINT_REGISTRYconst (path → variant-name lookup, 20 entries) +COMMITTEE_QUORUM_SIGNED_KINDSconst for runtime envelope-shape selection. HTTP-layer fields are hex strings (hashHex/signatureHex), distinct from action-internal[u8; N]arrays — Gotcha doc added. Excludes the 2 legacy binary-envelope account-registration endpoints. Re-opens v0.5.x stream for one final coordination cut; demo May 11 is the next time-bound milestone. - v0.5.5 — final-polish patch: retired dead
scripts/diff-vs-sdk.mjs(SDK collapsed to a re-export shim in v0.4.0; the diff-tool now compares chain-schemas to itself transitively). Addedpnpm verify:packpre-publish file-list audit (catches stray.tmp*/.env/ credential files in the tarball — wired inprepublishOnly). Added opt-inpnpm test:liveintegration suite that fetches/blocks/latest,/block/{height},/status,/healthfromVELOCITY_NODE_URLand parses through the corresponding schemas. Marks the package as feature-complete; further polish ROI is exhausted. - v0.5.4 — additive: 4 new Action variants for design §18 admin/quorum recovery paths (32 → 36).
ChainAdminForceUnpauseNamespaceandChainAdminForceRotateNamespaceOwner(single chain-admin signer + 48hnot_beforetimelock);CommitteeQuorumForceUnpauseNamespaceandCommitteeQuorumForceRotateNamespaceOwner(threshold-many committeebridge_pubkeysignatures). PluscommitteeQuorumSigSchemaexported as a reusable per-signer entry shape. ExistingForceUnpauseNamespace+ForceRotateNamespaceOwnerunchanged byte-for-byte (they ARE the NamespaceAdmin path). Per blockchain ship atchain-schemas/18-force-actions-shipped.md(counter-proposal-driven; we picked 4 variants over the originally-proposed shape change atchain-schemas/18-going-with-4-variants.md).verify:driftscript also tightened: package-ahead is now a warning (transient absorption window) while package-behind stays a hard fail (real drift). - v0.5.3 — additive:
"Swap"appended toACCOUNT_EVENT_KINDS(13 → 14) for Phase 3 fixed-shop trades, symmetric withAmmSwap/FlashSwap. Per explorer ask atchain-schemas/explorer-needs-swap-kind.md— closes the asymmetry where Phase 3 trades were visible on block-detail but invisible on per-account history. - v0.5.2 — additive (tightenings + polish):
DepositRefundRequest.message.source_chaintightened tobridgeOriginSchemaenum (wasnonEmptyString);reasontightened to typedrefundReasonSchemaenum ("UnroutableMemo" | "InactiveDestinationNamespace" | "AmountBelowMinimum" | "Other"). Both per blockchain's ack atchain-schemas/v32-drift-acked-source-chain-tighten-please.md. Polish: dedicated tests foraccount-keys+finalized-since(coverage gaps from v0.1 closed); JSDoc@exampleon all 28 schemas (IDE hover docs). - v0.5.1 — additive: 32nd Action variant
DepositRefundRequest(drift fix; SDK + chain-schemas were behind blockchain's main). Polish: drift-detection CI step (pnpm verify:drift), JSDoc with@exampleon all 11 helpers,sideEffects: false, source-map drop, v1.0 stability commitment doc,pnpm scaffold:variantcodegen helper. Zod aligned to^4.4.1to match constellation. - v0.5.0 — breaking:
block.actions[]is now typed viablockActionSchema(31-variant externally-tagged discriminated union), wasz.array(z.unknown()). Lifted from velocity-blockchain's CI-published JSON Schema artifact + golden-mirrored against velocity-js-sdk'sChainActionSchemav0.3.0. Adds 6 helpers (actionAmount,bytes32,bytes64,byteArray,tokenIdSchema,namespaceCapabilitySchema) describing action-internal shapes (distinct from response-levelnonNegBigInt/hashHex/signatureHex). Also exportschainActionSchemaas an SDK-mirror alias, and tightened sub-enums forNamespaceAdminOp,PlayerManageOp,FlashStep. Consumers narrow viaif ("Mint" in action) { ... }. - v0.4.2 — additive:
accountEventsRequestSchemafor client-side request validation. CSV-kinds parsed + per-element validated, direction enum widened tosent|received|both, limit bounded 1..200 (server default 50), permissive cursor (mirrors indexer's bad-cursor → restart posture). Also corrects the inline error-code documentation to match the indexer's actual codes (UNKNOWN_KIND,INVALID_HEIGHT,INVALID_LIMIT— were guessed wrong in v0.4.0). - v0.4.1 — additive:
wireAccountEventSchema+wireAccountEventsResponseSchemafor emitter-side use (indexer'srowToWire). MirrorsaccountEventSchemabut keepsheightandamountas digit-strings instead of transforming to bigint. Lets the indexer collapse its inlineWireEvent/WireResponseinterfaces to package types. - v0.4.0 — added
account-events(per-account event history from velocity-explorer's indexer service):accountEventKindEnum(13-string enum, also exported asACCOUNT_EVENT_KINDSconst value),accountEventSchema,accountEventsResponseSchema,accountEventsErrorSchema. Endpoint at{indexer_base}/account/:ns/:user/events. Source-of-truth is explorer's production schema, not chainhandlers.rs— the indexer aggregates per-account rows from the block stream. - v0.3.0 —
produced_at_unix(u64-stringified, required,0= unknown sentinel) added toblock,blocks-list(per summary), andblocks-latest.signing_key_origin(string, required) added tohealth.committee.
Account-scoped:
account-events—GET {indexer}/account/{ns}/{user}/events(per-account history; served by the indexer service, not chain proper)account-keys—GET /account/{ns}/{user}/keysaccount-namespaces—GET /accounts/{account}/namespacesbalance(single token) —GET /balance/{account}/{ns}/{token}balances(bulk) —GET /account/{ns}/{account}/balancesbridge-nonce—GET /accounts/{account}/bridge-nonce
Block-scoped:
block—GET /block/{height}(typedactions[]viablockActionSchemasince v0.5)block-action— typed 31-variant discriminated union forblock.actions[]. Use directly when consuming individual actions; auto-applied insideblock.actions.blocks-latest—GET /blocks/latestblocks-list—GET /blocks?from&to&limit
Bridge-scoped:
bridge-event-stream— SSE/blocks/streampayloadbridge-in-status—GET /bridge-in/{tx}/statusbridge-out-status—GET /bridge-out/{event_hash}/statusfinalized-since—GET /finalized-since/{height}proof—GET /proof/{event_hash}(see Gotchas)
Namespace-scoped:
namespaces—GET /namespacesnamespace-admins—GET /namespaces/{id}/adminsnamespace-bridge—GET /namespaces/{id}/bridgenamespace-capabilities—GET /namespaces/{id}/capabilitiescross-ns-grants—GET /namespaces/{id}/cross-ns-grants
Pools (Phase 3 / Phase 4):
swap-pools—GET /swap-pools?namespace=<id>andGET /swap-pools/{ns}/{...}(list + detail share the same shape)amm-pools—GET /amm-pools?namespace=<id>andGET /amm-pools/{ns}/{...}(constant-product, fee in basis points)
Capabilities (Phase 6):
capability-prices—GET /capability-prices
Chain vitals:
committee—GET /committeehealth—GET /healthmetrics—GET /metricsstatus—GET /statusvalidators-health—GET /validators/attestation-health
Helpers:
nonNegBigInt— accepts bigint / number / string-u64, validates ≥ 0hashHex— 64-char lowercase hex (strict-lowercase)signatureHex— 128-char lowercase hex (strict-lowercase)nonEmptyString—z.string().min(1)usize—z.number().int().nonnegative()for bounded counters
Gotchas
proofships two schemas, not one.proofReadyResponseSchemaparses the 200 body (Borsh proof bytes hex-encoded).proofBelowThresholdBodySchemaparses the 409 body (attestation progress for the fill bar). Consumers branch on HTTP status before parsing — don't try to parse a 409 with the ready schema or vice versa.- u64 fields are JSON strings on the wire (post
u64-stringifymigration). Always usenonNegBigInt, neverz.number().usizeis fine for bounded counters (committee size, batch counts) — those values fit in JS Number safely. - Hash regexes are strict-lowercase. Mixed-case hex on the wire indicates drift; strict matching has caught real bugs. Don't loosen
hashHex/signatureHexto[0-9a-fA-F]. block.actionsisz.array(z.unknown())by design. The 19 Action enum variants live in consumer-side renderers, branched on the top-level discriminator key. Adding a discriminated union here would force every consumer to import every payload shape they don't care about.finalized-sincereusesbridgeEventStreamPayloadSchema. Both the SSE frames and the REST list project the same RustBridgeEventJsonstruct.- Empty arrays vs 404. velocity-node's convention: unknown-but-valid queries return 200 with empty arrays, not 404. Don't add
nonemptyconstraints toaccount-namespaces,balances,blocks-list,account-keys,finalized-since, orvalidators-health. produced_at_unix = 0nmeans "unknown". Older blocks (pre-chain-commit5da4881) deserialize with0for the new producer-wall-clock field. Consumer rendering should fall back to ingest time (or hide the timestamp) on0n, not display 1970-01-01.health.committee.signing_key_originis intentionally a loosez.string().min(1). Current values are"loaded_from_path"and"generated_at_boot"; future values like"hsm_attached"are expected. The schema accepts any non-empty string so consumers don't break on chain-side additions. Strict gating (e.g. "refuse to start if origin isgenerated_at_boot") belongs at the consumer level — seevelocity-explorer/services/indexer/src/preflight.tsfor the canonical example.account-eventsis indexer-served, not chain-served. Endpoint base URL is{indexer_base}(typically configured via consumer env var likeVITE_INDEXER_URL), not the chain's:3000. Wire shape source of truth isvelocity-explorer/src/lib/indexer/schemas.ts, nothandlers.rs. The Pattern 2 chain-source cross-check used for other schemas doesn't apply.account-eventsfield-population matrix is documented but not enforced. The schema accepts anykind+directioncombo and any null/non-null pattern oncounterparty/token/amount/destination_*. Per-kind expectations (e.g.Mintalways hasdirection: "received",counterparty: null) live in source comments andsiblings-comms/chain-schemas/explorer-per-account-history-beta.md. The indexer enforces correlation at row creation; the schema validates wire shape only.accountEventsErrorSchema.error.codeis loosenonEmptyStringto match thesigning_key_originforward-compat policy. Known codes:INVALID_KIND,INVALID_DIRECTION,INVALID_FROM_HEIGHT,INVALID_TO_HEIGHT,HEIGHT_RANGE_INVERTED. New codes don't require a coordinated package bump.wireAccountEventSchema≠accountEventSchema— they describe different layers of the same contract. The wire schema (height,amountas digit-strings) is for emitter-side use (indexer'srowToWire); the post-parse schema (bigint vianonNegBigInt) is for consumer-side parsing. Don't merge them, and don't use the wire schema to validate consumer-received data — consumers should always useaccountEventSchemaso they get bigint at the boundary.accountEventsRequestSchemabakes in indexer policy choices (limit max 200 default 50; direction enum widened tosent|received|both; cursor permissive). If a future consumer needs different bounds (e.g. a CLI paginating with limit > 200), file an ask to widen the package shape — don't copy-paste a local schema. The whole point is one source of truth for the request surface.blockActionSchemais externally-tagged, not internally-tagged. Each action is{ "Mint": { ... } }not{ kind: "Mint", ... }. TypeScript narrowing usesif ("Mint" in action)— Zod 4'sdiscriminatedUniondoesn't support outer-key discriminators, so the union isz.unionover.strict()single-key objects. Per-variant signature field names vary (authority_key,admin_key,added_by,attester,owner_key— see source comments).- Action-internal u64s are plain JSON numbers, response-level u64s are stringified. Different layer of contract:
block.actions[].Mint.amountisactionAmount=z.number();block.heightisnonNegBigInt= bigint via union transform. Both come through the same JSON response, encoded differently per Rust serde rules. The package mirrors that asymmetry — don't merge. - Action-internal byte arrays are
[u8; N]JSON arrays, response-level hashes are hex strings. Same surface-layer split:mintInner.authority_keyisbytes32= 32-element number array;block.block_hashishashHex= 64-char lowercase hex. Don't try to unify; chain emits each at the correct layer. tokenIdSchema(action-internal) ≠tokenRefSchema(account-events row). Both have{ namespace, name }. tokenIdSchema is action-internal (always present, both fieldsnonEmptyString); tokenRefSchema is account-events-row (.nullable()for lifecycle events with no token).- HTTP envelope hex strings ≠ action-internal byte arrays.
wrappedSignedActionSchema.signed_action.authority_keyis a 64-char hex string (hashHex);block.actions[].Mint.authority_keyis a 32-element number array (bytes32). Same conceptual key, different layer of the contract — chain emits hex on the HTTP envelope,[u8; N]JSON arrays inside action-struct fields. Don't try to unify; pick the layer-appropriate schema. Same applies tosignaturefields (envelope:signatureHex; action-internal:bytes64). SIGNED_ENDPOINT_REGISTRYincludes 2 dual-shape legacy account paths since v0.5.7./account/register-signedand/account/add-key-signedaccept BOTH the wrapped-JSON envelope (canonical, this registry's contract) AND a legacy hex-blob envelope ({ signed_action_hex: "..." }) used by velocity-identity's existing flow. The wrapped form is canonical from the SDK / unity-sdk side; the legacy hex-blob shape stays for backwards-compat. Chain dispatches via untagged serde enum (same pattern as/bridge-out).chainActionSchemais an alias forblockActionSchema— same Zod schema, two export names. SDK consumers can re-export under the SDK's existingChainActionSchemasymbol; chain-schemas-internal callers can useBlockActionSchemato match the block context. Pick the name that fits your mental model; runtime cost is zero.
Versioning
v0.5+ stability posture. As of v0.5.0, the package covers the full chain → consumer wire surface (28 schemas, 11 helpers, 31 → 32 action variants). Three of the four sibling repos (velocity-bridge-ui, velocity-explorer, @velocity-chain/sdk) consume the package as their single source of truth for chain wire shapes; SDK collapsed its local ChainActionSchema to a re-export shim in v0.4.0. Drift detection runs in CI against velocity-blockchain's published JSON Schema artifact on every default-branch commit.
Versioning rules going forward:
- Patch (
0.5.x): additive new exports, doc fixes, internal refactors. Always safe to take. - Minor (
0.x.0): new schemas for new endpoints; tightening of existing loose-typed shapes; helper additions. Mostly additive, occasional contained breaking changes (clearly flagged in changelog). - Major (
1.0.0): stability commitment. After 1.0.0, every breaking change goes through a major. Re-exports and new schemas continue as minors.
Pin precision. Pin to a minor range "^0.5.0" for current-best behavior with patch-bump safety. Pin exact "0.5.1" if your repo prefers reproducible installs over auto-upgrade.
Path to v1.0.0. Two gates remaining:
- Demo (May 11) ships without surfacing schema-shape bugs in any consumer.
- One additional minor cycle absorbs whatever the demo surfaces, post-demo.
Tentative target: v1.0.0 mid-to-late May 2026, conditional on a quiet post-demo period. The 1.0 cut won't add new functionality — it's the stability commitment + a final API audit. Migration from 0.5.x to 1.0.0 is expected to be a no-op pnpm install.
After 1.0.0, breaking changes only arrive via majors with at least one minor of overlap (1.x continues to receive backports during the 2.x ramp-up).
Pre-publish coordination commitments (in effect since v0.4):
- chain-schemas → js-sdk before any chain-schemas major (now relaxed since SDK v0.4 absorbs via re-export shim, but courtesy heads-ups continue per cadence).
- chain-schemas → js-sdk before any
bridge-nonceschema change at any version. - blockchain → chain-schemas + js-sdk before any chain-side struct change to a schema-package-covered shape OR Action variant.
- Per-version cadence on heads-ups, not batched digests.
Convention has been 7-for-7 clean through v0.5.0; one drift slip caught by the artifact-verification step at v0.5.1.
Contributing
Schema changes that affect either consumer should land here first, then both consumers bump. PRs welcome from bridge-ui and explorer teams. New schemas (e.g. for endpoints velocity-node ships post-demo) come in via PR with golden fixtures matching velocity-node's response handlers.
License
Copyright (c) 2026 Surgent International FZ-LLC. All rights reserved.
This software is proprietary. The source code is made publicly visible for transparency and review purposes only. Public visibility does not constitute a grant of any rights.
