Skip to content
Cursor Agent edited this page Apr 9, 2026 · 32 revisions

open-captable-protocol-daml

DAML implementation of the Open Cap Format (OCF) standard on Canton Network.

Clone This Wiki

if [ -d wiki/.git ]; then git -C wiki pull; else git clone https://github.com/fairmint/open-captable-protocol-daml.wiki.git wiki; fi

Quick Commands

npm run build && npm run test    # Verify changes before commit (build runs NFT core invariant check)
npm run codegen                  # Generate JS bindings after DAML changes (merges packages into lib/)
npm run verify-package           # After codegen: import resolution + Node load of merged lib (same as CI)

# Deployment (after build/test pass)
npm run upload-dar -- --package ocp --network devnet
npm run upload-dar -- --package ocp --network mainnet

Build Tooling (dpm)

This repo uses dpm (Digital Asset Package Manager) for DAML builds. The daml CLI is deprecated.

Key points:

  • dpm build automatically handles multi-package dependencies via multi-package.yaml
  • Running dpm build from any package directory builds all dependencies first
  • dpm is installed to ~/.dpm/bin (add to PATH)

Direct dpm commands (if not using npm scripts):

dpm build              # Build current package + dependencies
dpm test               # Run tests
dpm codegen-js         # Generate JavaScript bindings
dpm clean              # Clean build artifacts

Installation:

curl https://get.digitalasset.com/install/install.sh | sh
export PATH="$HOME/.dpm/bin:$PATH"

Repo Structure

Directory Purpose
OpenCapTable-v34/ Core OCF contracts (current)
OpenCapTableReports-v01/ Reports
OpenCapTableProofOfOwnership-v01/ Proof of ownership
NftApi-v01/ Nft interface + shared NFT types (Nft.Api.V1)
NftReference-v01/ Reference impl: registry, assets, metadata (Nft.Reference.V1)
EquityCertificateShared/ Shared equity certificate types and validation
EquityCertificate-v01/ Certificate + registry DAML (EquityCertificate package)
Shared/ Shared helpers/types
CantonPayments/ Payment streams
CouponMinter/ Coupon minting contracts
Test/ DAML Script tests (depends on packages above)
scripts/ TypeScript deployment scripts
dars/ Backed-up DAR files for mainnet deployments

Implementation Status

All OCF object types are implemented. See ADR-001 for architecture and excluded/deprecated types.

Tasks: Tracked in Linear under the Eng team (including NFT and OCP contract work). See canton/AGENTS.md for Linear workflow.

Verifying Changes

Before submitting a PR, verify by running:

npm run build    # All packages must build
npm run test     # All tests must pass

After DAML or codegen pipeline changes, run npm run codegen then npm run verify-package so the merged lib/ tree matches what CI publishes.

ADR Status Values

Status Meaning
Proposed Design under discussion, not yet approved
Implemented Design approved AND supporting code/contracts are implemented

Skip "Accepted" as an intermediate state—go directly from "Proposed" to "Implemented" when the implementation is complete.

DAML Coding Standards

Write tight, concise, easy-to-read DAML code. Fail fast on invalid inputs. Code should be self-documenting—prefer comments that explain intent, schema links, or non-obvious constraints.

Style

  • No empty Text - Validate with validateOptionalText
  • Arrays always present - Use [] for empty, never omit
  • No trivial type aliases - Use validators instead
  • Shorthand .. - Use when all remaining record fields are in scope
  • Dynamic controllers - Use actor : Party as first choice param when controller varies

Naming

  • assert* - Includes error message, performs assertion (fail fast)
  • validate* - Returns Bool for composability; combine with assertMsg for context

Schema Alignment Rule (Critical)

  • OCF JSON schema is the source of truth for data validity.
  • Contract validators must not be stricter than schema unless an explicit ADR documents the stricter rule.
  • For any schema/contract mismatch incident:
    1. Fix contract validation to match schema intent.
    2. Add regression tests for the exact offending field/value.
    3. Perform a package minor upgrade for the affected DAML package.

