Canton Development Fund Proposal
Denex Developer SDK: Type-Safe SDK and Code Generation for the Canton Network
1. Executive Summary
We propose the continued development and open-source release of the Denex
Developer SDK, a TypeScript toolchain for building Canton applications. It
addresses the two highest-priority TypeScript gaps identified in the
2026 Developer Experience and Tooling Survey:
Typed Client SDKs & Code Generators and Typed SDKs & Language Bindings.
The toolchain is organized as three independently usable pieces:
- A strongly typed
Ledger interface defining a high-level, ergonomic API
to interact with Canton.
- Pluggable transport implementations of that interface. A direct
server-side implementation against the JSON Ledger API v2, the Scan Proxy
API, and Token Standard registries; a tRPC-based client transport for browser
and automation use that exposes the same shape over a thin RPC. Additional
transports can be plugged in without changing application code.
- Typed contract interaction tooling driven by Zod schemas: a command
builder, fluent contract wrappers, paginated-query pipelines, and a
disclosure-aware exercise model. The schemas can be hand-written for
third-party packages or generated by the code generator adz from Daml
source.
These pieces can be used independently — a developer who already has a preferred
transport can use just the schemas and command builder; a developer who already
has hand-written types can use just the typed Ledger and its transports — and
integrate cleanly when used together.
Funding Requested: 17,000,000 CC Timeline: 9 months Team: 6
developers
2. Problem Statement
The 2026 Developer Experience and Tooling Survey identifies typed SDKs and
codegen among the high-impact tooling needs across the Canton developer
community. The report's "magic wand" wishlist names Typed SDKs & Language
Bindings; the "additional developer tooling opportunities" section names
Typed Client SDK & Code Generator as a distinct category. The survey also
notes that 71% of respondents come from an Ethereum (EVM) background, with
explicit name-drops of Web3.js, MetaMask, Hardhat, Foundry, and Anchor as the
ergonomic baselines those developers expect.
For TypeScript developers building on current Canton, significant pieces of the
stack between "OpenAPI-generated HTTP client" and "application code" are
typically assembled per project:
- The JSON Ledger API's OpenAPI specification types Daml contract payloads,
choice arguments, and choice results as
unknown — the spec cannot describe
them, because they are Daml-defined, not Canton-defined. Decoding, validation,
and command assembly happen at every call site.
- Multi-call workflows — token-standard factory resolution, ACS pagination
across an offset-stable snapshot, choice-context lookups, disclosure handling
— are not covered by transport-layer codegen and are typically re-implemented
per project.
- Fluent contract-interaction ergonomics familiar from EVM ecosystems
(
counter.increment({ amount: 2n }), .archive(), paginated .query()
pipelines) are not provided by transport-layer codegen and are not defined as
a pattern any single library currently delivers across both server and browser
environments.
The Denex SDK aims to fill these gaps by consolidating typing, command building,
and contract-interaction ergonomics into a single layer above existing
transports.
3. Proposed Solution
3.1 adz: Daml-to-TypeScript Code Generator
Category: Typed Client SDK & Code Generator
adz reads Daml source and emits, for each Daml package:
- Zod schemas for all data types, templates, interfaces, and choices, with
inferred TypeScript types ready to use directly in application code.
- A package descriptor that ties those schemas to the package's module,
template, and choice hierarchy and feeds directly into the SDK's typed package
context (see §3.2).
Key features:
- Comprehensive Daml construct support: records, variants, enums, templates
with consuming and non-consuming choices, interfaces with views, cross-module
imports.
- Application-shaped TypeScript types. Daml scalars decode into the
TypeScript types developers actually use:
Int → bigint,
Numeric → Decimal (with no precision loss), Time → Date, Party and
ContractId as branded strings with format checks.
- Comment carry-over. Daml documentation comments on records, fields,
templates, and choices are propagated to the generated TypeScript as TSDoc
comments, so editor tooltips, IntelliSense, and generated docs see the same
prose the Daml author wrote.
- Cross-package dependency resolution for projects that depend on
third-party Daml packages.
- Faithful Daml-LF JSON encoding. The generated schemas target the
documented
Daml-LF JSON encoding
on both sides of the transform. An
Int arrives as a string on the wire and
is decoded into a bigint for application code; a Numeric arrives as a
string and is decoded into a Decimal without precision loss; on submission,
both are re-encoded to the strings the ledger expects. Variant round-trips
as { tag, value }, Optional as null or value, records as JSON objects —
all per the LF spec.
- Graceful degradation. Types that cannot be represented exactly are emitted
with permissive schemas and explanatory comments rather than causing
generation to fail silently.
What the generated code looks like
For a small Daml template:
module Counter where
-- | A simple owned counter.
template Counter
with
owner : Party -- ^ Party owning the counter.
count: Int -- ^ Current value.
tag: Text -- ^ Free-form label for grouping.
where
signatory owner
-- | Increase the count by `amount`.
choice Increment : ContractId Counter
with
amount : Int -- ^ Can be negative!
controller owner
do create this with count = this.count + amount
-- | Decrease the count by `amount`.
choice Decrement : ContractId Counter
with
amount : Int
controller owner
do create this with count = this.count - amount
-- | Read the current count without modifying the contract.
nonconsuming choice Read : Int
controller owner
do return this.count
adz emits Zod schemas with the Daml comments propagated as TSDoc:
// AUTO-GENERATED from DAML source. Do not edit by hand.
import { z } from "zod";
import * as Daml from "@denex/sdk-api/daml";
/** A simple owned counter. */
export const Counter = z.object({
/** Party owning the counter. */
owner: Daml.Party,
/** Current value. */
count: Daml.Int,
/** Free-form label for grouping. */
tag: Daml.Text,
});
export type Counter = z.infer<typeof Counter>;
/** Increase the count by `amount`. */
export const Increment_ChoiceArg = z.object({
/** Can be negative! */
amount: Daml.Int,
});
export type Increment_ChoiceArg = z.infer<typeof Increment_ChoiceArg>;
export const Increment_ChoiceResult = Daml.ContractIdOf("Counter.Counter");
export type Increment_ChoiceResult = z.infer<typeof Increment_ChoiceResult>;
/** Decrease the count by `amount`. */
export const Decrement_ChoiceArg = z.object({
amount: Daml.Int,
});
export type Decrement_ChoiceArg = z.infer<typeof Decrement_ChoiceArg>;
export const Decrement_ChoiceResult = Daml.ContractIdOf("Counter.Counter");
export type Decrement_ChoiceResult = z.infer<typeof Decrement_ChoiceResult>;
/** Read the current count without modifying the contract. */
export const Read_ChoiceArg = z.object({});
export type Read_ChoiceArg = z.infer<typeof Read_ChoiceArg>;
export const Read_ChoiceResult = Daml.Int;
export type Read_ChoiceResult = z.infer<typeof Read_ChoiceResult>;
…and a package descriptor that ties them together for the SDK:
// AUTO-GENERATED from DAML source. Do not edit by hand.
import * as CounterZod from "./Counter.zod.ts";
export default {
packageId: "#counter" as const,
modules: {
Counter: {
templates: {
Counter: {
payload: CounterZod.Counter,
choices: {
Increment: {
argument: CounterZod.Increment_ChoiceArg,
result: CounterZod.Increment_ChoiceResult,
consuming: true,
},
Decrement: {
argument: CounterZod.Decrement_ChoiceArg,
result: CounterZod.Decrement_ChoiceResult,
consuming: true,
},
Read: {
argument: CounterZod.Read_ChoiceArg,
result: CounterZod.Read_ChoiceResult,
consuming: false,
},
},
},
},
},
},
} as const;
The Zod schemas are independently usable — a developer who only wants
runtime-validated payload types can import them directly into application code.
The package descriptor is what unlocks the higher-level command builder and
contract wrappers in the SDK.
3.2 Denex SDK: Layered, Typed Canton Ledger Access
Category: Typed SDKs & Language Bindings
The Denex SDK is a TypeScript monorepo organized as three layers, each
independently usable.
3.2.1 The typed Ledger interface
A higher-level facade over a curated subset of Canton's REST APIs, exposing the
operations a typical Canton application needs: identity and ledger state,
command submission, contract and interface queries with paginated results, and
the multi-call workflows for the Canton token standard (factory resolution,
choice contexts, disclosed contracts, instrument metadata). All inputs and
outputs are typed end-to-end against Zod schemas, so payloads, choice arguments,
and choice results are validated at the boundary rather than left as unknown.
A parallel client-side variant of the interface is intended for less trusted
environments such as a web browser. Its surface is the same shape as the
server-side interface, minus the operations that are inappropriate in those
environments (e.g. disclosure access). The distinction is enforced at the
TypeScript type level so that client code cannot accidentally call into
server-only functionality.
3.2.2 Pluggable transport implementations
Two transports are delivered under this grant:
- Direct server-side: implements
Ledger against the JSON Ledger API v2,
the Scan Proxy API, and the Token Standard registry, transfer-instruction, and
allocation-instruction APIs. Uses OpenAPI-generated clients for the HTTP
layer; we do not reimplement transport.
- tRPC-based client transport: implements
ClientLedger over
tRPC, with a thin proxy router that wraps a server-side
Ledger and exposes it to browser or Node clients. Application code on either
side of the wire imports the same schemas, command types, and contract
wrappers; the wire format is defined by the same Zod schemas the rest of the
SDK uses.
The interface is transport-agnostic by design. Additional transports can be
added without changes to application code, schemas, command builder, or contract
wrappers.
3.2.3 Typed contract interaction
Given a package descriptor (hand-written or adz-generated) and a Ledger
instance, the SDK builds a typed package context with fluent template and
contract wrappers:
import { createPackageContext } from "@denex/sdk-api";
import { paginate } from "@denex/sdk-api/pagination";
import CounterPackage from "./generated/counter"; // adz-generated
// One-time setup: bind a ledger, package descriptor, and acting party.
const pkg = createPackageContext(ledger, CounterPackage, userParty);
// Template wrapper: typed factory for Counter contracts.
const Counter = pkg.Template("Counter.Counter");
// Create a contract — payload type-checked and runtime-validated.
let counter = await Counter.create({
owner: pkg.actAs,
count: 3n, // bigint, not string
tag: "demo",
});
// Exercise a consuming choice — argument type-checked, result wrapped
// automatically into a new Counter contract wrapper.
counter = await counter.increment({ amount: 2n });
// Find active Counter contracts visible to userParty, paginated and filtered.
const matching = await paginate(Counter.query)
.filter((c) => c.payload.tag === "demo")
.collect();
// Archive
await counter.archive();
For multi-command transactions, every method has a corresponding .asCommand()
factory that produces a Command carrying its own argument and result schemas:
const [created, incremented] = await ledger.submit(userParty, [
Counter.create.asCommand({ owner: userParty, count: 0n, tag: "x" }),
Counter(existingCid).increment.asCommand({ amount: 1n }),
]);
// Each tuple element is typed precisely to its command's result.
The submit return type is computed from the input commands: each tuple
position carries the result type of the corresponding command, with no manual
casts.
3.2.4 Pagination over query results
The JSON Ledger API exposes contract queries as a single request returning a
single result set, capped by a configured per-call result-size limit on the
participant; exceeding the limit produces a 413 Content Too Large rather than
continuing the read. The OpenAPI spec describes no pagination construct on top
of that.
The Denex SDK presents query results as a paginated, fluent pipeline that scales
beyond the per-call limit:
import { paginate } from "@denex/sdk-api/pagination";
const matching = await paginate(Counter.query)
.filter((c) => c.payload.tag === "demo")
.collect();
Pages are consumed lazily, and a paginate(...) pipeline can be filtered,
mapped, reduced, or used directly with for await without exposing offsets,
page tokens, or snapshot mechanics to application code.
3.2.5 Token standard and Splice support
The SDK builds on the Daml interfaces and registry REST APIs defined in
CIP-0056,
covering the workflows a token-standard dApp actually has to perform: resolving
transfer and allocation factories with their choice contexts and disclosures,
exercising instruction-level choices on transfer and allocation instructions,
and listing instrument metadata. Splice-specific surfaces — Canton Coin's
AmuletRules, OpenMiningRound, and featured- app rights — are also wired in
for dApps building on Splice directly.
- A high-level API for factory and disclosure resolution. Exercising a
transfer or allocation choice under CIP-0056 requires resolving the factory
contract, fetching its choice context, and collecting the disclosed contracts
the factory needs before assembling the
submit call. The SDK provides typed
helpers that perform that resolution and return a bundle in the shape submit
consumes; the dApp passes the bundle into submit directly, without
constructing registry request bodies, unpacking response envelopes, or
threading choice-context payloads and disclosed-contract arrays by hand.
- Splice and token-standard data types are described as Zod schemas. Factory
arguments, choice contexts, instrument metadata, transfer and allocation
instruction shapes, and Splice-specific contracts like
AmuletRules and
OpenMiningRound — registry and scan-proxy responses arrive as fully typed,
decoded TypeScript values rather than the unknown payloads the CIP-0056
OpenAPI specs necessarily leave for the client to handle.
4. Relationship to Existing Tooling
The Denex SDK builds on top of OpenAPI-generated clients, providing a
higher-level, ergonomic API that elevates build-time and runtime type safety
with Zod schemas and orchestrates multi-step workflows.
dpm codegen-js produces TypeScript type definitions paired with decoders and
encoders built on
@mojotech/json-type-validation,
and a per-template runtime registry. Wrapper libraries that mediate between the
JSON Ledger API and application code consume this runtime surface to validate
contract payloads, serialize command arguments, and round-trip choice results.
adz produces a different output set — Zod schemas, per-package descriptor
modules, and TSDoc preserved from Daml docstrings — that the Denex SDK consumes
for the same purposes, with application-shaped scalar values (bigint for
Int, Decimal for Numeric, Date for Time, branded ContractId<T> and
Party) decoded at the boundary rather than threaded through call sites as
branded strings. The Zod schemas are independently usable: a project that
prefers to work directly against an OpenAPI-generated client can plug them into
its own request and response handling to recover the same boundary validation
and application-shaped values, without taking on the rest of the SDK.
5. Development Roadmap
Phase 1: Open-Source Release
| Milestone; Deadline | Deliverable | Category | Requested Funding (CC) | Description |
|---|
| 1.1; Approval + 3 months | adz initial release | adz | 1,000,000 | Daml source code parser and TypeScript code generator producing Zod schemas and typed package descriptors from Daml projects |
| 1.2; Approval + 3 months | SDK initial release | Denex SDK | 500,000 | Collection of Typescript packages providing the typed Ledger interface, server-side direct transport (JSON Ledger API v2, Scan Proxy, Token Standard), tRPC client transport, command builder, contract wrappers, and pagination. Publish @denex/* packages to public npm registry. |
| 1.3; Approval + 3 months | Token standard fluent API | Denex SDK | 500,000 | Higher-level fluent wrappers for CIP-0056 token standard operations (transfer, allocation, settlement) built on top of the existing schema and query layer |
Phase 1 Funding Requested: 2,000,000 CC
Phase 2: Ecosystem Integration
| Milestone; Deadline | Deliverable | Category | Requested Funding (CC) | Description |
|---|
| 2.1; Approval + 6 months | adz distribution | adz | 1,000,000 | Self-contained single file distribution |
| 2.2; Approval + 6 months | Documentation | All | 1,000,000 | Unified documentation site; additional examples covering common patterns (DeFi workflows, tokenization, multi-party coordination) |
Phase 2 Funding Requested: 2,000,000 CC
Phase 3: Hardening
| Milestone; Deadline | Deliverable | Category | Requested Funding (CC) | Description |
|---|
| 3.1; Approval + 9 months | adz hardening releases | adz | 1,000,000 | Broaden test coverage, improve diagnostic output for unresolved types |
| 3.2; Approval + 9 months | SDK hardening releases | Denex SDK | 1,000,000 | Harden SDK APIs based on real-world usage; improve error handling and edge case coverage across ledger query, command submission, and pagination paths; expand integration test suite |
Phase 3 Funding Requested: 2,000,000 CC
6. Funding & Resources
Total Canton Coins requested: 17,000,000 CC
| Phase / Commitment; Deadline | Amount (CC) | Trigger |
|---|
| Phase 1; Approval + 3 months | 2,000,000 | Committee acceptance of deliverables |
| Phase 2; Approval + 6 months | 2,000,000 | Committee acceptance of deliverables |
| Phase 3; Approval + 9 months | 2,000,000 | Committee acceptance of deliverables |
| Acceleration of Phases 1-3; Approval + 6 months | 1,000,000 | Committee acceptance of deliverables |
| Adoption of Developer SDK and adz | 10,000,000 | 1,000,000 CC per instance of repo used by a production-scale or representative Canton deployment; maximum of 10,000,000 CC or 10 instances |
Adoption Requirements
This milestone focuses on validating the tools with real-world Canton app
development and ensuring its usability across diverse applications. Denex will:
- Collaborate with the Canton Foundation and ecosystem to onboard at least 10
member companies or representative Canton deployments
- Support these teams in running the tool on their codebases
- Collect feedback and refine the tool’s output and usability
- Attestation of participation or usage of the tool
- Evidence of successful runs across participating projects
Team Composition (6 developers)
| Role | Count | Focus |
|---|
| Senior TypeScript/SDK Engineers | 3 | Denex SDK core |
| Compiler/Tooling Engineers | 2 | adz code generator, parser/grammar tooling |
| Technical Writer / DX Engineer | 1 | Documentation, tutorials, sample applications, developer onboarding |
Resource requirements
Engineering team (6 developers) Infrastructure & tooling (CI/CD, hosting,
testing) Documentation, design & community feedback Contingency & open-source
maintenance buffer
Volatility Stipulation
This grant is denominated in fixed Canton Coin. Should the project timeline
extend beyond 6 months, the remaining un-minted milestones will be renegotiated
to account for any significant USD/CC price volatility.
7. Timeline & Scope Risk Management
Denex explicitly assumes the financial risk of executing engineering phases
(Phases 1-3) parallel to incorporating community feedback. Should the community
discussion change the scope presented in the above Phases, Denex absorbs the
wasted work of working against an in-flux specification without requesting
supplemental Foundation funds. Only if major scope is added to the proposal as
part of the community discussion will the delivery milestone rewards be
revisited.
8. Team Qualifications
The Denex team has been building on the Canton Network since its early days,
with deep experience across the Daml/Canton stack:
- Active builders: We are the developers behind the Denex platform, the
creator of the Denex Gas Station and provider of Canton application hosting
infrastructure. Our tools are born from solving real problems in production.
- Ecosystem contributors: We have first-hand experience with the developer
pain points identified in the survey; our tools were built because we
encountered these gaps ourselves.
- TypeScript and tooling expertise: Our team includes engineers with
backgrounds in compiler tooling (parser/grammar tooling, code generation),
TypeScript SDK design (OpenAPI, Zod, tRPC), and infrastructure automation
(Docker, Kubernetes).
- Existing, working code: Unlike a speculative proposal, we are presenting
functional, tested tools with integration test suites running against
Canton sandboxes. This grant funds maturation and open-source release, not
greenfield R&D.
9. Open-Source Commitment
All tools developed under this grant will be released under the Apache 2.0
license:
10. Alignment with Canton Network Priorities
This proposal directly addresses the Canton Network's stated priorities from the
2026 Developer Experience and Tooling Survey:
| Survey Priority | Denex SDK Component | Alignment |
|---|
| Typed SDKs & Language Bindings | Denex SDK | Typed Ledger interface with pluggable transports, fluent contract wrappers, Zod-driven runtime validation |
| Typed Client SDK & Code Generator | adz | Daml → Zod schemas + package descriptors, eliminating manual type extraction |
| Conceptual Paradigm Shift (EVM → Canton learning curve) | Denex SDK | Familiar ethers.js-like ergonomics (.create(), .increment(), .archive()) |
| Integration Friction (Package ID discovery, frontend integration) | adz + Denex SDK | Comprehensive Daml-to-TypeScript mapping, including package ID embedding; tRPC client transport for browser dApps |
Submitted by the Denex Team for consideration by the Canton Development Fund
Grants Committee.