Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion Test/daml/OpenCapTable/TestDocument.daml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ module OpenCapTable.TestDocument where
import Fairmint.OpenCapTable.OCF.Document
import qualified Fairmint.OpenCapTable.CapTable as CT
import OpenCapTable.Setup
import OpenCapTable.TestHelpers
import Daml.Script

documentData : Text -> [OcfObjectReference] -> DocumentOcfData
documentData documentId relatedObjects = DocumentOcfData with
id = documentId
path = Some ("/docs/" <> documentId <> ".pdf")
uri = None
md5 = "0123456789abcdef0123456789abcdef"
related_objects = relatedObjects
comments = []

testCreateDocument = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
-- Add document to CapTable via batch
Expand Down Expand Up @@ -52,7 +62,7 @@ testCreateDocumentWithOptionalsSome = script do
path = None
uri = Some "https://cdn.example.com/docs/acme/charter.pdf"
md5 = "0123456789abcdef0123456789abcdef"
related_objects = [ OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISS_FOR_DOC" ]
related_objects = [ OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISSUER_1" ]
comments = ["External link"]]
edits = []
deletes = []
Expand Down Expand Up @@ -85,3 +95,48 @@ testEditDocument = script do
comments = ["Updated version"]]
deletes = []
pure ()

testDocumentRelatedIssuerReferenceMustExist = script do
TestOcp{issuer, cap_table} <- setupTestOcp

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateDocument (documentData "DOC_BAD_ISSUER_REF" [OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISSUER_MISSING"])]
edits = []
deletes = []

testDocumentRelatedObjectCreatedInSameBatchSucceeds = script do
TestOcp{issuer, cap_table} <- setupTestOcp

_ <- submit issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates =
[ CT.OcfCreateDocument (documentData "DOC_STOCK_CLASS_REF" [OcfObjectReference with object_type = OcfObjStockClass, object_id = "SC_DOC_REF"])
, CT.OcfCreateStockClass (defaultStockClass "SC_DOC_REF")
]
edits = []
deletes = []
pure ()

testDocumentEditMissingRelatedObjectFails = script do
TestOcp{issuer, cap_table} <- setupTestOcp
result <- submit issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_REF" [])]
edits = []
deletes = []

submitMustFail issuer do
exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with
creates = []
edits = [CT.OcfEditDocument (documentData "DOC_EDIT_REF" [OcfObjectReference with object_type = OcfObjStockClass, object_id = "SC_MISSING_FOR_DOC"])]
deletes = []

testDocumentUnsupportedRelatedObjectTypeFails = script do
TestOcp{issuer, cap_table} <- setupTestOcp

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateDocument (documentData "DOC_UNSUPPORTED_REF" [OcfObjectReference with object_type = OcfObjFinancing, object_id = "FINANCING_1"])]
edits = []
deletes = []
4 changes: 2 additions & 2 deletions dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar
Git LFS file not shown
4 changes: 2 additions & 2 deletions dars/dars.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
]
},
"OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": {
"sha256": "0746dde9ed79da54e1bb5e92a90c2da5af05ea0594b20f48416020c0af32790c",
"size": 2476037,
"sha256": "5b3a2b94ae2958fd9bd75e7f3c7c8acbd2f2554e407a01111aae6224a9f06b07",
"size": 2496888,
"sdkVersion": "3.4.10",
"uploadedAt": "2026-07-07T21:50:59.634Z",
"networks": []
Expand Down
30 changes: 30 additions & 0 deletions scripts/codegen/generate-captable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const SECURITY_ID_INDEX_MAPS: Record<string, string> = {
WarrantIssuance: 'warrant_issuances_by_security_id',
};

const OBJECT_TYPE_CONSTRUCTOR_OVERRIDES: Record<string, string> = {
StakeholderRelationshipChangeEvent: 'OcfObjCeStakeholderRelationship',
StakeholderStatusChangeEvent: 'OcfObjCeStakeholderStatus',
};

interface TypeDef {
name: string;
module: string;
Expand All @@ -57,6 +62,8 @@ interface TypeDef {
validates_stock_class_authorized_adjustment: boolean;
validates_stock_issuance_authorized_shares: boolean;
validates_issuer_id_reference: boolean;
is_document: boolean;
object_type_constructor: string;
// For issuance types, the name of the security_id index map (e.g., 'stock_issuances_by_security_id')
security_id_index_map: string | null;
}
Expand Down Expand Up @@ -102,6 +109,27 @@ function pluralize(str: string): string {
return `${str}s`;
}

function objectTypeConstructor(name: string): string {
if (OBJECT_TYPE_CONSTRUCTOR_OVERRIDES[name]) return OBJECT_TYPE_CONSTRUCTOR_OVERRIDES[name];
if (
/^(Convertible|EquityCompensation|Stock|Warrant).*(Acceptance|Cancellation|Conversion|Exercise|Issuance|Release|Repricing|Retraction|Transfer|Consolidation|Repurchase|Reissuance)$/.test(
name
) ||
name.startsWith('IssuerAuthorizedSharesAdjustment') ||
name.startsWith('StockClassAuthorizedSharesAdjustment') ||
name.startsWith('StockClassConversionRatioAdjustment') ||
name.startsWith('StockClassSplit') ||
name.startsWith('StockPlanPoolAdjustment') ||
name.startsWith('StockPlanReturnToPool') ||
name === 'VestingAcceleration' ||
name === 'VestingEvent' ||
name === 'VestingStart'
) {
return `OcfObjTx${name}`;
}
return `OcfObj${name}`;
}

/** Parse a DAML file to find its main data type and field name */
function parseTypeInfo(filePath: string, typeName: string): { dataType: string; fieldName: string } | null {
const content = fs.readFileSync(filePath, 'utf-8');
Expand Down Expand Up @@ -238,6 +266,8 @@ function discoverTypes(config: Config): TypeDef[] {
validates_stock_class_authorized_adjustment: name === 'StockClassAuthorizedSharesAdjustment',
validates_stock_issuance_authorized_shares: name === 'StockIssuance',
validates_issuer_id_reference: name === 'IssuerAuthorizedSharesAdjustment',
is_document: name === 'Document',
object_type_constructor: objectTypeConstructor(name),
security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null,
};

Expand Down
23 changes: 23 additions & 0 deletions scripts/codegen/templates/CapTable.daml.template
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import DA.Optional (isNone, isSome)
import Fairmint.OpenCapTable.Types.Core (Context)
import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..))
import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData)
import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..))

