Skip to content
HardlyDifficult edited this page Jul 14, 2026 · 32 revisions

open-captable-protocol-daml

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

This repository contains the OpenCapTable DAML package and npm bindings. Release automation uses package-scoped tags such as OpenCapTable-v35-v0.0.1; see Package-Tag-Release-Process.

Current deployment

Layer Package / version Notes
Source OpenCapTable-v35 (0.0.2, SDK 3.5.1) Current repo; 0.0.2 enforces conversion mechanism validation at issuance (#233; see Release-2026-06-22-ocp-v35-conversion-mechanism-validation)
On-chain OpenCapTable-v34 (0.0.1) Still deployed on devnet + mainnet per dars/dars.lock until v35 upload
npm @fairmint/open-captable-protocol-daml-js 0.3.5 JS bindings from v35 source; separate from on-chain package id

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
npm run codegen                  # OCP JS bindings + merged lib/ (see NPM section)
npm run verify-package           # After codegen: same checks as CI (merged lib + import tests)

# 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 / item Purpose
OpenCapTable-v35/ Core OCF contracts (source in this repo)
Test/ DAML Script tests; data-dependencies → built OpenCapTable-v35 DAR
scripts/ TypeScript: codegen, deploy, verify, npm runtime
libs/splice/ Git submodule (Splice DARs for OCP data-dependencies)
dars/ OpenCapTable DAR backups and dars.lock (OCP only; see DAR-Backup)
generated/, lib/ Build/codegen output (gitignored where applicable)

Other Fairmint DAML (Shared, Reports, proof-of-ownership, NFT, CantonPayments, equity certificate, CouponMinter, …) is not in this tree; it lives in fairmint/daml. Consumed on-chain and in TypeScript via @fairmint/daml-js, not from this package’s root export.

Implementation Status

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

Contributors: Use GitHub Issues for bugs and feature requests.

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)

  • The official OCF schema and explicit OCF protocol rules are the only sources of truth for data validity.
  • Contract validators must not add Fairmint-specific, inferred, or ADR-authorized requirements beyond OCF.
  • See OCF-Validation-Policy for the evidence standard, cross-object reference rules, and review checklist.
  • 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 and integrations.
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

  • CapTable as authority — Single CapTable contract holds maps of OCF object IDs and handles all create/edit/delete via UpdateCapTable (see ADR-002-Stateful-Issuer); Issuer is a plain OCF object, not a factory
  • 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 by type (need to know which map to delete from)
data OcfDeleteData = OcfDeleteFoo Text | OcfDeleteBar 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-v35/daml/

Package Upgrades & Releases

Major vs Minor Upgrades

  • Major upgrade (breaking change): Creates new package directory (e.g., OpenCapTable-v34OpenCapTable-v35)
  • 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

For this repo, the versioned OCP package folder is OpenCapTable-vNN (e.g. OpenCapTable-v35). The script matches the base name of that folder (e.g. OpenCapTableOpenCapTable-v35).

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

This renames the folder, resets daml.yaml, and search/replaces references. Other Fairmint packages are upgraded in fairmint/daml, not here.

See Upgrade-Guide for full documentation.

Release Process

Use package-scoped release tags such as OpenCapTable-v35-v0.0.1. The full automated release workflow, required secrets, and manual checks are documented in Package-Tag-Release-Process.

Document human-facing release notes on the wiki Releases page using the naming convention there.

Other packages: Upload, vetting, and Splice lineages for CantonPayments and related DAML are documented and operated from fairmint/daml — not this repository.

CantonPayments + Splice: If mainnet upload fails with NOT_VALID_UPGRADE_PACKAGE across splice lineages, or you change splice-amulet in daml.yaml, follow the upload and lineage playbooks in fairmint/daml before iterating.

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-v35 --version 0.0.2 --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 published to the public npm registry via package-scoped release tags (e.g. OpenCapTable-v35-v0.0.1), not by merging to main alone. See Package-Tag-Release-Process and publishConfig.access in package.json.

Release workflows:

Workflow Trigger What it does
release.yml (canonical) Push package-scoped tag or manual workflow_dispatch Build, DAR backup, lint/test, upload DAR (devnet + mainnet), create missing factories, npm publish, commit artifacts to main
publish.yml (legacy) Every push to main Auto patch bump, prepare-release, npm publish, v$VERSION git tag — npm only; does not upload DARs or create factories

