From e13d1a927e9b2f8247238c4461c52299d7d3dfed Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 18:58:53 -0400 Subject: [PATCH 1/3] Enforce document related object references --- Test/daml/OpenCapTable/TestDocument.daml | 57 ++++++++++++++++++- scripts/codegen/generate-captable.ts | 28 +++++++++ .../codegen/templates/CapTable.daml.template | 21 +++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 52f97506..04625212 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -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 @@ -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 = [] @@ -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 = [] diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index 206122e5..498033c4 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -45,6 +45,11 @@ const SECURITY_ID_INDEX_MAPS: Record = { WarrantIssuance: 'warrant_issuances_by_security_id', }; +const OBJECT_TYPE_CONSTRUCTOR_OVERRIDES: Record = { + StakeholderRelationshipChangeEvent: 'OcfObjCeStakeholderRelationship', + StakeholderStatusChangeEvent: 'OcfObjCeStakeholderStatus', +}; + interface TypeDef { name: string; module: string; @@ -57,6 +62,7 @@ interface TypeDef { validates_stock_class_authorized_adjustment: boolean; validates_stock_issuance_authorized_shares: boolean; validates_issuer_id_reference: 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; } @@ -102,6 +108,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'); @@ -238,6 +265,7 @@ 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', + object_type_constructor: objectTypeConstructor(name), security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null, }; diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 5d61954c..8c5553d6 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -20,6 +20,7 @@ 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 (OcfObjectReference(..), OcfObjectType(..)) -- OCF Types {{#each types_alpha}} @@ -270,6 +271,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) +{{#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 @@ -403,6 +423,7 @@ template CapTable (mapsAfterEdits, []) sortedCreates + validateDocumentRelatedObjects finalMaps newIssuerCid validateIssuerAuthorizedShares finalMaps newIssuerCid -- Create new CapTable with updated maps and potentially new issuer From 5d9323ca3d7eb754f15329e5d0643d0841c822c8 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 19:20:39 -0400 Subject: [PATCH 2/3] Fix generated document imports lint --- scripts/codegen/generate-captable.ts | 2 ++ scripts/codegen/templates/CapTable.daml.template | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index 498033c4..a03e93c4 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -62,6 +62,7 @@ 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; @@ -265,6 +266,7 @@ 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, }; diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b41e0fc7..5c585a28 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -20,11 +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 (OcfObjectReference(..), OcfObjectType(..)) +import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) -- OCF Types {{#each types_alpha}} +{{#unless is_document}} import {{module}} ({{name}}(..), {{data_type}}) +{{/unless}} {{/each}} From 802c70fe8a7a85f65b6c76ec68e4570427c81a2d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 19:25:44 -0400 Subject: [PATCH 3/3] Update DAR backup for document references --- dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index d388a4c2..ae986327 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0746dde9ed79da54e1bb5e92a90c2da5af05ea0594b20f48416020c0af32790c -size 2476037 +oid sha256:5b3a2b94ae2958fd9bd75e7f3c7c8acbd2f2554e407a01111aae6224a9f06b07 +size 2496888 diff --git a/dars/dars.lock b/dars/dars.lock index 5b1d0ef3..ca5c1203 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -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": []