File Structure

  1. One-line file summary (first line of the file, before module)
  2. module ... where
  3. Imports
  4. template block
  5. Main object data record (with OCF object block + schema URL on the type)
  6. Subtype/helper data definitions
  7. Validators immediately after types

Skip the one-line summary in test-only modules if it adds no signal.

Module summary and OCF link

Non-test modules start with a one-line file summary (before module), then module ... where, then the OCF object title and raw schema URL before imports, then the template and data definitions:

-- OCF convertible cancellation transaction template and data; referenced from the generated CapTable with other OCF records, and used by tests, NFT integrations, and ownership proofs.
module Fairmint.OpenCapTable.OCF.ConvertibleCancellation where

-- Object - Convertible Cancellation Transaction
-- Object describing a cancellation of a convertible security
-- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/cancellation/ConvertibleCancellation.schema.json

import Fairmint.OpenCapTable.Types.Core (Context)
import ...

template ConvertibleCancellation
  with
    context: Context
    cancellation_data: ConvertibleCancellationOcfData
  where
    signatory context.issuer, context.system_operator
    ensure validateConvertibleCancellationOcfData cancellation_data

data ConvertibleCancellationOcfData = ConvertibleCancellationOcfData
  with
    ...

Choice Ordering

  • Lifecycle order when clear, otherwise alphabetical
  • Create-style choices before archive-style

Field Ordering

data Example = Example with
    id: Text                    -- ID first
    required_field: Text        -- Required (alphabetical)
    items: [Text]               -- Arrays (alphabetical)
    optional_field: Optional Text  -- Optional (alphabetical)

Architecture

See canton/docs/developer/adr/001-ocf-captable-on-canton.md for detailed decisions.

Contract diagrams: See Contract-Diagram for visual Mermaid diagrams of the contract hierarchy and flow.

Diagram maintenance: Update the Contract-Diagram wiki page when:

  • Adding new OCF object types or transactions
  • Changing the contract hierarchy or relationships
  • Modifying validation patterns or signatories

Key Patterns

  • Issuer as factory - Issuer contract creates all other OCF objects
  • Dual signatories - All contracts require issuer + system operator
  • Archive + recreate - No direct mutation; archive old contract, create new one
  • Batch operations - Use UpdateCapTable choice for efficient bulk creates/edits/deletes

CapTable Code Generation

CapTable.daml is auto-generated. Never edit it directly. Modify the generator instead:

# Files involved
scripts/codegen/generate-captable.ts   # TypeScript generator
scripts/codegen/captable-config.yaml   # Config: validations, processing tiers

Running the Generator

CapTable.daml is regenerated as part of npm run build (via scripts/codegen/generate-captable.ts). There is no separate npm script—run a full build from the repo root after changing OCF types or generator config.

Config Structure

captable-config.yaml defines:

  • validations - Reference validation rules (e.g., stakeholder_id → stakeholders map)
  • tiers - Processing order for batch operations (dependency ordering)
validations:
  StockIssuance: [stakeholder_id, stock_class_id]
tiers:
  1: [Stakeholder, StockClass, ...] # Create first
  2: [Valuation, ...] # Depends on tier 1
  3: [StockIssuance, ...] # Depends on tier 1 & 2

DAML Sum Types

Sum types in DAML require each constructor to have a single argument:

-- Creates/Edits: Use OCF data type directly (ID is in the data record)
data OcfCreateData = OcfCreateFoo FooOcfData | OcfCreateBar BarOcfData
data OcfEditData = OcfEditFoo FooOcfData | OcfEditBar BarOcfData

-- Deletes: Tagged ID type (need to know which map to delete from)
data OcfObjectId = OcfFooId Text | OcfBarId Text

Choice Wrapping Limitation

Cannot exercise self OtherChoice inside a choice body—the contract is already consumed:

-- WRONG: Double consumption error
choice CreateFoo : ContractId CapTable
  do
    result <- exercise self UpdateCapTable with ...  -- ERROR!
    pure result.updatedCapTableCid

-- CORRECT: Implement logic directly
choice CreateFoo : ContractId CapTable
  do
    cid <- create Foo with ...
    create this with foos = Map.insert id cid foos