npm auth: release.yml uses the NPM_TOKEN secret; publish.yml uses npm trusted publishing (GitHub Actions OIDC, no NPM_TOKEN). New releases should use package-scoped tags and release.yml only; publish.yml remains for backward compatibility on main merges.

CI workflows:

Workflow Trigger What it does
ci.yml Every push Build, lint/format (with auto-fix), DAML lint, upgrade-compat check, codegen, verify-package, tests
check-dars.yml Changes under dars/ Verifies backed-up DAR integrity (npm run verify-dars)

The published tarball includes one merged lib/ tree built from OpenCapTable (v35) codegen plus bundled DA and Splice modules required at runtime. The root export exposes Fairmint (including Fairmint.OpenCapTable.*), DA, Splice, and OCP_TEMPLATES — stable aliases for CapTable, IssuerAuthorization, and OcpFactory templateId values (written by scripts/create-root-index.ts; #210 fixed a regression where OCP_TEMPLATES was missing from the root index). It does not export Nft, CantonPayments, or OpenCapTableReports on the root — use @fairmint/daml-js for those packages. Subpath exports include opencaptable.dar, openCapTableDarPath, and ocp-factory-contract-id.json (see root package.json exports).

Other packages: JS bindings for Reports, NFT, CantonPayments, and related Fairmint DAML are published from fairmint/daml (@fairmint/daml-js), not this repo.

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-v35/.daml/dist/ after npm run build). It is gitignored locally; CI and npm publish run codegen so consumers 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 and test:imports: required lib/ files, no Nft / CantonPayments on the root index, DAR helpers only on the openCapTableDarPath subpath, OCP_TEMPLATES present, and (verify-merged-lib + test:imports) keep fs off the browser-facing root entry.

Equity certificate packages (EquityCertificateShared, EquityCertificate-v01) live in fairmint/daml (#216) and are not part of the @fairmint/open-captable-protocol-daml-js root export — use @fairmint/daml-js.

How It Works

  1. Create a PR with your changes.
  2. Get it reviewed and approved.
  3. Merge to main after CI passes.
  4. Push a package-scoped release tag such as OpenCapTable-v35-v0.0.1 when the DAML package and npm package are ready to publish.
  5. Let the tag workflow publish after it builds, validates, uploads the DARs, creates missing factories, prepares npm artifacts, publishes npm, and records generated release artifacts back to main.

Version Bumping

Two version numbers are intentional and independent:

  • DAML package version (OpenCapTable-v35/daml.yaml, e.g. 0.0.1) — source of truth for on-chain package identity; the package-scoped release tag suffix must match this. scripts/packages.ts reads daml.yaml at build time.
  • npm package version (root package.json, e.g. 0.3.x) — tracks the published JS tarball; bump manually before tagging when a new npm publication is expected. Latest published on npm: 0.3.5 (root package.json may lag until the next release bump).

Manual Release (Local)

Prefer the tag workflow in Package-Tag-Release-Process. Use local npm publish only for an explicitly approved manual recovery path.

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

OcpFactory (scripts/create-ocp-factory.ts): submits via Intellect only so system_operator is the Intellect/Catalyst participant (not 5n). upload-dar still targets both Intellect and 5n so either participant can exercise vetted contracts. Factory state is recorded in generated/ocp-factory-contract-id.json (per-network contract IDs plus optional packageName, packageVersion, sourceDir, updatedAt written on factory creation; read by detect-factory-need.ts during tag releases). On-chain factories still reference v34 until v35 is uploaded and factories are redeployed.

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

ADRs for CouponMinter, CantonPayments, Reports, proof-of-ownership, and other packages now in fairmint/daml are not listed in this public wiki — see ADRs and that repo’s documentation.

See ADRs for the full ADR index.

License

The main repository and the npm package @fairmint/open-captable-protocol-daml-js are distributed under GNU General Public License v3.0 only (SPDX GPL-3.0-only). See LICENSE in the repo root.

Related Repos

Repo / package Purpose Docs / notes
fairmint/daml Additional Fairmint DAML packages (Shared, Reports, NFT, CantonPayments, equity certificate, …) fairmint/daml — companion after #216
@fairmint/daml-js (npm) Generated JS for fairmint/daml packages Use for Nft, CantonPayments, reports, etc. — not from @fairmint/open-captable-protocol-daml-js root
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 SDK/tooling around equity certificates Equity certificate DAML is in fairmint/daml; see that repo’s wiki for product docs

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