cantonjs
v0.4.0
Published
Application-side TypeScript SDK for Canton participant Ledger API V2
Downloads
584
Maintainers
Readme
cantonjs is the application-side TypeScript SDK for teams building directly against a Canton participant's Ledger API V2. It provides tree-shakeable Ledger, Admin, and Test clients; injected transports; real-time streaming; structured errors; and first-class testing support for participant-connected app code.
The repo is aimed first at backend or full-stack participant services, participant-private React apps, and integration or data teams with participant access. Public Scan consumers and advanced stable/public Splice integrators are secondary users. It starts after Daml lifecycle, full-stack onboarding, and wallet connection are already handled by the official upstream tools.
Start with docs/positioning.md, the target users guide, and the ecosystem-fit guide for the detailed scope and boundary story.
Existing users: the current positioning reset changes the repo mental model and package boundaries, and it removes the legacy wallet and validator-overlap surfaces from the current package set. See CHANGELOG.md and the migration notes.
Development policy: the included runtime surface is gated at 100% statements, branches, functions, and lines, and every coverage exclusion or inline v8 ignore must be justified in EXCLUSIONS.md.
Features
- Participant-side clients — Ledger, Admin, and Test client factories for direct application work
- Injected transports — JSON API V2, gRPC, and fallback transport layers without hidden I/O
- Real-time streaming — AsyncIterator WebSocket streams with auto-reconnect
- Structured errors and testing — CJ-coded errors, recovery hints, mock transports, and sandbox fixtures
- Optional codegen — generate TypeScript from existing Daml DAR artifacts when app code needs it
- Participant-private React hooks — TanStack Query-powered hooks via cantonjs-react
- Focused add-ons — public Scan, validator, token-standard, and interface packages around the participant-runtime core
Install
npm install cantonjsPackage Map
The repo centers on an app-side Ledger API V2 core plus focused add-ons around that boundary.
| Tier | Package | Stability | Purpose |
| ---- | ------- | --------- | ------- |
| Core | cantonjs | GA | Application-side Ledger, Admin, and Test clients; transports; streaming; errors; chains; and runtime typing helpers |
| Optional Convenience | cantonjs-react | GA | Participant-private React hooks for application code |
| Optional Convenience | cantonjs-codegen | GA | Optional DAR-to-TypeScript convenience from existing Daml artifacts |
| Add-On | cantonjs-splice-scan | GA | Public Scan reads for DSO metadata, update history, and public ANS lookups |
| Add-On | cantonjs-splice-validator | GA | Selected stable external validator support: ANS and filtered Scan Proxy reads |
| Add-On | cantonjs-splice-interfaces | GA | Stable published Splice interface descriptors and generated types |
| Add-On | cantonjs-splice-token-standard | GA | Participant-first CIP-0056 helpers for new token transfer and allocation flows |
For the canonical scope note that drives this package map, see docs/positioning.md. For a tool-by-tool "when to use what" guide, see docs/guide/ecosystem-fit.md. For the persona and anti-pitch version of that same story, see docs/guide/target-users.md.
Stability Tiers
- GA: Covered by the normal semver promise for the pinned release line.
- Experimental: May break in minor releases while the upstream surface is still moving.
Current compatibility target:
- Canton GA line:
3.4.x - Splice GA line:
0.5.x - Vendored Splice artifacts:
0.5.17 - Last pre-prune legacy line:
0.3.1
See compatibility policy and migration notes.
Quick Start
import { createLedgerClient, jsonApi } from 'cantonjs'
// 1. Create a transport
const transport = jsonApi({
url: 'http://localhost:7575',
token: 'your-jwt-token',
})
// 2. Create a party-scoped client
const client = createLedgerClient({
transport,
actAs: 'Alice::1234',
})
// 3. Create a contract
const created = await client.createContract('#my-pkg:Main:Asset', {
owner: 'Alice',
value: '100',
})
// 4. Exercise a choice
const tx = await client.exerciseChoice('#my-pkg:Main:Asset', created.contractId, 'Transfer', {
newOwner: 'Bob',
})
// 5. Query active contracts
const contracts = await client.queryContracts('#my-pkg:Main:Asset')Static bearer tokens remain supported. For request-scoped auth, provide an async token or session provider:
import { createLedgerClient, jsonApi, type AuthProvider } from 'cantonjs'
const auth: AuthProvider = async ({ request }) => {
if (request.path.startsWith('/v2/state/')) return undefined
return await getFreshJwtForAudience('participant')
}
const transport = jsonApi({
url: 'http://localhost:7575',
auth,
})
const client = createLedgerClient({
transport,
actAs: 'Alice::1234',
})Streaming
Subscribe to real-time updates with auto-reconnect and offset tracking:
import { streamUpdates } from 'cantonjs'
const controller = new AbortController()
for await (const update of streamUpdates(transport, {
beginExclusive: '0',
signal: controller.signal,
})) {
console.log('Update:', update.updateId)
}Subpath Imports
Import only what you need for smaller bundles:
import { createLedgerClient } from 'cantonjs/ledger'
import { createAdminClient } from 'cantonjs/admin'
import { createTestClient } from 'cantonjs/testing'
import { localNet, devNet, testNet, mainNet, withChainOverrides } from 'cantonjs/chains'
import type { TemplateDescriptor, InferPayload } from 'cantonjs/codegen'Network Presets
Built-in public presets are discovery-first. They carry public Scan discovery metadata, auth audience hints, and the pinned Splice release line, but they do not commit operator-specific participant or validator URLs.
Layer concrete deployment settings on top:
import { devNet, withChainOverrides } from 'cantonjs/chains'
const chain = withChainOverrides(devNet, {
participant: {
jsonApiUrl: process.env.CANTON_JSON_API_URL,
},
scan: {
url: process.env.SPLICE_SCAN_URL,
},
validator: {
apiBaseUrl: process.env.SPLICE_VALIDATOR_URL,
},
})chain.scan.discoveryRoot remains available when you need to resolve a live public Scan deployment from operator documentation first.
Clients
| Client | Purpose |
| ---------------------- | ------------------------------------------------------------------- |
| createLedgerClient() | Party-scoped contract operations (create, exercise, query, stream) |
| createAdminClient() | Node administration (parties, users, packages, IDP) |
| createTestClient() | Sandbox testing (time control, party allocation, sandbox lifecycle) |
Transports
| Transport | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------- |
| jsonApi({ url, token }) | HTTP transport for Canton JSON API V2 with static bearer auth |
| jsonApi({ url, auth }) | HTTP transport with async per-request token lookup |
| jsonApi({ url, session }) | HTTP transport with async per-request token and header injection |
| grpc({ grpcTransport, token }) | gRPC via injected ConnectRPC client with static bearer auth |
| grpc({ grpcTransport, auth }) | gRPC via injected ConnectRPC client with async per-request token lookup |
| grpc({ grpcTransport, session }) | gRPC via injected ConnectRPC client with async per-request token and header injection |
| fallback({ transports }) | Failover across multiple transports |
Error Handling
Every error is a CantonjsError with a machine-readable code, recovery hints, and a traversable cause chain:
import { CommandRejectedError, TokenExpiredError } from 'cantonjs'
try {
await client.createContract(templateId, args)
} catch (error) {
if (error instanceof TokenExpiredError) {
// error.code === 'CJ2001'
// error.metaMessages === ['Refresh your JWT token']
}
}| Range | Domain | | ------ | ------------------------------------------- | | CJ1xxx | Transport (connection, HTTP, gRPC, timeout) | | CJ2xxx | Authentication (JWT, token lifecycle) | | CJ3xxx | Ledger (command rejection, authorization) | | CJ4xxx | Admin (party, user, package management) | | CJ5xxx | Streaming (WebSocket, reconnection) | | CJ6xxx | Codegen (type mismatch, generation) |
Packages
cantonjs-codegen
cantonjs-codegen is an optional DAR-to-TypeScript convenience for application code built from existing Daml artifacts. DPM remains canonical for Daml build, test, and codegen workflows.
Generate TypeScript types from a DAR you already have:
npm install --save-dev cantonjs-codegen
npx cantonjs-codegen --dar ./model.dar --output ./src/generatedRecords become type aliases, variants become discriminated unions, templates become companion const objects with templateId and choices. See packages/cantonjs-codegen.
cantonjs-react
React hooks for participant-private ledger application state, powered by TanStack Query:
npm install cantonjs-react @tanstack/react-queryimport { createLedgerClient, jsonApi } from 'cantonjs'
import { CantonProvider, useContracts, useCreateContract } from 'cantonjs-react'
const client = createLedgerClient({
transport: jsonApi({ url: 'http://localhost:7575', token: 'your-jwt-token' }),
actAs: 'Alice::1234',
})
function App() {
return (
<CantonProvider client={client}>
<AssetList />
</CantonProvider>
)
}
function AssetList() {
const { data: assets, isLoading } = useContracts({
templateId: '#my-pkg:Main:Asset',
})
const { mutate: create } = useCreateContract({
templateId: '#my-pkg:Main:Asset',
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<button onClick={() => create({ createArguments: { owner: 'Alice', value: '100' } })}>
Create
</button>
<ul>
{assets?.map((c) => (
<li key={c.createdEvent.contractId}>{JSON.stringify(c.createdEvent.createArgument)}</li>
))}
</ul>
</div>
)
}cantonjs-react stays focused on participant-private ledger state.
For public Splice data, use TanStack Query directly with cantonjs-splice-scan; see docs/examples/react.md and docs/examples/public-scan-dashboard.md.
Splice Packages
cantonjs-splice-scan— GA public Scan reads for DSO metadata, updates, and public ANS lookups. Experimental Scan routes stay behindcantonjs-splice-scan/experimental. See docs/guide/scan.md.cantonjs-splice-validator— selected stable external validator support for ANS and the filtered GA Scan Proxy subset. See docs/guide/validator-ans.md.cantonjs-splice-token-standardandcantonjs-splice-interfaces— GA stable CIP-0056 descriptors and ledger-centric helpers for new transfer and allocation flows. See docs/guide/token-standard.md.
Testing
cantonjs provides first-class testing utilities. No vi.mock() needed — all dependencies are injected:
import { createLedgerClient } from 'cantonjs'
import { createMockTransport } from 'cantonjs/testing'
const transport = createMockTransport({
responses: [{ contractId: 'contract-1', templateId: '#pkg:Mod:T' }],
})
const client = createLedgerClient({ transport, actAs: 'Alice::1234' })
const created = await client.createContract('#pkg:Mod:T', { owner: 'Alice' })Integration testing with a real Canton sandbox:
import { setupCantonSandbox } from 'cantonjs/testing'
const sandbox = await setupCantonSandbox()
// sandbox.client is a fully configured TestClientCanton Concepts
| Concept | Description |
| ---------------- | ------------------------------------------------------------ |
| Party | Identity unit (not an address), permissions via JWT |
| Template | Daml contract definition (packageId:moduleName:entityName) |
| Choice | Operation on a contract (like a function call) |
| ContractId | Unique UTXO-style identifier for a contract instance |
| DAR | Daml Archive — the deployment artifact |
| Synchronizer | Consensus domain for transaction ordering |
Bundle Size
cantonjs is designed for minimal footprint:
| Entry Point | Size (minified + brotli) |
| ----------------- | ------------------------ |
| cantonjs | 5.78 kB |
| cantonjs/ledger | 1.1 KB |
Requirements
- Node.js >= 18
- TypeScript >= 5.0.4 (optional peer dependency)
Development
git clone https://github.com/merged-one/cantonjs.git
cd cantonjs
npm install
npm test # Run root tests
npm run test:coverage # Enforced root coverage gate
npm run test:coverage:all # Root + package coverage gates
npm run verify:ci:pr # Full PR validation suite
npm run typecheck # Type-check
npm run lint # Lint
npm run build # Build ESM + CJS + types
npm run size # Bundle size audit
npm run docs:dev # Start docs dev serverContributing
See CONTRIBUTING.md for development setup, coding conventions, and pull request guidelines.
Architecture Decisions
Design decisions are documented as Architecture Decision Records (ADRs):
| ADR | Topic | | ------------------------------------------------------ | -------------------------------- | | 0001 | TypeScript with function exports | | 0002 | Transport abstraction | | 0003 | Party-scoped client architecture | | 0004 | Structured error model | | 0005 | Streaming architecture | | 0006 | Testing strategy | | 0007 | Codegen architecture | | 0008 | React integration |
Related
- cantonctl — CLI companion for sandbox, admin, and test workflows
- Canton Network — The Canton Network
- Canton Docs — Official Canton documentation