Adding New Types

Before adding a type to Types.daml, search the codebase for existing definitions.

# Check if type already exists
grep -r "data OcfYourType" OpenCapTable-v34/daml/

Package Upgrades & Releases

Major vs Minor Upgrades

  • Major upgrade (breaking change): Creates new package directory (e.g., OpenCapTable-v33OpenCapTable-v34)
  • Minor upgrade (non-breaking): Increments patch version (e.g., 0.0.10.0.2)

AI agents: Never perform a major version upgrade without explicit instructions from the user.

Upgrade Script

npm run upgrade-package -- --package OpenCapTable --type major

This will:

  1. Rename folder (e.g., OpenCapTable-v33/OpenCapTable-v34/)
  2. Update daml.yaml (name and version reset to 0.0.1)
  3. Search/replace all references across the repo

See Upgrade-Guide for full documentation.

Release Process

  1. Upgrade package: npm run upgrade-package -- --package <name> --type <major|minor>
  2. Make code changes: Edit DAML files as needed
  3. Build & test: npm run build && npm run test
  4. Document the release on the wiki Releases page (see naming convention there)
  5. Deploy:
    npm run upload-dar -- --package ocp --network devnet
    npm run upload-dar -- --package ocp --network mainnet
    npx tsx scripts/create-ocp-factory.ts --network devnet
    npx tsx scripts/create-ocp-factory.ts --network mainnet
    npm run verify-dars

DAR File Backup System

DAR files uploaded to mainnet are backed up in dars/ to preserve the exact bytes. DAML builds are only deterministic with the same compiler version.

# After successful mainnet upload, backup the DAR:
npm run backup-dar -- --package OpenCapTable-v34 --version 0.0.1 --network mainnet

# Verify all backed-up DARs (also runs in CI):
npm run verify-dars

See DAR-Backup for full documentation.

NPM Publishing

The package @fairmint/open-captable-protocol-daml-js is automatically published to NPM when changes are merged to main.

The published tarball includes one merged lib/ directory (OCP, reports, CantonPayments, NFT packages, and bundled Splice/DA paths codegen needs). The root export exposes Fairmint, Nft, CantonPayments, DA, and Splice, plus OCP_TEMPLATES: stable aliases for CapTable, IssuerAuthorization, and OcpFactory templateId values (same as the nested Fairmint.OpenCapTable.* modules). Those fields are written into merged lib/index by scripts/create-root-index.ts during codegen; #210 fixed a regression where OCP_TEMPLATES was missing from the root index. Subpath imports use package.json exports (e.g. @fairmint/open-captable-protocol-daml-js/lib/...).

OpenCapTable DAR (npm): The package also publishes a stable subpath @fairmint/open-captable-protocol-daml-js/opencaptable.dar, resolved to published-dars/OpenCapTable.dar in the tarball. That file is produced at the end of npm run codegen (which copies the built DAR from OpenCapTable-v34/.daml/dist/ after npm run build). It is gitignored locally; CI and npm publish run codegen so consumers always get the DAR matching the published JS.

  • Programmatic path: import getOpenCapTableDarPath(), resolveOpenCapTableDarPath(), OPEN_CAP_TABLE_DAR_PATH_ENV, and OPEN_CAP_TABLE_DAR_EXPORT_SUBPATH only from @fairmint/open-captable-protocol-daml-js/openCapTableDarPath (Node fs; not exposed on the package root so Next.js and other browser bundles stay valid). Returns an absolute path; throws if the staged DAR is missing—e.g. corrupt install or checkout without codegen. OPEN_CAP_TABLE_DAR_EXPORT_SUBPATH is './opencaptable.dar', matching package.json exports["./opencaptable.dar"] for bundlers and tooling.
  • Resolver-style: require.resolve('@fairmint/open-captable-protocol-daml-js/opencaptable.dar') or createRequire(import.meta.url).resolve(...) from ESM.

DAR path resolution (resolveOpenCapTableDarPath)

