-
Notifications
You must be signed in to change notification settings - Fork 0
Home
DAML implementation of the Open Cap Format (OCF) standard on Canton Network.
if [ -d wiki/.git ]; then git -C wiki pull; else git clone https://github.com/Fairmint/open-captable-protocol-daml.wiki.git wiki; finpm 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 mainnetThis repo uses dpm (Digital Asset Package Manager) for DAML builds. The daml CLI is deprecated.
Key points:
-
dpm buildautomatically handles multi-package dependencies viamulti-package.yaml - Running
dpm buildfrom any package directory builds all dependencies first -
dpmis 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 artifactsInstallation:
curl https://get.digitalasset.com/install/install.sh | sh
export PATH="$HOME/.dpm/bin:$PATH"| Directory / item | Purpose |
|---|---|
OpenCapTable-v34/ |
Core OCF contracts (source in this repo) |
Test/ |
DAML Script tests; data-dependencies → built OpenCapTable-v34 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.
All OCF object types are implemented. See ADR-001 for architecture and excluded/deprecated types.
Tasks: Tracked in Linear under the Eng team (OCP and related contract work). See canton/AGENTS.md for Linear workflow.
Before submitting a PR, verify by running:
npm run build # All packages must build
npm run test # All tests must passAfter DAML or codegen pipeline changes, run npm run codegen then npm run verify-package so the merged lib/ tree matches what CI publishes.
| 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.
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.
-
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 : Partyas first choice param when controller varies
-
assert*- Includes error message, performs assertion (fail fast) -
validate*- ReturnsBoolfor composability; combine withassertMsgfor context
- 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:
- Fix contract validation to match schema intent.
- Add regression tests for the exact offending field/value.
- Perform a package minor upgrade for the affected DAML package.
- One-line file summary (first line of the file, before
module) module ... where- Imports
-
templateblock - Main object
datarecord (with OCF object block + schema URL on the type) - Subtype/helper
datadefinitions - Validators immediately after types
Skip the one-line summary in test-only modules if it adds no signal.
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
...
- Lifecycle order when clear, otherwise alphabetical
- Create-style choices before archive-style
data Example = Example with
id: Text -- ID first
required_field: Text -- Required (alphabetical)
items: [Text] -- Arrays (alphabetical)
optional_field: Optional Text -- Optional (alphabetical)
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
- 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
UpdateCapTablechoice for efficient bulk creates/edits/deletes
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 tiersCapTable.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.
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 & 2Sum 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
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
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/-
Major upgrade (breaking change): Creates new package directory (e.g.,
OpenCapTable-v33→OpenCapTable-v34) -
Minor upgrade (non-breaking): Increments patch version (e.g.,
0.0.1→0.0.2)
AI agents: Never perform a major version upgrade without explicit instructions from the user.
For this repo, the versioned OCP package folder is OpenCapTable-vNN (e.g. OpenCapTable-v34). The script matches the base name of that folder (e.g. OpenCapTable → OpenCapTable-v34).
npm run upgrade-package -- --package OpenCapTable --type majorThis 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.
-
Upgrade OCP package (this repo): e.g.
npm run upgrade-package -- --package OpenCapTable --type <major|minor>(or editdaml.yamlfor a minor bump) - Make code changes: Edit DAML files as needed
-
Build & test:
npm run build && npm run test - Document the release on the wiki Releases page (see naming convention there)
-
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
Other packages: Upload, vetting, and Splice lineages for CantonPayments and related DAML are documented and operated from fairmint/daml — not this repository.
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-darsSee DAR-Backup for full documentation.
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/ built from OpenCapTable-v34 codegen: root exports Fairmint, DA, Splice, and OCP_TEMPLATES (stable templateIds for CapTable, IssuerAuthorization, OcpFactory from scripts/create-root-index.ts). It does not export Nft, CantonPayments, or OpenCapTableReports on the root — use @fairmint/daml-js for those packages. Subpath exports: opencaptable.dar, openCapTableDarPath, ocp-factory-contract-id.json (see root package.json exports).
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, andOPEN_CAP_TABLE_DAR_EXPORT_SUBPATHonly from@fairmint/open-captable-protocol-daml-js/openCapTableDarPath(Nodefs; 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_SUBPATHis'./opencaptable.dar', matchingpackage.jsonexports["./opencaptable.dar"]for bundlers and tooling. -
Resolver-style:
require.resolve('@fairmint/open-captable-protocol-daml-js/opencaptable.dar')orcreateRequire(import.meta.url).resolve(...)from ESM.
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:
-
OPEN_CAP_TABLE_DAR_PATH— if set, must point to an existing.dar(absolute or relative toprocess.cwd()); throws if set but the file is missing. -
Packaged DAR —
getOpenCapTableDarPath()(normal install). -
siblingDarPath— optional path (absolute, or relative tosiblingSearchFrom); ignored unless it resolves to an existing file. RelativesiblingDarPathwithoutsiblingSearchFromthrows. -
Default sibling layout — when
siblingSearchFromis 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, and OCP_TEMPLATES present.
- Create a PR with your changes
- Get it reviewed and approved
-
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)
- Patch bumps (0.2.51 → 0.2.52): Automatic on every merge to main
-
Minor/Major bumps: Manually update
package.jsonversion before merging
npm run prepare-release # Bumps version, generates changelog
npm publish # Publishes to NPM (requires NPM_TOKEN)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.
| # | 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.
| Repo / package | Purpose | Docs / notes |
|---|---|---|
fairmint/daml |
Closed-source DAML monorepo (Shared, Reports, NFT, CantonPayments, equity certificate, …) | Companion to this repo 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 |
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
Develop
Decisions
Releases
- Releases (index)
- Package tag release process
- 2026-06-22 — OCP v35 conversion mechanism validation
- 2026-06-22 — OCP v35 SDK scaffold
- 2026-05-04 — OCP v34 major upgrade
- 2026-05-04 — DAML extraction / fairmint-daml
- 2026-03-05 — OCP v32 PPS-based conversion rename
- 2026-02-17 — OCP v31 ArchiveCapTable
- 2026-02-12 — OCP v31 vesting zero portion
Other Fairmint DAML
Documentation for CantonPayments, NFT, Reports, CouponMinter, equity certificate, proof-of-ownership, and related deployment playbooks lives with fairmint/daml and @fairmint/daml-js — not in this wiki.