-- OCF Types
{{#each types_alpha}}
{{#unless is_document}}
import {{module}} ({{name}}(..), {{data_type}})
{{/unless}}
{{/each}}


Expand Down Expand Up @@ -269,6 +272,25 @@ validateIssuerReference issuerCid issuerId = do
("Issuer ID mismatch: " <> issuerId <> " does not match cap table issuer " <> issuerContract.issuer_data.id)
(issuerId == issuerContract.issuer_data.id)

validateObjectReference : CapTableMaps -> ContractId Issuer -> OcfObjectReference -> Update ()
validateObjectReference maps issuerCid ref = case ref.object_type of
OcfObjIssuer -> do
issuerContract <- fetch issuerCid
assertMsg ("Issuer not found: " <> ref.object_id) (ref.object_id == issuerContract.issuer_data.id)
Comment on lines +277 to +279
{{#each types_alpha}}
{{object_type_constructor}} ->
assertMsg ("{{name}} not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.{{map_field}}))
{{/each}}
_ -> assertMsg ("Unsupported related object type: " <> ref.object_id) False

validateDocumentRelatedObjects : CapTableMaps -> ContractId Issuer -> Update ()
validateDocumentRelatedObjects maps issuerCid =
mapA_
(\documentCid -> do
document <- fetch documentCid
mapA_ (validateObjectReference maps issuerCid) document.document_data.related_objects)
(Map.values maps.documents)

-- | Get the processing tier for a create operation
getCreateTier : OcfCreateData -> Int
getCreateTier createData = case createData of
Expand Down Expand Up @@ -402,6 +424,7 @@ template CapTable
(mapsAfterEdits, [])
sortedCreates

validateDocumentRelatedObjects finalMaps newIssuerCid
validateIssuerAuthorizedShares finalMaps newIssuerCid

-- Create new CapTable with updated maps and potentially new issuer
Expand Down
Loading