diff --git a/proposals/denex-development-toolkit.md b/proposals/denex-development-toolkit.md new file mode 100644 index 00000000..15b64918 --- /dev/null +++ b/proposals/denex-development-toolkit.md @@ -0,0 +1,547 @@ +# 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](https://discuss.daml.com/t/canton-network-developer-experience-and-tooling-survey-analysis-2026/8412): +**Typed Client SDKs & Code Generators** and **Typed SDKs & Language Bindings**. + +The toolchain is organized as three independently usable pieces: + +1. **A strongly typed `Ledger` interface** defining a high-level, ergonomic API + to interact with Canton. +2. **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. +3. **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](https://docs.digitalasset.com/build/3.4/reference/json-api/lf-value-specification.html) + 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: + +```haskell +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: + +```typescript +// 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; + +/** Increase the count by `amount`. */ +export const Increment_ChoiceArg = z.object({ + /** Can be negative! */ + amount: Daml.Int, +}); +export type Increment_ChoiceArg = z.infer; +export const Increment_ChoiceResult = Daml.ContractIdOf("Counter.Counter"); +export type Increment_ChoiceResult = z.infer; +/** Decrease the count by `amount`. */ +export const Decrement_ChoiceArg = z.object({ + amount: Daml.Int, +}); +export type Decrement_ChoiceArg = z.infer; +export const Decrement_ChoiceResult = Daml.ContractIdOf("Counter.Counter"); +export type Decrement_ChoiceResult = z.infer; +/** Read the current count without modifying the contract. */ +export const Read_ChoiceArg = z.object({}); +export type Read_ChoiceArg = z.infer; +export const Read_ChoiceResult = Daml.Int; +export type Read_ChoiceResult = z.infer; +``` + +…and a package descriptor that ties them together for the SDK: + +```typescript +// 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](https://trpc.io/), 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: + +```typescript +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: + +```typescript +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: + +```typescript +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](https://github.com/canton-foundation/cips/blob/main/cip-0056/cip-0056.md), +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`](https://github.com/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` 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: + +- **adz:** https://github.com/denex-io/adz +- **Denex SDK:** https://github.com/denex-io/denex-sdk + +--- + +## 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._