For tests and local tooling, use resolveOpenCapTableDarPath() from the same subpath as above. The merged root lib/index.js intentionally does not re-export these helpers. Source in-repo: scripts/npm-published-lib/openCapTableDarPath.ts.

Resolution order:

  1. OPEN_CAP_TABLE_DAR_PATH — if set, must point to an existing .dar (absolute or relative to process.cwd()); throws if set but the file is missing.
  2. Packaged DARgetOpenCapTableDarPath() (normal install).
  3. siblingDarPath — optional path (absolute, or relative to siblingSearchFrom); ignored unless it resolves to an existing file. Relative siblingDarPath without siblingSearchFrom throws.
  4. Default sibling layout — when siblingSearchFrom is the dependent repo root: {siblingSearchFrom}/../open-captable-protocol-daml/published-dars/OpenCapTable.dar (monorepo dev against a sibling checkout).

If the packaged DAR is missing and steps 3–4 do not yield a file, the thrown Error may set cause to the original packaged-DAR error (Error instances only), with a message that hints at env/sibling options.

CI: npm run verify-package runs verify-merged-lib, which checks that lib/openCapTableDarPath.js exists and root lib/index.js does not export DAR path helpers (same invariant as test:imports: keep fs off the browser-facing entry).

Equity certificate packages (EquityCertificateShared, EquityCertificate-v01) are built and tested in this monorepo but are not merged into that published lib/npm run codegen only runs JS codegen for OCP, Shared, Reports, NFT API/Reference (then CantonPayments) before bundling.

How It Works

  1. Create a PR with your changes
  2. Get it reviewed and approved
  3. Merge to main - CI automatically:
    • Builds DAML packages and generates TypeScript bindings
    • Runs npm run verify-package (import checks, verify-merged-lib + test:imports, including DAR path subpath + root-index separation)
    • Increments the patch version in package.json
    • Generates a changelog from commits since last release
    • Publishes to NPM
    • Creates and pushes a git tag (e.g., v0.2.52)

Version Bumping

  • Patch bumps (0.2.51 → 0.2.52): Automatic on every merge to main
  • Minor/Major bumps: Manually update package.json version before merging

Manual Release (Local)

npm run prepare-release   # Bumps version, generates changelog
npm publish               # Publishes to NPM (requires NPM_TOKEN)

Contract Party Configuration

Party assignments are documented in canton/docs/contract-party-configuration.md.

When adding or modifying contract creation scripts (e.g., create-*-factory.ts), update the party configuration doc to reflect:

  • Which party operates the contract
  • Network-specific party IDs
  • Script location and output files

Architecture Decision Records

# Title Status
ADR-001 OCF Cap Table on Canton Implemented
ADR-002-Stateful-Issuer Stateful Cap Table with OCF Object References Implemented
ADR-003-Featured-App-Markers Value-Based Coupon Minting for OCP Transactions Implemented
ADR-004-CouponMinter CouponMinter Contract Implemented
ADR-005-Canton-Payments CantonPayments - Payment Streams and Airdrops Implemented
ADR-006-Reports OpenCapTableReports Package Implemented
ADR-007-Proof-of-Ownership Proof of Ownership Contract Implemented

See ADRs for the full ADR index.

Related Repos

Repo Purpose Docs
canton Trading infrastructure, ADRs, party configuration AGENTS.md, docs/contract-party-configuration.md
canton-explorer Next.js explorer UI AGENTS.md, cantonops.fairmint.com
canton-fairmint-sdk Shared TypeScript utilities AGENTS.md
canton-node-sdk Low-level Canton client AGENTS.md, sdk.canton.fairmint.com
ocp-canton-sdk High-level OCP TypeScript SDK AGENTS.md, ocp.canton.fairmint.com
ocp-equity-certificate See also (SDK/tooling); equity certificate DAML lives in this repo — EquityCertificate-Module AGENTS.md

Living Document

Keep this wiki up-to-date. Update it when:

  • A best practice or pattern is established
  • An architectural or coding decision is made
  • New features, endpoints, or patterns are added
  • Before creating a PR: review for generalizable learnings

Clone this wiki locally