From 87737fa232347c4973afe0aef0a59dc541a90f94 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 21:03:59 -0400 Subject: [PATCH 01/50] Enforce stock plan reserved share ceilings --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestStockPlanReservedShares.daml | 263 ++++++++++++++++++ .../TestStockPlanReturnToPool.daml | 62 +++-- .../0.0.5/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + .../codegen/templates/CapTable.daml.template | 158 +++++++++++ 7 files changed, 470 insertions(+), 27 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestStockPlanReservedShares.daml create mode 100644 dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 4d300d29..f16d912a 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.4 +version: 0.0.5 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index c6e7f2d6..64cd009c 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.4.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.5.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml new file mode 100644 index 00000000..23672c45 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -0,0 +1,263 @@ +module OpenCapTable.TestStockPlanReservedShares where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +stockPlanPoolAdjustment : Text -> Text -> Decimal -> StockPlanPoolAdjustmentOcfData +stockPlanPoolAdjustment adjustmentId stockPlanId sharesReserved = StockPlanPoolAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_plan_id = stockPlanId + shares_reserved = sharesReserved + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockPlanReturnToPool : Text -> Text -> Text -> Decimal -> StockPlanReturnToPoolOcfData +stockPlanReturnToPool returnId securityId stockPlanId quantity = StockPlanReturnToPoolOcfData with + id = returnId + date = defaultTestDate + security_id = securityId + stock_plan_id = stockPlanId + quantity = quantity + reason_text = "Returned to pool" + comments = [] + +equityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + stockholder_approval_date = None + vesting_terms_id = None + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + comments = [] + +createPlanAndStockIssuance : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Decimal -> + Script (ContractId CT.CapTable, Text, Text) +createPlanAndStockIssuance issuer capTableCid stockPlanId reservedShares issuedShares = do + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer capTableCid + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = reservedShares + let issuance = (defaultStockIssuance ("TX_" <> stockPlanId) ("SEC-" <> stockPlanId) stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = issuedShares + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure (result.updatedCapTableCid, stockClassId, stakeholderId) + +testStockPlanReservedShares_StockIssuanceCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_OVER_POOL" "SEC-STOCK-OVER-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_OVER_POOL" "SEC-EC-OVER-POOL" stakeholderId stockClassId stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_ADJUSTED" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_UP" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_ADJUSTED_POOL" "SEC-STOCK-ADJUSTED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SAME_DATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let higherAdjustment = stockPlanPoolAdjustment "TX_POOL_HIGH" stockPlanId 200.0 + let lowerAdjustment = stockPlanPoolAdjustment "TX_POOL_LOW" stockPlanId 120.0 + let issuance = (defaultStockIssuance "TX_STOCK_SAME_DATE_POOL" "SEC-STOCK-SAME-DATE-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment higherAdjustment + , CT.OcfCreateStockPlanPoolAdjustment lowerAdjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_REUSE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = equityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_TO_POOL" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = equityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_OVER" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_RETURN_OVER" "SEC-EC-RETURN-OVER" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_OVER" issuance.security_id stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolMustMatchSecurityPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_SOURCE" + let targetPlanId = "PLAN_RETURN_TARGET" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_RETURN_WRONG_PLAN" "SEC-EC-RETURN-WRONG-PLAN" stakeholderId stockClassId sourcePlanId 50.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_WRONG_PLAN" issuance.security_id targetPlanId 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_ADJ_DROP" 200.0 150.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlanPoolAdjustment (stockPlanPoolAdjustment "TX_POOL_DOWN" "PLAN_ADJ_DROP" 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_DeleteAdjustmentCannotInvalidateAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_DELETE_ADJ" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_DELETE" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_DELETE_ADJ" "SEC-STOCK-DELETE-ADJ" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlanPoolAdjustment adjustment.id] + +testStockPlanReservedShares_StockPlanEditCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, stockClassId, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_EDIT_DROP" 200.0 150.0 + let editedPlan = (defaultStockPlan "PLAN_EDIT_DROP" stockClassId) with initial_shares_reserved = 100.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml index 53da24bb..019e8539 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml @@ -4,7 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockPlan import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStockClass) +import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -14,18 +14,24 @@ import Daml.Script -- Test: Create and archive return to pool (termination scenario) testCreateAndArchiveStockPlanReturnToPool = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStockClass issuer cap_table + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_001" result <- submit issuer do - exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2021" - plan_name = "2021 Equity Incentive Plan" - initial_shares_reserved = 1_000_000.0 - board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) - stockholder_approval_date = None - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2021" + plan_name = "2021 Equity Incentive Plan" + initial_shares_reserved = 1_000_000.0 + board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) + stockholder_approval_date = None + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_001" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2021" + quantity = 25000.0) + ] edits = [] deletes = [] -- Employee termination - unvested options return to pool @@ -34,7 +40,7 @@ testCreateAndArchiveStockPlanReturnToPool = script do creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with id = "TX_RETURN_TO_POOL_1" date = DT.time (DA.date 2024 Jun 30) 0 0 0 - security_id = "SEC_OPT_001" + security_id = securityId stock_plan_id = "PLAN_2021" quantity = 25000.0 reason_text = "Employee termination - unvested options cancelled and returned to pool" @@ -47,18 +53,24 @@ testCreateAndArchiveStockPlanReturnToPool = script do -- Test: Create and archive with Archive choice (expiration scenario) testCreateAndArchiveStockPlanReturnToPoolExpiration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStockClass issuer cap_table + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_002" result <- submit issuer do - exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2015" - plan_name = "2015 Stock Option Plan" - initial_shares_reserved = 500_000.0 - board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2015" + plan_name = "2015 Stock Option Plan" + initial_shares_reserved = 500_000.0 + board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_002" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2015" + quantity = 10000.0) + ] edits = [] deletes = [] -- Expired unexercised options return to pool @@ -67,7 +79,7 @@ testCreateAndArchiveStockPlanReturnToPoolExpiration = script do creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with id = "TX_RETURN_TO_POOL_2" date = DT.time (DA.date 2025 Mar 15) 0 0 0 - security_id = "SEC_OPT_002" + security_id = securityId stock_plan_id = "PLAN_2015" quantity = 10000.0 reason_text = "Options expired unexercised after 10-year term" diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar new file mode 100644 index 00000000..9e5f0cd2 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e38575e820c30da4730470506f0ed9e2683f63917081b7da40df4c0970ceb5c5 +size 2543505 diff --git a/dars/dars.lock b/dars/dars.lock index f55f1f97..0390bd54 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -31,6 +31,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:49:24.957Z", "networks": [] + }, + "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { + "sha256": "e38575e820c30da4730470506f0ed9e2683f63917081b7da40df4c0970ceb5c5", + "size": 2543505, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T01:03:00.766Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ab4511ee..17288a0d 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -154,6 +154,14 @@ issuerAuthorizedAdjustmentTakesPrecedence candidate current = candidate.date > current.date || (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) +stockPlanPoolAdjustmentTakesPrecedence : + StockPlanPoolAdjustmentOcfData -> + StockPlanPoolAdjustmentOcfData -> + Bool +stockPlanPoolAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.shares_reserved < current.shares_reserved) + stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) stockClassAuthorizedAdjustmentIndex maps = foldlA @@ -212,6 +220,31 @@ currentIssuerAuthorizedLimit maps issuerCid = do Some adjustment -> Some adjustment.new_shares_authorized None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized +stockPlanPoolAdjustmentIndex : CapTableMaps -> Update (Map Text StockPlanPoolAdjustmentOcfData) +stockPlanPoolAdjustmentIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let stockPlanId = adjustment.adjustment_data.stock_plan_id + let nextIndex = case Map.lookup stockPlanId index of + Some current -> + if stockPlanPoolAdjustmentTakesPrecedence adjustment.adjustment_data current + then Map.insert stockPlanId adjustment.adjustment_data index + else index + None -> Map.insert stockPlanId adjustment.adjustment_data index + pure nextIndex) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + +currentStockPlanReservedShares : + Map Text StockPlanPoolAdjustmentOcfData -> + StockPlanOcfData -> + Decimal +currentStockPlanReservedShares adjustmentIndex stockPlanData = + case Map.lookup stockPlanData.id adjustmentIndex of + Some adjustment -> adjustment.shares_reserved + None -> stockPlanData.initial_shares_reserved + nonNegativeDecimal : Decimal -> Decimal nonNegativeDecimal n = if n < 0.0 then 0.0 else n @@ -372,6 +405,130 @@ validateAuthorizedShareCeilings maps issuerCid = do validateAllStockClassAuthorizedShares maps adjustmentIndex validateIssuerAuthorizedShares maps adjustmentIndex issuerCid +stockIssuancePlanAllocation : CapTableMaps -> Text -> Update Decimal +stockIssuancePlanAllocation maps stockPlanId = + foldlA + (\total issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some planId -> + if planId == stockPlanId + then pure (total + issuance.issuance_data.quantity) + else pure total + None -> pure total) + 0.0 + (Map.values maps.stock_issuances) + +equityCompensationPlanAllocation : CapTableMaps -> Text -> Update Decimal +equityCompensationPlanAllocation maps stockPlanId = + foldlA + (\total issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some planId -> + if planId == stockPlanId + then pure (total + issuance.issuance_data.quantity) + else pure total + None -> pure total) + 0.0 + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnedQuantity : CapTableMaps -> Text -> Update Decimal +stockPlanReturnedQuantity maps stockPlanId = + foldlA + (\total returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.stock_plan_id == stockPlanId + then pure (total + returnToPool.return_data.quantity) + else pure total) + 0.0 + (Map.values maps.stock_plan_return_to_pools) + +stockPlanReturnedQuantityForSecurity : CapTableMaps -> Text -> Update Decimal +stockPlanReturnedQuantityForSecurity maps securityId = + foldlA + (\total returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.security_id == securityId + then pure (total + returnToPool.return_data.quantity) + else pure total) + 0.0 + (Map.values maps.stock_plan_return_to_pools) + +stockPlanSecurityAllocation : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal +stockPlanSecurityAllocation maps returnData = do + let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id + let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id + case (stockCidOpt, equityCompensationCidOpt) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertMsg + ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) + (stockPlanId == returnData.stock_plan_id) + pure issuance.issuance_data.quantity + None -> do + assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False + pure 0.0 + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertMsg + ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) + (stockPlanId == returnData.stock_plan_id) + pure issuance.issuance_data.quantity + None -> do + assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False + pure 0.0 + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> returnData.security_id) False + pure 0.0 + (None, None) -> do + assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False + pure 0.0 + +validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () +validateStockPlanReturnToPoolQuantities maps = + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + securityAllocation <- stockPlanSecurityAllocation maps returnToPool.return_data + returnedQuantity <- stockPlanReturnedQuantityForSecurity maps returnToPool.return_data.security_id + assertMsg + ("Stock plan return to pool exceeds security allocation for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds allocated " <> show securityAllocation) + (returnedQuantity <= securityAllocation)) + (Map.values maps.stock_plan_return_to_pools) + +stockPlanNetAllocatedQuantity : CapTableMaps -> Text -> Update Decimal +stockPlanNetAllocatedQuantity maps stockPlanId = do + stockAllocation <- stockIssuancePlanAllocation maps stockPlanId + equityCompensationAllocation <- equityCompensationPlanAllocation maps stockPlanId + returnedQuantity <- stockPlanReturnedQuantity maps stockPlanId + pure (stockAllocation + equityCompensationAllocation - returnedQuantity) + +validateStockPlanReservedShares : + CapTableMaps -> + Map Text StockPlanPoolAdjustmentOcfData -> + Update () +validateStockPlanReservedShares maps adjustmentIndex = + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data + allocatedQuantity <- stockPlanNetAllocatedQuantity maps stockPlan.plan_data.id + assertMsg + ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) + (allocatedQuantity <= reservedShares)) + (Map.values maps.stock_plans) + +validateStockPlanPoolCeilings : CapTableMaps -> Update () +validateStockPlanPoolCeilings maps = do + adjustmentIndex <- stockPlanPoolAdjustmentIndex maps + validateStockPlanReturnToPoolQuantities maps + validateStockPlanReservedShares maps adjustmentIndex + validateIssuerReference : ContractId Issuer -> Text -> Update () validateIssuerReference issuerCid issuerId = do issuerContract <- fetch issuerCid @@ -623,6 +780,7 @@ template CapTable validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps validateAuthorizedShareCeilings finalMaps newIssuerCid + validateStockPlanPoolCeilings finalMaps -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with From 283256c5a3ba23d5f105265de638c338fcce3a2f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:18:35 -0400 Subject: [PATCH 02/50] Restack stock plan ceiling on reference integrity --- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 916c6299..322a9aae 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab899345724b6bd4ee0ce173e84407ea05d45966deaffa75183feebc108ff61e -size 2555738 +oid sha256:419453f4e4e7ffa4c7ffb8846b7e19f6ec73e27baa63ecec17e0060c38e3d4ba +size 2556855 diff --git a/dars/dars.lock b/dars/dars.lock index d074fe9f..e88aebd7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "ab899345724b6bd4ee0ce173e84407ea05d45966deaffa75183feebc108ff61e", - "size": 2555738, + "sha256": "419453f4e4e7ffa4c7ffb8846b7e19f6ec73e27baa63ecec17e0060c38e3d4ba", + "size": 2556855, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T02:27:43.397Z", "networks": [] From ecd24228c15626029815d4e1162d59dd75eedc56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 03:39:23 +0000 Subject: [PATCH 03/50] Optimize stock plan pool ceiling validation indexes --- .../codegen/templates/CapTable.daml.template | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 1e244597..edd33669 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -469,54 +469,49 @@ validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits = _ -> pure ()) edits -stockIssuancePlanAllocation : CapTableMaps -> Text -> Update Decimal -stockIssuancePlanAllocation maps stockPlanId = +addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal +addQuantityById id quantity totals = + let current = fromOptional 0.0 (Map.lookup id totals) + in Map.insert id (current + quantity) totals + +stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndex maps = foldlA - (\total issuanceCid -> do + (\totals issuanceCid -> do issuance <- fetch issuanceCid case issuance.issuance_data.stock_plan_id of - Some planId -> - if planId == stockPlanId - then pure (total + issuance.issuance_data.quantity) - else pure total - None -> pure total) - 0.0 + Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + None -> pure totals) + Map.empty (Map.values maps.stock_issuances) -equityCompensationPlanAllocation : CapTableMaps -> Text -> Update Decimal -equityCompensationPlanAllocation maps stockPlanId = +equityCompensationPlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndex maps = foldlA - (\total issuanceCid -> do + (\totals issuanceCid -> do issuance <- fetch issuanceCid case issuance.issuance_data.stock_plan_id of - Some planId -> - if planId == stockPlanId - then pure (total + issuance.issuance_data.quantity) - else pure total - None -> pure total) - 0.0 + Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + None -> pure totals) + Map.empty (Map.values maps.equity_compensation_issuances) -stockPlanReturnedQuantity : CapTableMaps -> Text -> Update Decimal -stockPlanReturnedQuantity maps stockPlanId = +stockPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanReturnedQuantityIndex maps = foldlA - (\total returnCid -> do + (\totals returnCid -> do returnToPool <- fetch returnCid - if returnToPool.return_data.stock_plan_id == stockPlanId - then pure (total + returnToPool.return_data.quantity) - else pure total) - 0.0 + pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals)) + Map.empty (Map.values maps.stock_plan_return_to_pools) -stockPlanReturnedQuantityForSecurity : CapTableMaps -> Text -> Update Decimal -stockPlanReturnedQuantityForSecurity maps securityId = +stockPlanReturnedQuantityBySecurityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanReturnedQuantityBySecurityIndex maps = foldlA - (\total returnCid -> do + (\totals returnCid -> do returnToPool <- fetch returnCid - if returnToPool.return_data.security_id == securityId - then pure (total + returnToPool.return_data.quantity) - else pure total) - 0.0 + pure (addQuantityById returnToPool.return_data.security_id returnToPool.return_data.quantity totals)) + Map.empty (Map.values maps.stock_plan_return_to_pools) stockPlanSecurityAllocation : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal @@ -554,34 +549,41 @@ stockPlanSecurityAllocation maps returnData = do pure 0.0 validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () -validateStockPlanReturnToPoolQuantities maps = +validateStockPlanReturnToPoolQuantities maps = do + returnedQuantityBySecurity <- stockPlanReturnedQuantityBySecurityIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid securityAllocation <- stockPlanSecurityAllocation maps returnToPool.return_data - returnedQuantity <- stockPlanReturnedQuantityForSecurity maps returnToPool.return_data.security_id + let returnedQuantity = fromOptional 0.0 (Map.lookup returnToPool.return_data.security_id returnedQuantityBySecurity) assertMsg ("Stock plan return to pool exceeds security allocation for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds allocated " <> show securityAllocation) (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) -stockPlanNetAllocatedQuantity : CapTableMaps -> Text -> Update Decimal -stockPlanNetAllocatedQuantity maps stockPlanId = do - stockAllocation <- stockIssuancePlanAllocation maps stockPlanId - equityCompensationAllocation <- equityCompensationPlanAllocation maps stockPlanId - returnedQuantity <- stockPlanReturnedQuantity maps stockPlanId - pure (stockAllocation + equityCompensationAllocation - returnedQuantity) +stockPlanNetAllocatedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndex maps = do + stockAllocation <- stockIssuancePlanAllocationIndex maps + equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps + returnedQuantity <- stockPlanReturnedQuantityIndex maps + let allocatedByPlan = Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId quantity totals) Map.empty stockAllocation + let allocatedByPlanWithEquityCompensation = + Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId quantity totals) allocatedByPlan equityCompensationAllocation + let netAllocatedByPlan = + Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId (-quantity) totals) allocatedByPlanWithEquityCompensation returnedQuantity + pure netAllocatedByPlan validateStockPlanReservedShares : CapTableMaps -> Map Text StockPlanPoolAdjustmentOcfData -> Update () -validateStockPlanReservedShares maps adjustmentIndex = +validateStockPlanReservedShares maps adjustmentIndex = do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex maps mapA_ (\stockPlanCid -> do stockPlan <- fetch stockPlanCid let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data - allocatedQuantity <- stockPlanNetAllocatedQuantity maps stockPlan.plan_data.id + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) assertMsg ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) (allocatedQuantity <= reservedShares)) From 73ad3fc2e76f54eec3be39ae59cf3e064dea9a1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 03:40:02 +0000 Subject: [PATCH 04/50] Refine map aggregation helpers for stock plan checks --- scripts/codegen/templates/CapTable.daml.template | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index edd33669..2c370a48 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -470,9 +470,13 @@ validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits = edits addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal -addQuantityById id quantity totals = - let current = fromOptional 0.0 (Map.lookup id totals) - in Map.insert id (current + quantity) totals +addQuantityById key quantity totals = + let current = fromOptional 0.0 (Map.lookup key totals) + in Map.insert key (current + quantity) totals + +mergeQuantityMaps : Map Text Decimal -> Map Text Decimal -> Map Text Decimal +mergeQuantityMaps base delta = + Map.foldlWithKey (\totals key quantity -> addQuantityById key quantity totals) base delta stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) stockIssuancePlanAllocationIndex maps = @@ -566,9 +570,8 @@ stockPlanNetAllocatedQuantityIndex maps = do stockAllocation <- stockIssuancePlanAllocationIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps - let allocatedByPlan = Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId quantity totals) Map.empty stockAllocation - let allocatedByPlanWithEquityCompensation = - Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId quantity totals) allocatedByPlan equityCompensationAllocation + let allocatedByPlan = mergeQuantityMaps Map.empty stockAllocation + let allocatedByPlanWithEquityCompensation = mergeQuantityMaps allocatedByPlan equityCompensationAllocation let netAllocatedByPlan = Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId (-quantity) totals) allocatedByPlanWithEquityCompensation returnedQuantity pure netAllocatedByPlan From 5fa720492fee65f7ac6639fc365f5264ada325fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 03:40:35 +0000 Subject: [PATCH 05/50] Simplify stock plan net allocation map merging --- scripts/codegen/templates/CapTable.daml.template | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 2c370a48..aeb83044 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -478,6 +478,10 @@ mergeQuantityMaps : Map Text Decimal -> Map Text Decimal -> Map Text Decimal mergeQuantityMaps base delta = Map.foldlWithKey (\totals key quantity -> addQuantityById key quantity totals) base delta +negateQuantityMap : Map Text Decimal -> Map Text Decimal +negateQuantityMap = + Map.foldlWithKey (\totals key quantity -> Map.insert key (-quantity) totals) Map.empty + stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) stockIssuancePlanAllocationIndex maps = foldlA @@ -570,10 +574,9 @@ stockPlanNetAllocatedQuantityIndex maps = do stockAllocation <- stockIssuancePlanAllocationIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps - let allocatedByPlan = mergeQuantityMaps Map.empty stockAllocation + let allocatedByPlan = stockAllocation let allocatedByPlanWithEquityCompensation = mergeQuantityMaps allocatedByPlan equityCompensationAllocation - let netAllocatedByPlan = - Map.foldlWithKey (\totals stockPlanId quantity -> addQuantityById stockPlanId (-quantity) totals) allocatedByPlanWithEquityCompensation returnedQuantity + let netAllocatedByPlan = mergeQuantityMaps allocatedByPlanWithEquityCompensation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan validateStockPlanReservedShares : From 9fb27ae7447b77f47f618d7e75f250e803eaaa0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 03:41:09 +0000 Subject: [PATCH 06/50] Polish stock plan allocation variable naming --- scripts/codegen/templates/CapTable.daml.template | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index aeb83044..1bb35905 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -574,9 +574,8 @@ stockPlanNetAllocatedQuantityIndex maps = do stockAllocation <- stockIssuancePlanAllocationIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps - let allocatedByPlan = stockAllocation - let allocatedByPlanWithEquityCompensation = mergeQuantityMaps allocatedByPlan equityCompensationAllocation - let netAllocatedByPlan = mergeQuantityMaps allocatedByPlanWithEquityCompensation (negateQuantityMap returnedQuantity) + let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation + let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan validateStockPlanReservedShares : From 43c394f8176adde0403f4a93e9a73835fd971b32 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 01:22:57 -0400 Subject: [PATCH 07/50] Restack stock plan ceiling on reference fixes --- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- scripts/codegen/templates/CapTable.daml.template | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 68a85e9b..08fea0de 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:594af8844ca7c1b7f1ac36a72a2fc6468545ff65cf4630b90261133fb197b2aa -size 2562179 +oid sha256:1d05fa87a480b79c1539a37f0d09956ca9c0a0ba29f31808e54a3721a71ec47f +size 2566265 diff --git a/dars/dars.lock b/dars/dars.lock index 2dd914cf..27b7106e 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "594af8844ca7c1b7f1ac36a72a2fc6468545ff65cf4630b90261133fb197b2aa", - "size": 2562179, + "sha256": "1d05fa87a480b79c1539a37f0d09956ca9c0a0ba29f31808e54a3721a71ec47f", + "size": 2566265, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T02:27:43.397Z", + "uploadedAt": "2026-07-09T05:22:23.000Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b26be46e..42e70137 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -476,11 +476,11 @@ addQuantityById key quantity totals = mergeQuantityMaps : Map Text Decimal -> Map Text Decimal -> Map Text Decimal mergeQuantityMaps base delta = - Map.foldlWithKey (\totals key quantity -> addQuantityById key quantity totals) base delta + foldl (\totals (key, quantity) -> addQuantityById key quantity totals) base (Map.toList delta) negateQuantityMap : Map Text Decimal -> Map Text Decimal -negateQuantityMap = - Map.foldlWithKey (\totals key quantity -> Map.insert key (-quantity) totals) Map.empty +negateQuantityMap quantities = + foldl (\totals (key, quantity) -> Map.insert key (-quantity) totals) Map.empty (Map.toList quantities) stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) stockIssuancePlanAllocationIndex maps = From 771f7b8da341fb9b198ba361e1a19768dff0a1e4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 01:27:47 -0400 Subject: [PATCH 08/50] Refresh stock plan DAR after reference restack --- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 08fea0de..cffd6171 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d05fa87a480b79c1539a37f0d09956ca9c0a0ba29f31808e54a3721a71ec47f -size 2566265 +oid sha256:1e1acc974706f162d28e746ee2a3b40c0ee624171bcdacda7277157363232176 +size 2562978 diff --git a/dars/dars.lock b/dars/dars.lock index b2d6e7cb..7aa68307 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "1d05fa87a480b79c1539a37f0d09956ca9c0a0ba29f31808e54a3721a71ec47f", - "size": 2566265, + "sha256": "1e1acc974706f162d28e746ee2a3b40c0ee624171bcdacda7277157363232176", + "size": 2562978, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T05:22:23.000Z", + "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] } } From e64b2f44352b4be9a0a353370f6c965be4ead039 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 01:48:07 -0400 Subject: [PATCH 09/50] Enforce unique security arrays --- .../OpenCapTable/OCF/ConvertibleTransfer.daml | 4 +- .../OCF/EquityCompensationTransfer.daml | 4 +- .../OpenCapTable/OCF/StockConsolidation.daml | 3 +- .../OpenCapTable/OCF/StockTransfer.daml | 4 +- .../OpenCapTable/OCF/WarrantTransfer.daml | 4 +- .../OpenCapTable/Types/Validation.daml | 9 +++ .../OpenCapTable/TestStockConsolidation.daml | 17 +++++ Test/daml/OpenCapTable/TestStockTransfer.daml | 20 ++++++ .../TestUniqueSecurityArrays.daml | 71 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 11 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml index 35f6b119..465c1c80 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/ConvertibleTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -66,7 +66,7 @@ validateConvertibleTransferOcfData d = d.amount.amount > 0.0 && not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml index 619916df..66e3a9be 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/EquityCompensationTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,7 +61,7 @@ validateEquityCompensationTransferOcfData d = d.quantity > 0.0 && not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml index ca6c29b4..43dff6b0 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConsolidation where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/consolidation/StockConsolidation.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,5 @@ validateStockConsolidationOcfData d = -- Arrays element validation (security_ids required per Consolidation base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.security_ids && + validateUniqueStringsArray d.security_ids && not (null d.security_ids) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml index 2b20b1a9..6ad3bf7d 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/StockTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,7 +61,7 @@ validateStockTransferOcfData d = d.quantity > 0.0 && not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml index 2238151d..6c7b1c44 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/WarrantTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,7 +61,7 @@ validateWarrantTransferOcfData d = d.quantity > 0.0 && not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index dcda2c04..ef67ff9a 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -15,6 +15,15 @@ import Fairmint.Shared.TypeHelpers (validateRequiredText) validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs +hasDuplicateTexts : [Text] -> Bool +hasDuplicateTexts texts = case texts of + [] -> False + text :: rest -> text `elem` rest || hasDuplicateTexts rest + +-- Ensures no element appears more than once; empty arrays are allowed +validateUniqueStringsArray : [Text] -> Bool +validateUniqueStringsArray xs = not (hasDuplicateTexts xs) + -- Country code -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountryCode.schema.json validCountryCodes : [Text] diff --git a/Test/daml/OpenCapTable/TestStockConsolidation.daml b/Test/daml/OpenCapTable/TestStockConsolidation.daml index 4598bf2c..e486062e 100644 --- a/Test/daml/OpenCapTable/TestStockConsolidation.daml +++ b/Test/daml/OpenCapTable/TestStockConsolidation.daml @@ -59,6 +59,23 @@ testStockConsolidation_TwoSecurities = script do deletes = [] pure () +testStockConsolidation_DuplicateSecurityIdsRejected = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + result <- createStockIssuance issuer capTableCid "SSEC-DUP-CONSOLIDATE" 100.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + id = "TX_STOCK_CONSOLIDATION_DUP" + date = DT.time (DA.date 2025 Apr 15) 0 0 0 + security_ids = ["SSEC-DUP-CONSOLIDATE", "SSEC-DUP-CONSOLIDATE"] + resulting_security_id = "SSEC-DUP-CONSOLIDATED" + comments = [] + reason_text = Some "Duplicate source security IDs are not schema-valid"] + edits = [] + deletes = [] + pure () + -- Test 2: Consolidation of multiple securities with comments testStockConsolidation_MultipleSecurities = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index 5f65170d..b4b2ffd7 100644 --- a/Test/daml/OpenCapTable/TestStockTransfer.daml +++ b/Test/daml/OpenCapTable/TestStockTransfer.daml @@ -61,6 +61,26 @@ testStockTransfer_SplitTransfer = script do deletes = [] pure () +testStockTransfer_DuplicateResultingSecurityIdsRejected = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SSEC-DUP-TRANSFER-001" + result <- createStockIssuance issuer capTableCid securityId 100.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_DUP_RESULT" + date = DT.time (DA.date 2025 Aug 05) 0 0 0 + quantity = 50.0 + security_id = securityId + resulting_security_ids = ["SSEC-DUP-RESULT", "SSEC-DUP-RESULT"] + comments = [] + balance_security_id = Some "SSEC-DUP-BALANCE" + consideration_text = None] + edits = [] + deletes = [] + pure () + -- Test 2: Transfer with consideration text and comments (founder transfer) -- Based on prod: Early founder stock transfer with detailed consideration testStockTransfer_FounderTransferWithConsideration = script do diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml new file mode 100644 index 00000000..566e1112 --- /dev/null +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -0,0 +1,71 @@ +module OpenCapTable.TestUniqueSecurityArrays where + +import Fairmint.OpenCapTable.OCF.ConvertibleTransfer +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantTransfer +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.TestHelpers (defaultTestDate) +import Daml.Script + +duplicateResultingSecurityIds : [Text] +duplicateResultingSecurityIds = ["SEC-DUP", "SEC-DUP"] + +assertInvalid : Text -> Bool -> Script () +assertInvalid label validationResult = + assertMsg (label <> " should reject duplicate security IDs") (not validationResult) + +testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = script do + assertInvalid "stock transfer" $ + validateStockTransferOcfData StockTransferOcfData with + id = "TX_STOCK_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "SSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "convertible transfer" $ + validateConvertibleTransferOcfData ConvertibleTransferOcfData with + id = "TX_CONVERTIBLE_TRANSFER_DUP" + amount = OcfMonetary with amount = 1.0, currency = "USD" + date = defaultTestDate + security_id = "CSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "warrant transfer" $ + validateWarrantTransferOcfData WarrantTransferOcfData with + id = "TX_WARRANT_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "WSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "equity compensation transfer" $ + validateEquityCompensationTransferOcfData EquityCompensationTransferOcfData with + id = "TX_EC_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "stock consolidation" $ + validateStockConsolidationOcfData StockConsolidationOcfData with + id = "TX_STOCK_CONSOLIDATION_DUP" + date = defaultTestDate + resulting_security_id = "SSEC-CONSOLIDATED" + comments = [] + security_ids = ["SSEC-1", "SSEC-1"] + reason_text = None diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index cffd6171..0bf28bee 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e1acc974706f162d28e746ee2a3b40c0ee624171bcdacda7277157363232176 -size 2562978 +oid sha256:677a5e3ba83c601b34fd9f42706d4c34c93e7721322aedc58985206dc25cdea1 +size 2565235 diff --git a/dars/dars.lock b/dars/dars.lock index 7aa68307..d465809a 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "1e1acc974706f162d28e746ee2a3b40c0ee624171bcdacda7277157363232176", - "size": 2562978, + "sha256": "677a5e3ba83c601b34fd9f42706d4c34c93e7721322aedc58985206dc25cdea1", + "size": 2565235, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From f4b9bf247d5e8027be1e1bef1c24527c8f1e62e7 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 02:03:16 -0400 Subject: [PATCH 10/50] Enforce document location oneOf --- .../Fairmint/OpenCapTable/OCF/Document.daml | 9 +++--- Test/daml/OpenCapTable/TestDocument.daml | 31 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml index ccc35a9c..f3861add 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml @@ -54,10 +54,10 @@ validateOcfDocument d = validateOcfMd5 d.md5 && validateOptionalText d.path && validateOptionalText d.uri && - ((case (d.path, d.uri) of - (Some _, _) -> True - (_, Some _) -> True - _ -> False)) && + (case (d.path, d.uri) of + (Some _, None) -> True + (None, Some _) -> True + _ -> False) && validateNonEmptyStringsArray d.comments && all validateOcfObjectReference d.related_objects @@ -95,4 +95,3 @@ data OcfObjectType = | OcfObjTxWarrantAcceptance | OcfObjTxWarrantCancellation | OcfObjTxWarrantExercise | OcfObjTxWarrantIssuance | OcfObjTxWarrantRetraction | OcfObjTxWarrantTransfer | OcfObjTxVestingAcceleration | OcfObjTxVestingStart | OcfObjTxVestingEvent deriving (Eq, Show) - diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 15fb202e..87c3107d 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -68,6 +68,21 @@ testCreateDocumentWithOptionalsSome = script do deletes = [] pure () +testDocumentCreateWithPathAndUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument DocumentOcfData with + id = "DOC_PATH_AND_URI" + path = Some "/docs/acme/charter.pdf" + uri = Some "https://cdn.example.com/docs/acme/charter.pdf" + md5 = "0123456789abcdef0123456789abcdef" + related_objects = [] + comments = []] + edits = [] + deletes = [] + testEditDocument = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add document via batch @@ -96,6 +111,22 @@ testEditDocument = script do deletes = [] pure () +testDocumentEditWithPathAndUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_PATH_URI" [])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditDocument ((documentData "DOC_EDIT_PATH_URI" []) with + path = Some "/docs/acme/charter.pdf" + uri = Some "https://cdn.example.com/docs/acme/charter.pdf")] + deletes = [] + testDocumentRelatedIssuerReferenceMustExist = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 0bf28bee..732158c8 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:677a5e3ba83c601b34fd9f42706d4c34c93e7721322aedc58985206dc25cdea1 -size 2565235 +oid sha256:79a3b0127be2a2a64899b1952367db3aa3f980a9ca72bfd61ac84dcc57c18f0c +size 2565180 diff --git a/dars/dars.lock b/dars/dars.lock index d465809a..d56a6724 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "677a5e3ba83c601b34fd9f42706d4c34c93e7721322aedc58985206dc25cdea1", - "size": 2565235, + "sha256": "79a3b0127be2a2a64899b1952367db3aa3f980a9ca72bfd61ac84dcc57c18f0c", + "size": 2565180, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From dac0764152ea7cc15b6a352c169db465922df200 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 02:20:31 -0400 Subject: [PATCH 11/50] Enforce stock class ratio conversion rights --- .../OpenCapTable/Types/Conversion.daml | 38 ++++++------------- .../TestConversionMechanisms.daml | 31 ++++++--------- Test/daml/OpenCapTable/TestStockClass.daml | 8 ++-- ...stStockClassConversionRightReferences.daml | 3 +- .../TestWarrantConversionRightReferences.daml | 17 +++++---- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 7 files changed, 42 insertions(+), 63 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 5fe62cdb..844581d9 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -7,6 +7,7 @@ module Fairmint.OpenCapTable.Types.Conversion where -- All conversion mechanism types for convertible securities, warrants, and stock. -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ +import DA.Optional (isNone) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) import Fairmint.OpenCapTable.Types.Validation (validateOcfPercentage) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) @@ -552,32 +553,17 @@ validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.converts_to_stock_class_id && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> - optional False validateOcfRatio conversionRight.ratio - OcfConversionMechanismPercentCapitalizationConversion -> - optional False (\p -> p > (0.0) && p <= (1.0 : Decimal)) conversionRight.percent_of_capitalization - OcfConversionMechanismFixedAmountConversion -> - optional False validateOcfMonetary conversionRight.conversion_price - OcfConversionMechanismValuationBasedConversion -> - optional False validateOcfMonetary conversionRight.reference_valuation_price_per_share - OcfConversionMechanismPpsBasedConversion -> - optional False validateOcfMonetary conversionRight.reference_share_price - OcfConversionMechanismSAFEConversion -> - ((case (conversionRight.discount_rate, conversionRight.valuation_cap) of - (Some d, _) -> d > (0.0) && d < (1.0 : Decimal) - (_, Some cap) -> validateOcfMonetary cap - _ -> False) && - optional True validateOcfMonetary conversionRight.floor_price_per_share && - optional True validateOcfMonetary conversionRight.ceiling_price_per_share) - OcfConversionMechanismNoteConversion -> - ((case (conversionRight.discount_rate, conversionRight.valuation_cap) of - (Some d, _) -> d > (0.0) && d < (1.0 : Decimal) - (_, Some cap) -> validateOcfMonetary cap - _ -> False) && - optional True validateOcfMonetary conversionRight.floor_price_per_share && - optional True validateOcfMonetary conversionRight.ceiling_price_per_share) - OcfConversionMechanismCustomConversion -> - optional False (/= "") conversionRight.custom_description) && - optional True validateOcfMonetary conversionRight.conversion_price + optional False validateOcfRatio conversionRight.ratio && + optional False validateOcfMonetary conversionRight.conversion_price && + isNone conversionRight.percent_of_capitalization && + isNone conversionRight.reference_share_price && + isNone conversionRight.reference_valuation_price_per_share && + isNone conversionRight.discount_rate && + isNone conversionRight.valuation_cap && + isNone conversionRight.custom_description && + isNone conversionRight.floor_price_per_share && + isNone conversionRight.ceiling_price_per_share + _ -> False) -- ============================================================================= -- CONVERSION TRIGGER diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index a1ea8057..6a33b762 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -233,26 +233,19 @@ stockClassRight mechanism = validStockClassRights : [OcfStockClassConversionRight] validStockClassRights = - [ stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None - , stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None - , stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None - , stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None - , stockClassRightWith OcfConversionMechanismPpsBasedConversion None None None (Some (usd 1.0)) None None None None - , stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None - , stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None - , stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion") - ] + [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None ] invalidStockClassRights : [(Text, OcfStockClassConversionRight)] invalidStockClassRights = - [ ("RATIO", stockClassRight OcfConversionMechanismRatioConversion) - , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 1.10) None None None None None None) - , ("FIXED", stockClassRight OcfConversionMechanismFixedAmountConversion) - , ("VALUATION", stockClassRight OcfConversionMechanismValuationBasedConversion) - , ("PPS", stockClassRight OcfConversionMechanismPpsBasedConversion) - , ("SAFE", stockClassRight OcfConversionMechanismSAFEConversion) - , ("NOTE", stockClassRight OcfConversionMechanismNoteConversion) - , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "")) + [ ("RATIO_MISSING_FIELDS", stockClassRight OcfConversionMechanismRatioConversion) + , ("RATIO_INVALID_PRICE", stockClassRightWith OcfConversionMechanismRatioConversion (Some invalidCurrency) None (Some validRatio) None None None None None) + , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None) + , ("FIXED", stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None) + , ("VALUATION", stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None) + , ("PPS", stockClassRightWith OcfConversionMechanismPpsBasedConversion None None None (Some (usd 1.0)) None None None None) + , ("SAFE", stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None) + , ("NOTE", stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None) + , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion")) ] convertibleRight : OcfConvertibleConversionMechanism -> OcfConvertibleConversionRight @@ -384,7 +377,7 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "any right convertible CUSTOM" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechCustom invalidCustomMechanism)))) assertValid "any right warrant CUSTOM" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")))) assertInvalid "any right warrant VALUATION" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismValuationBased with capitalization_definition = None, capitalization_definition_rules = None, valuation_amount = None, valuation_type = "valuation_cap")))) - assertValid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None))) + assertValid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None))) assertInvalid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRight OcfConversionMechanismRatioConversion))) testConversionMechanismRights_AllValidVariantsCreate = script do @@ -479,7 +472,7 @@ testConversionMechanismRights_InvalidVariantsReject = script do price_per_share = None liquidation_preference_multiple = None participation_cap_multiple = None - conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None] + conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None] comments = []) ] edits = [] diff --git a/Test/daml/OpenCapTable/TestStockClass.daml b/Test/daml/OpenCapTable/TestStockClass.daml index 677643ef..9356ab08 100644 --- a/Test/daml/OpenCapTable/TestStockClass.daml +++ b/Test/daml/OpenCapTable/TestStockClass.daml @@ -52,7 +52,7 @@ testCreateStockClass_OptionalsSome = script do let convRight = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = "SC_COMMON" @@ -91,8 +91,7 @@ testCreateStockClass_OptionalsSome = script do deletes = [] pure () --- Regression test for ENG-724: PPS_BASED_CONVERSION enum variant -testCreateStockClass_PpsBasedConversion = script do +testCreateStockClass_PpsBasedConversionFails = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp let convRight = OcfStockClassConversionRight with ceiling_price_per_share = None @@ -111,7 +110,7 @@ testCreateStockClass_PpsBasedConversion = script do reference_valuation_price_per_share = None type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None - _ <- submit issuer do + submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass commonStockClassData @@ -134,7 +133,6 @@ testCreateStockClass_PpsBasedConversion = script do ] edits = [] deletes = [] - pure () testCreateStockClass_PpsBasedConversionFailsWithoutReferencePrice = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml index b6e0186f..e4cbf19e 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -2,6 +2,7 @@ module OpenCapTable.TestStockClassConversionRightReferences where import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary import Fairmint.OpenCapTable.Types.Stock import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) @@ -13,7 +14,7 @@ stockClassConversionRightTo : Text -> OcfStockClassConversionRight stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = stockClassId diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index b6454029..dd76f51d 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -3,6 +3,7 @@ module OpenCapTable.TestWarrantConversionRightReferences where import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.Types.Conversion import Fairmint.OpenCapTable.Types.Monetary +import Fairmint.OpenCapTable.Types.Stock import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -44,17 +45,17 @@ stockClassConversionTriggerTo : Text -> OcfConversionTrigger stockClassConversionTriggerTo stockClassId = OcfConversionTrigger with conversion_right = OcfRightStockClass (OcfStockClassConversionRight with ceiling_price_per_share = None - conversion_mechanism = OcfConversionMechanismCustomConversion - conversion_price = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = warrantConversionTriggerTo None converts_to_future_round = None converts_to_stock_class_id = stockClassId - custom_description = Some "Custom" + custom_description = None discount_rate = None expires_at = None floor_price_per_share = None percent_of_capitalization = None - ratio = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) reference_share_price = None reference_valuation_price_per_share = None type_ = "STOCK_CLASS_CONVERSION_RIGHT" @@ -72,17 +73,17 @@ stockClassConversionTriggerWithNestedWarrantTarget : Text -> Optional Text -> Oc stockClassConversionTriggerWithNestedWarrantTarget stockClassId nestedStockClassId = OcfConversionTrigger with conversion_right = OcfRightStockClass (OcfStockClassConversionRight with ceiling_price_per_share = None - conversion_mechanism = OcfConversionMechanismCustomConversion - conversion_price = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = warrantConversionTriggerTo nestedStockClassId converts_to_future_round = None converts_to_stock_class_id = stockClassId - custom_description = Some "Custom" + custom_description = None discount_rate = None expires_at = None floor_price_per_share = None percent_of_capitalization = None - ratio = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) reference_share_price = None reference_valuation_price_per_share = None type_ = "STOCK_CLASS_CONVERSION_RIGHT" diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 732158c8..6bbe69ff 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79a3b0127be2a2a64899b1952367db3aa3f980a9ca72bfd61ac84dcc57c18f0c -size 2565180 +oid sha256:04058e35008e5d326428e371518a6bc35a2b91448fb7711eaffbefb5f3b062e4 +size 2562834 diff --git a/dars/dars.lock b/dars/dars.lock index d56a6724..c6b77be7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "79a3b0127be2a2a64899b1952367db3aa3f980a9ca72bfd61ac84dcc57c18f0c", - "size": 2565180, + "sha256": "04058e35008e5d326428e371518a6bc35a2b91448fb7711eaffbefb5f3b062e4", + "size": 2562834, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From 1a7bb5b0101a5dcd8f375ad04eadafa6b1868924 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 02:31:33 -0400 Subject: [PATCH 12/50] Enforce convertible conversion mechanism domain --- .../OpenCapTable/Types/Conversion.daml | 6 ++-- .../TestConversionMechanisms.daml | 29 +++++++++++++++---- .../0.0.5/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 844581d9..d0306712 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -472,9 +472,9 @@ validateOcfConvertibleConversionMechanism mechanism = OcfConvMechCustom m -> validateOcfCustomConversionMechanism m OcfConvMechPercentCapitalization m -> validateOcfPercentCapitalizationConversionMechanism m OcfConvMechFixedAmount m -> validateOcfFixedAmountConversionMechanism m - OcfConvMechPpsBased m -> validateOcfSharePriceBasedConversionMechanism m - OcfConvMechValuationBased m -> validateOcfValuationBasedConversionMechanism m - OcfConvMechRatio r -> validateOcfRatio r + OcfConvMechPpsBased _ -> False + OcfConvMechValuationBased _ -> False + OcfConvMechRatio _ -> False -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_rights/ConvertibleConversionRight.schema.json data OcfConvertibleConversionRight = OcfConvertibleConversionRight diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 6a33b762..ccb97081 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -141,9 +141,6 @@ validConvertibleMechanisms = , ("CUSTOM", OcfConvMechCustom validCustomMechanism) , ("PERCENT", OcfConvMechPercentCapitalization validPercentMechanism) , ("FIXED", OcfConvMechFixedAmount validFixedMechanism) - , ("PPS", OcfConvMechPpsBased validPpsMechanism) - , ("VALUATION", OcfConvMechValuationBased validValuationMechanism) - , ("RATIO", OcfConvMechRatio validRatio) ] invalidConvertibleMechanisms : [(Text, OcfConvertibleConversionMechanism)] @@ -153,9 +150,9 @@ invalidConvertibleMechanisms = , ("CUSTOM", OcfConvMechCustom invalidCustomMechanism) , ("PERCENT", OcfConvMechPercentCapitalization invalidPercentMechanism) , ("FIXED", OcfConvMechFixedAmount invalidFixedMechanism) - , ("PPS", OcfConvMechPpsBased invalidPpsMechanism) - , ("VALUATION", OcfConvMechValuationBased invalidValuationMechanism) - , ("RATIO", OcfConvMechRatio invalidRatio) + , ("PPS_UNSUPPORTED", OcfConvMechPpsBased validPpsMechanism) + , ("VALUATION_UNSUPPORTED", OcfConvMechValuationBased validValuationMechanism) + , ("RATIO_UNSUPPORTED", OcfConvMechRatio validRatio) ] validWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -517,6 +514,26 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with + id = "TX_UNSUPPORTED_CONVERTIBLE_CONVERSION_MECHANISM" + date = testTime + security_id = "CONV-UNSUPPORTED-MECHANISM" + custom_id = "CN-UNSUPPORTED" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + investment_amount = usd 100_000.0 + convertible_type = OcfConvertibleSecurity + conversion_triggers = [convertibleTrigger ("PPS-UNSUPPORTED", OcfConvMechPpsBased validPpsMechanism)] + pro_rata = None + seniority = 1 + comments = []] + edits = [] + deletes = [] submitMustFail issuer do exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 6bbe69ff..82b51ce1 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04058e35008e5d326428e371518a6bc35a2b91448fb7711eaffbefb5f3b062e4 -size 2562834 +oid sha256:e483b8cc90751f31889373d6c5160ce6a7056316609e1b1cc497cd248717c382 +size 2562643 diff --git a/dars/dars.lock b/dars/dars.lock index c6b77be7..93fd2f40 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "04058e35008e5d326428e371518a6bc35a2b91448fb7711eaffbefb5f3b062e4", - "size": 2562834, + "sha256": "e483b8cc90751f31889373d6c5160ce6a7056316609e1b1cc497cd248717c382", + "size": 2562643, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From e7291bdd66f1b35cb944af594f5e609183ff47be Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 02:40:57 -0400 Subject: [PATCH 13/50] Document unsupported convertible mechanisms --- .../daml/Fairmint/OpenCapTable/Types/Conversion.daml | 1 + dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index d0306712..83c66f00 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -472,6 +472,7 @@ validateOcfConvertibleConversionMechanism mechanism = OcfConvMechCustom m -> validateOcfCustomConversionMechanism m OcfConvMechPercentCapitalization m -> validateOcfPercentCapitalizationConversionMechanism m OcfConvMechFixedAmount m -> validateOcfFixedAmountConversionMechanism m + -- Kept for package upgrades; OCF does not support these for convertibles. OcfConvMechPpsBased _ -> False OcfConvMechValuationBased _ -> False OcfConvMechRatio _ -> False diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 82b51ce1..a4b5c1b0 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e483b8cc90751f31889373d6c5160ce6a7056316609e1b1cc497cd248717c382 -size 2562643 +oid sha256:f587957b8ebb0e504b55bede9c97a15a47012991a95cd0995a71391f78be0b88 +size 2562729 diff --git a/dars/dars.lock b/dars/dars.lock index 93fd2f40..3e431a4f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "e483b8cc90751f31889373d6c5160ce6a7056316609e1b1cc497cd248717c382", - "size": 2562643, + "sha256": "f587957b8ebb0e504b55bede9c97a15a47012991a95cd0995a71391f78be0b88", + "size": 2562729, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From 633c1473e2316d47a7283f3e9ab95ef991c04449 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 03:00:56 -0400 Subject: [PATCH 14/50] Enforce PPS discount oneOf --- .../OpenCapTable/Types/Conversion.daml | 4 +- .../TestConversionMechanisms.daml | 59 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 83c66f00..c47b8f73 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -240,8 +240,8 @@ validateOcfSharePriceBasedConversionMechanism m = (Some _, Some _) -> False else case (m.discount_percentage, m.discount_amount) of - (Some _, Some _) -> False - _ -> percentOk && amountOk + (None, None) -> True + _ -> False -- Enum - Valuation Based Formula Type -- Formula for valuation-based conversion diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index ccb97081..43b12c08 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -76,6 +76,13 @@ validPpsMechanism = OcfSharePriceBasedConversionMechanism with discount_amount = None discount_percentage = Some 0.20 +validPpsMechanismWithoutDiscount : OcfSharePriceBasedConversionMechanism +validPpsMechanismWithoutDiscount = validPpsMechanism with + description = "No discount to share price" + discount = False + discount_amount = None + discount_percentage = None + invalidPpsMechanism : OcfSharePriceBasedConversionMechanism invalidPpsMechanism = validPpsMechanism with discount_amount = Some (usd 1.0) @@ -85,6 +92,18 @@ invalidPpsMechanismDiscountDisabled = validPpsMechanism with discount_amount = Some (usd 1.0) discount_percentage = Some 0.20 +invalidPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None + +invalidPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with + discount = False + discount_amount = None + discount_percentage = Some 0.20 + validValuationMechanism : OcfValuationBasedConversionMechanism validValuationMechanism = OcfValuationBasedConversionMechanism with capitalization_definition = None @@ -193,6 +212,16 @@ invalidWarrantMechanisms = discount = False discount_amount = Some (usd 1.0) discount_percentage = Some 0.10) + , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Disabled discount with amount" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None) + , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Disabled discount with percentage" + discount = False + discount_amount = None + discount_percentage = Some 0.10) ] stockClassRightWith : @@ -353,6 +382,7 @@ testConversionMechanismValidators_AllVariants = script do assertValid "percent capitalization mechanism" (validateOcfPercentCapitalizationConversionMechanism validPercentMechanism) assertInvalid "percent capitalization mechanism" (validateOcfPercentCapitalizationConversionMechanism invalidPercentMechanism) assertValid "pps mechanism" (validateOcfSharePriceBasedConversionMechanism validPpsMechanism) + assertValid "pps mechanism without discount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismWithoutDiscount) assertInvalid "pps mechanism" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanism) assertValid "valuation mechanism" (validateOcfValuationBasedConversionMechanism validValuationMechanism) assertInvalid "valuation mechanism" (validateOcfValuationBasedConversionMechanism invalidValuationMechanism) @@ -364,6 +394,8 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "note mechanism" (validateOcfNoteConversionMechanism invalidNoteMechanism) assertInvalid "note mechanism invalid cap" (validateOcfNoteConversionMechanism invalidNoteMechanismCap) assertInvalid "pps mechanism disabled conflicting discounts" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabled) + assertInvalid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithAmount) + assertInvalid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithPercentage) assertValidConvertibleMechanisms validConvertibleMechanisms assertInvalidConvertibleMechanisms invalidConvertibleMechanisms assertValidWarrantMechanisms validWarrantMechanisms @@ -600,4 +632,31 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_PPS_DISABLED_DISCOUNT" + date = testTime + security_id = "WARRANT-PPS-DISABLED-DISCOUNT" + custom_id = "W-PPS-DISABLED" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [warrantTrigger ("PPS-DISABLED-DISCOUNT", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled but percentage present" + discount = False + discount_amount = None + discount_percentage = Some 0.10)] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] pure () diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index a4b5c1b0..f0c8dd2e 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f587957b8ebb0e504b55bede9c97a15a47012991a95cd0995a71391f78be0b88 -size 2562729 +oid sha256:22b1d979507c0b1d35bb856d6a4014972b76cc615ffa3168a1ed72fb7636fc94 +size 2562534 diff --git a/dars/dars.lock b/dars/dars.lock index 3e431a4f..e4e4cb04 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "f587957b8ebb0e504b55bede9c97a15a47012991a95cd0995a71391f78be0b88", - "size": 2562729, + "sha256": "22b1d979507c0b1d35bb856d6a4014972b76cc615ffa3168a1ed72fb7636fc94", + "size": 2562534, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From 440b3acd46e09f1edb1408a9e46447ac5a06ffae Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 03:19:15 -0400 Subject: [PATCH 15/50] Enforce valuation conversion oneOf --- .../OpenCapTable/Types/Conversion.daml | 32 ++++++-- .../TestConversionMechanisms.daml | 82 ++++++++++++++++++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 4 files changed, 107 insertions(+), 15 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index c47b8f73..2de93be1 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -248,6 +248,14 @@ validateOcfSharePriceBasedConversionMechanism m = data OcfValuationBasedFormulaType = OcfValuationCap | OcfValuationFixed | OcfValuationActual deriving (Eq, Show) +valuationBasedFormulaTypeFromText : Text -> Optional OcfValuationBasedFormulaType +valuationBasedFormulaTypeFromText valuationType = + case valuationType of + "CAP" -> Some OcfValuationCap + "FIXED" -> Some OcfValuationFixed + "ACTUAL" -> Some OcfValuationActual + _ -> None + -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_mechanisms/ValuationBasedConversionMechanism.schema.json data OcfValuationBasedConversionMechanism = OcfValuationBasedConversionMechanism with @@ -270,11 +278,12 @@ data OcfValuationBasedConversionMechanism = OcfValuationBasedConversionMechanism validateOcfValuationBasedConversionMechanism : OcfValuationBasedConversionMechanism -> Bool validateOcfValuationBasedConversionMechanism m = - case m.valuation_type of - OcfValuationActual -> True - OcfValuationCap -> optional False validateOcfMonetary m.valuation_amount - OcfValuationFixed -> optional False validateOcfMonetary m.valuation_amount - && validateOptionalText m.capitalization_definition + let amountOk = case m.valuation_type of + OcfValuationActual -> optional True validateOcfMonetary m.valuation_amount + OcfValuationCap -> optional False validateOcfMonetary m.valuation_amount + OcfValuationFixed -> optional False validateOcfMonetary m.valuation_amount + in amountOk && + validateOptionalText m.capitalization_definition -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_mechanisms/SAFEConversionMechanism.schema.json data OcfSAFEConversionMechanism = OcfSAFEConversionMechanism @@ -435,10 +444,15 @@ validateOcfWarrantConversionMechanism mechanism = validateOptionalText capitalization_definition OcfWarrantMechanismFixedAmount with converts_to_quantity -> converts_to_quantity > (0.0) - OcfWarrantMechanismValuationBased with valuation_type; valuation_amount; capitalization_definition -> - validateRequiredText valuation_type && - optional False validateOcfMonetary valuation_amount && - validateOptionalText capitalization_definition + OcfWarrantMechanismValuationBased with valuation_type; valuation_amount; capitalization_definition; capitalization_definition_rules -> + case valuationBasedFormulaTypeFromText valuation_type of + Some formulaType -> + validateOcfValuationBasedConversionMechanism OcfValuationBasedConversionMechanism with + valuation_type = formulaType + valuation_amount = valuation_amount + capitalization_definition = capitalization_definition + capitalization_definition_rules = capitalization_definition_rules + None -> False OcfWarrantMechanismPpsBased with description; discount; discount_percentage; discount_amount -> validateOcfSharePriceBasedConversionMechanism OcfSharePriceBasedConversionMechanism with description = description diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 43b12c08..28bbc519 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -111,9 +111,34 @@ validValuationMechanism = OcfValuationBasedConversionMechanism with valuation_amount = Some (usd 10_000_000.0) valuation_type = OcfValuationCap +validFixedValuationMechanism : OcfValuationBasedConversionMechanism +validFixedValuationMechanism = validValuationMechanism with + valuation_amount = Some (usd 5_000_000.0) + valuation_type = OcfValuationFixed + +validActualValuationMechanism : OcfValuationBasedConversionMechanism +validActualValuationMechanism = validValuationMechanism with + capitalization_definition = Some "fully-diluted" + valuation_amount = None + valuation_type = OcfValuationActual + +validActualValuationMechanismWithAmount : OcfValuationBasedConversionMechanism +validActualValuationMechanismWithAmount = validValuationMechanism with + valuation_amount = Some (usd 12_000_000.0) + valuation_type = OcfValuationActual + invalidValuationMechanism : OcfValuationBasedConversionMechanism invalidValuationMechanism = validValuationMechanism with valuation_amount = Some invalidCurrency +invalidValuationMechanismMissingAmount : OcfValuationBasedConversionMechanism +invalidValuationMechanismMissingAmount = validValuationMechanism with valuation_amount = None + +invalidActualValuationMechanismWithInvalidAmount : OcfValuationBasedConversionMechanism +invalidActualValuationMechanismWithInvalidAmount = validActualValuationMechanism with valuation_amount = Some invalidCurrency + +invalidActualValuationMechanismWithInvalidCapitalizationDefinition : OcfValuationBasedConversionMechanism +invalidActualValuationMechanismWithInvalidCapitalizationDefinition = validActualValuationMechanism with capitalization_definition = Some "" + validSafeMechanism : OcfSAFEConversionMechanism validSafeMechanism = OcfSAFEConversionMechanism with capitalization_definition = None @@ -186,7 +211,12 @@ validWarrantMechanisms = capitalization_definition = None capitalization_definition_rules = None valuation_amount = Some (usd 5_000_000.0) - valuation_type = "valuation_cap") + valuation_type = "CAP") + , ("VALUATION_ACTUAL", OcfWarrantMechanismValuationBased with + capitalization_definition = Some "fully-diluted" + capitalization_definition_rules = None + valuation_amount = None + valuation_type = "ACTUAL") , ("PPS", OcfWarrantMechanismPpsBased with description = "10% warrant discount" discount = True @@ -206,7 +236,22 @@ invalidWarrantMechanisms = capitalization_definition = None capitalization_definition_rules = None valuation_amount = None + valuation_type = "CAP") + , ("VALUATION_DOMAIN", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some (usd 1.0) valuation_type = "valuation_cap") + , ("VALUATION_ACTUAL_INVALID_AMOUNT", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some invalidCurrency + valuation_type = "ACTUAL") + , ("VALUATION_INVALID_CAPITALIZATION", OcfWarrantMechanismValuationBased with + capitalization_definition = Some "" + capitalization_definition_rules = None + valuation_amount = None + valuation_type = "ACTUAL") , ("PPS", OcfWarrantMechanismPpsBased with description = "Conflicting discounts" discount = False @@ -385,7 +430,13 @@ testConversionMechanismValidators_AllVariants = script do assertValid "pps mechanism without discount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismWithoutDiscount) assertInvalid "pps mechanism" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanism) assertValid "valuation mechanism" (validateOcfValuationBasedConversionMechanism validValuationMechanism) + assertValid "fixed valuation mechanism" (validateOcfValuationBasedConversionMechanism validFixedValuationMechanism) + assertValid "actual valuation mechanism" (validateOcfValuationBasedConversionMechanism validActualValuationMechanism) + assertValid "actual valuation mechanism with amount" (validateOcfValuationBasedConversionMechanism validActualValuationMechanismWithAmount) assertInvalid "valuation mechanism" (validateOcfValuationBasedConversionMechanism invalidValuationMechanism) + assertInvalid "valuation mechanism missing amount" (validateOcfValuationBasedConversionMechanism invalidValuationMechanismMissingAmount) + assertInvalid "actual valuation mechanism invalid amount" (validateOcfValuationBasedConversionMechanism invalidActualValuationMechanismWithInvalidAmount) + assertInvalid "actual valuation mechanism invalid capitalization" (validateOcfValuationBasedConversionMechanism invalidActualValuationMechanismWithInvalidCapitalizationDefinition) assertValid "SAFE mechanism" (validateOcfSAFEConversionMechanism validSafeMechanism) assertInvalid "SAFE mechanism" (validateOcfSAFEConversionMechanism invalidSafeMechanism) assertInvalid "SAFE mechanism invalid cap" (validateOcfSAFEConversionMechanism invalidSafeMechanismCap) @@ -405,7 +456,7 @@ testConversionMechanismValidators_AllVariants = script do assertValid "any right convertible SAFE" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechSAFE validSafeMechanism)))) assertInvalid "any right convertible CUSTOM" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechCustom invalidCustomMechanism)))) assertValid "any right warrant CUSTOM" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")))) - assertInvalid "any right warrant VALUATION" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismValuationBased with capitalization_definition = None, capitalization_definition_rules = None, valuation_amount = None, valuation_type = "valuation_cap")))) + assertInvalid "any right warrant VALUATION" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismValuationBased with capitalization_definition = None, capitalization_definition_rules = None, valuation_amount = None, valuation_type = "CAP")))) assertValid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None))) assertInvalid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRight OcfConversionMechanismRatioConversion))) @@ -659,4 +710,31 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_VALUATION_ACTUAL_AMOUNT" + date = testTime + security_id = "WARRANT-VALUATION-ACTUAL-INVALID" + custom_id = "W-VALUATION-ACTUAL" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [warrantTrigger ("VALUATION-ACTUAL-INVALID", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some invalidCurrency + valuation_type = "ACTUAL")] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] pure () diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index f0c8dd2e..90b3ec1b 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b1d979507c0b1d35bb856d6a4014972b76cc615ffa3168a1ed72fb7636fc94 -size 2562534 +oid sha256:df70697cf7204f7414bca68dd00e6378237f34e8e5a27cb686af89c51d189150 +size 2564253 diff --git a/dars/dars.lock b/dars/dars.lock index e4e4cb04..9dba08a9 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "22b1d979507c0b1d35bb856d6a4014972b76cc615ffa3168a1ed72fb7636fc94", - "size": 2562534, + "sha256": "df70697cf7204f7414bca68dd00e6378237f34e8e5a27cb686af89c51d189150", + "size": 2564253, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From a48f2e0c209c80b072742655e4cbe217937e650b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 07:18:35 -0400 Subject: [PATCH 16/50] Enforce conversion trigger variant fields --- .../OpenCapTable/OCF/ConvertibleIssuance.daml | 15 +- .../OpenCapTable/Types/Conversion.daml | 40 +++-- .../TestConversionMechanisms.daml | 161 +++++++++++++++++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 5 files changed, 195 insertions(+), 29 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml index 631cbe2f..bbd1d172 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, validateOcfSecurityExemption) -import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight) +import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight, validateOcfConversionTriggerFields) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -124,17 +124,8 @@ data OcfConvertibleConversionTrigger = OcfConvertibleConversionTrigger validateOcfConvertibleConversionTrigger : OcfConvertibleConversionTrigger -> Bool validateOcfConvertibleConversionTrigger t = - let hasDate = case t.trigger_date of Some _ -> True; None -> False - hasCond = case t.trigger_condition of Some c -> c /= ""; None -> False - in t.trigger_id /= "" && + t.trigger_id /= "" && validateOptionalText t.nickname && validateOptionalText t.trigger_description && - validateOptionalText t.trigger_condition && validateOcfConvertibleConversionRight t.conversion_right && - case t.type_ of - OcfTriggerTypeTypeAutomaticOnDate -> hasDate - OcfTriggerTypeTypeAutomaticOnCondition -> hasCond - OcfTriggerTypeTypeElectiveOnCondition -> hasCond - OcfTriggerTypeTypeElectiveInRange -> True - OcfTriggerTypeTypeElectiveAtWill -> True - OcfTriggerTypeTypeUnspecified -> True + validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index af0d8c46..92195699 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -7,7 +7,7 @@ module Fairmint.OpenCapTable.Types.Conversion where -- All conversion mechanism types for convertible securities, warrants, and stock. -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ -import DA.Optional (isNone) +import DA.Optional (isNone, isSome) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) import Fairmint.OpenCapTable.Types.Validation (validateOcfPercentage) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) @@ -599,6 +599,31 @@ validateOcfAnyConversionRight right = OcfRightWarrant warrantRight -> validateOcfWarrantConversionRight warrantRight OcfRightStockClass stockClassRight -> validateOcfStockClassConversionRight stockClassRight +-- Conversion trigger variants have additionalProperties: false in OCF; optional fields +-- must be present only on the variants whose schemas define them. +validateOcfConversionTriggerFields : OcfConversionTriggerType -> Optional Time -> Optional Time -> Optional Text -> Optional Time -> Bool +validateOcfConversionTriggerFields triggerType startDate endDate triggerCondition triggerDate = + let hasCondition = optional False validateRequiredText triggerCondition + noCondition = isNone triggerCondition + hasTriggerDate = isSome triggerDate + noTriggerDate = isNone triggerDate + hasStartDate = isSome startDate + hasEndDate = isSome endDate + noRange = isNone startDate && isNone endDate + in case triggerType of + OcfTriggerTypeTypeAutomaticOnDate -> + hasTriggerDate && noCondition && noRange + OcfTriggerTypeTypeAutomaticOnCondition -> + hasCondition && noTriggerDate && noRange + OcfTriggerTypeTypeElectiveOnCondition -> + hasCondition && noTriggerDate && noRange + OcfTriggerTypeTypeElectiveInRange -> + hasStartDate && hasEndDate && noCondition && noTriggerDate + OcfTriggerTypeTypeElectiveAtWill -> + noCondition && noTriggerDate && noRange + OcfTriggerTypeTypeUnspecified -> + noCondition && noTriggerDate && noRange + -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/primitives/types/conversion_triggers/ConversionTrigger.schema.json data OcfConversionTrigger = OcfConversionTrigger with @@ -631,17 +656,8 @@ data OcfConversionTrigger = OcfConversionTrigger validateOcfConversionTrigger : OcfConversionTrigger -> Bool validateOcfConversionTrigger t = - let hasDate = case t.trigger_date of Some _ -> True; None -> False - hasCond = optional False validateRequiredText t.trigger_condition - in validateRequiredText t.trigger_id && + validateRequiredText t.trigger_id && validateOptionalText t.nickname && validateOptionalText t.trigger_description && - validateOptionalText t.trigger_condition && validateOcfAnyConversionRight t.conversion_right && - case t.type_ of - OcfTriggerTypeTypeAutomaticOnDate -> hasDate - OcfTriggerTypeTypeAutomaticOnCondition -> hasCond - OcfTriggerTypeTypeElectiveOnCondition -> hasCond - OcfTriggerTypeTypeElectiveInRange -> True - OcfTriggerTypeTypeElectiveAtWill -> True - OcfTriggerTypeTypeUnspecified -> True + validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index b2d76634..1767c27b 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -5,7 +5,7 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script import qualified Fairmint.OpenCapTable.CapTable as CT -import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..), validateOcfConvertibleConversionTrigger) import Fairmint.OpenCapTable.OCF.StockClass (StockClassOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.Types.Conversion @@ -358,6 +358,84 @@ warrantTrigger (suffix, mechanism) = OcfConversionTrigger with trigger_id = "WARRANT-" <> suffix type_ = OcfTriggerTypeTypeElectiveAtWill +validConvertibleTriggers : [OcfConvertibleConversionTrigger] +validConvertibleTriggers = + [ convertibleTrigger ("AT-WILL", OcfConvMechCustom validCustomMechanism) + , (convertibleTrigger ("AUTO-DATE", OcfConvMechCustom validCustomMechanism)) with + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate + , (convertibleTrigger ("AUTO-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Qualified financing closes" + type_ = OcfTriggerTypeTypeAutomaticOnCondition + , (convertibleTrigger ("ELECTIVE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Holder elects after qualified financing" + type_ = OcfTriggerTypeTypeElectiveOnCondition + , (convertibleTrigger ("ELECTIVE-RANGE", OcfConvMechCustom validCustomMechanism)) with + start_date = Some testTime + end_date = Some testTime + type_ = OcfTriggerTypeTypeElectiveInRange + , (convertibleTrigger ("UNSPECIFIED", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeUnspecified + ] + +invalidConvertibleTriggers : [(Text, OcfConvertibleConversionTrigger)] +invalidConvertibleTriggers = + [ ("ELECTIVE_RANGE_MISSING_DATES", (convertibleTrigger ("RANGE-MISSING", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("ELECTIVE_RANGE_WITH_CONDITION", (convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + start_date = Some testTime + end_date = Some testTime + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("AT_WILL_WITH_DATE", (convertibleTrigger ("AT-WILL-DATE", OcfConvMechCustom validCustomMechanism)) with + trigger_date = Some testTime) + , ("AUTOMATIC_ON_DATE_WITH_CONDITION", (convertibleTrigger ("AUTO-DATE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Not allowed on date triggers" + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (convertibleTrigger ("AUTO-CONDITION-MISSING", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeAutomaticOnCondition) + ] + +validWarrantTriggers : [OcfConversionTrigger] +validWarrantTriggers = + [ warrantTrigger ("AT-WILL", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant") + , (warrantTrigger ("AUTO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate + , (warrantTrigger ("AUTO-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Qualified financing closes" + type_ = OcfTriggerTypeTypeAutomaticOnCondition + , (warrantTrigger ("ELECTIVE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Holder elects after qualified financing" + type_ = OcfTriggerTypeTypeElectiveOnCondition + , (warrantTrigger ("ELECTIVE-RANGE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + start_date = Some testTime + end_date = Some testTime + type_ = OcfTriggerTypeTypeElectiveInRange + , (warrantTrigger ("UNSPECIFIED", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeUnspecified + ] + +invalidWarrantTriggers : [(Text, OcfConversionTrigger)] +invalidWarrantTriggers = + [ ("ELECTIVE_RANGE_MISSING_DATES", (warrantTrigger ("RANGE-MISSING", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("ELECTIVE_RANGE_WITH_CONDITION", (warrantTrigger ("RANGE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + start_date = Some testTime + end_date = Some testTime + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("AT_WILL_WITH_DATE", (warrantTrigger ("AT-WILL-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime) + , ("AUTOMATIC_ON_DATE_WITH_CONDITION", (warrantTrigger ("AUTO-DATE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Not allowed on date triggers" + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (warrantTrigger ("AUTO-CONDITION-MISSING", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeAutomaticOnCondition) + ] + assertValid : Text -> Bool -> Script () assertValid label result = assertMsg (label <> " should validate") result @@ -418,6 +496,38 @@ assertInvalidStockClassRights rights = assertInvalid ("stock class " <> label) (validateOcfStockClassConversionRight right) assertInvalidStockClassRights rest +assertValidConvertibleTriggers : [OcfConvertibleConversionTrigger] -> Script () +assertValidConvertibleTriggers triggers = + case triggers of + [] -> pure () + trigger :: rest -> do + assertValid "convertible conversion trigger" (validateOcfConvertibleConversionTrigger trigger) + assertValidConvertibleTriggers rest + +assertInvalidConvertibleTriggers : [(Text, OcfConvertibleConversionTrigger)] -> Script () +assertInvalidConvertibleTriggers triggers = + case triggers of + [] -> pure () + (label, trigger) :: rest -> do + assertInvalid ("convertible trigger " <> label) (validateOcfConvertibleConversionTrigger trigger) + assertInvalidConvertibleTriggers rest + +assertValidWarrantTriggers : [OcfConversionTrigger] -> Script () +assertValidWarrantTriggers triggers = + case triggers of + [] -> pure () + trigger :: rest -> do + assertValid "warrant conversion trigger" (validateOcfConversionTrigger trigger) + assertValidWarrantTriggers rest + +assertInvalidWarrantTriggers : [(Text, OcfConversionTrigger)] -> Script () +assertInvalidWarrantTriggers triggers = + case triggers of + [] -> pure () + (label, trigger) :: rest -> do + assertInvalid ("warrant trigger " <> label) (validateOcfConversionTrigger trigger) + assertInvalidWarrantTriggers rest + testConversionMechanismValidators_AllVariants = script do assertValid "ratio mechanism" (validateOcfRatioConversionMechanism validRatioMechanism) assertInvalid "ratio mechanism" (validateOcfRatioConversionMechanism invalidRatioMechanism) @@ -454,6 +564,10 @@ testConversionMechanismValidators_AllVariants = script do assertInvalidWarrantMechanisms invalidWarrantMechanisms assertValidStockClassRights validStockClassRights assertInvalidStockClassRights invalidStockClassRights + assertValidConvertibleTriggers validConvertibleTriggers + assertInvalidConvertibleTriggers invalidConvertibleTriggers + assertValidWarrantTriggers validWarrantTriggers + assertInvalidWarrantTriggers invalidWarrantTriggers assertValid "any right convertible SAFE" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechSAFE validSafeMechanism)))) assertInvalid "any right convertible CUSTOM" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechCustom invalidCustomMechanism)))) assertValid "any right warrant CUSTOM" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")))) @@ -638,6 +752,27 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with + id = "TX_INVALID_CONVERTIBLE_TRIGGER_FIELDS" + date = testTime + security_id = "CONV-INVALID-TRIGGER-FIELDS" + custom_id = "CN-INVALID-TRIGGER" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + investment_amount = usd 100_000.0 + convertible_type = OcfConvertibleSecurity + conversion_triggers = [(convertibleTrigger ("RANGE-MISSING-DATES", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeElectiveInRange] + pro_rata = None + seniority = 1 + comments = []] + edits = [] + deletes = [] warrantControl <- submit issuer do exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with @@ -661,6 +796,30 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_TRIGGER_FIELDS" + date = testTime + security_id = "WARRANT-INVALID-TRIGGER-FIELDS" + custom_id = "W-INVALID-TRIGGER" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [(warrantTrigger ("AT-WILL-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] submitMustFail issuer do exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 198d0ce3..1f26d54f 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb9c42789381f6f0c42638a6c30cccd22f746031318b27116f1682e3b136bdbd -size 2620879 +oid sha256:8b1208e5c8bb88d66bdb75021344423a74858e35fc6244a1492317cffe074555 +size 2621993 diff --git a/dars/dars.lock b/dars/dars.lock index 900a241b..32c35f15 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "cb9c42789381f6f0c42638a6c30cccd22f746031318b27116f1682e3b136bdbd", - "size": 2620879, + "sha256": "8b1208e5c8bb88d66bdb75021344423a74858e35fc6244a1492317cffe074555", + "size": 2621993, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] From d45f3a19fd90a3e257ccf6e1a8f8ef0e99e5f392 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 07:33:46 -0400 Subject: [PATCH 17/50] Enforce currency code pattern --- OpenCapTable-v34/daml.yaml | 2 +- .../Fairmint/OpenCapTable/Types/Monetary.daml | 13 ++++++++++- Test/daml.yaml | 2 +- Test/daml/OpenCapTable/TestMonetary.daml | 23 +++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 3 +++ dars/dars.lock | 7 ++++++ 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestMonetary.daml create mode 100644 dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index f16d912a..70b5e670 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.5 +version: 0.0.6 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml index fc466e31..3f4f2e84 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml @@ -29,7 +29,18 @@ validateOcfMonetary : OcfMonetary -> Bool validateOcfMonetary monetary = monetary.amount >= (0.0) && validateRequiredText monetary.currency && - Text.length monetary.currency == 3 + validateOcfCurrencyCode monetary.currency + +validCurrencyCodeCharacters : [Text] +validCurrencyCodeCharacters = + [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" + , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" + ] + +validateOcfCurrencyCode : Text -> Bool +validateOcfCurrencyCode currency = + Text.length currency == 3 && + all (`elem` validCurrencyCodeCharacters) (Text.explode currency) -- Type - Tax ID -- Type representation of a tax identifier and issuing country diff --git a/Test/daml.yaml b/Test/daml.yaml index 64cd009c..0c90f894 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.5.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.6.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestMonetary.daml b/Test/daml/OpenCapTable/TestMonetary.daml new file mode 100644 index 00000000..c9064388 --- /dev/null +++ b/Test/daml/OpenCapTable/TestMonetary.daml @@ -0,0 +1,23 @@ +module OpenCapTable.TestMonetary where + +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..), validateOcfMonetary) +import Daml.Script + +assertMonetaryValid : Text -> OcfMonetary -> Script () +assertMonetaryValid label monetary = + assertMsg (label <> " should validate") (validateOcfMonetary monetary) + +assertMonetaryInvalid : Text -> OcfMonetary -> Script () +assertMonetaryInvalid label monetary = + assertMsg (label <> " should not validate") (not (validateOcfMonetary monetary)) + +monetary : Text -> OcfMonetary +monetary currency = OcfMonetary with amount = 1.0, currency = currency + +testMonetaryCurrencyCodePattern = script do + assertMonetaryValid "uppercase currency" (monetary "USD") + assertMonetaryInvalid "lowercase currency" (monetary "usd") + assertMonetaryInvalid "mixed-case currency" (monetary "UsD") + assertMonetaryInvalid "symbol currency" (monetary "U$D") + assertMonetaryInvalid "short currency" (monetary "US") + assertMonetaryInvalid "long currency" (monetary "USDD") diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar new file mode 100644 index 00000000..d1f431c7 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0cedb8d784a87b8877acdbbb277e37b87c142eff02a319778be84c9e42accd8 +size 2623698 diff --git a/dars/dars.lock b/dars/dars.lock index 32c35f15..075231e7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -38,6 +38,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T05:27:06.000Z", "networks": [] + }, + "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { + "sha256": "b0cedb8d784a87b8877acdbbb277e37b87c142eff02a319778be84c9e42accd8", + "size": 2623698, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T11:31:30.320Z", + "networks": [] } } } From 031377a1f94e629c384373b5e30f3624ed33666c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 07:47:47 -0400 Subject: [PATCH 18/50] Enforce OCF phone number pattern --- OpenCapTable-v34/daml.yaml | 2 +- .../Fairmint/OpenCapTable/Types/Contact.daml | 36 ++++++++++++++++++- Test/daml.yaml | 2 +- Test/daml/OpenCapTable/TestContact.daml | 30 ++++++++++++++++ Test/daml/OpenCapTable/TestIssuer.daml | 2 +- .../0.0.7/OpenCapTable-v34.dar | 3 ++ dars/dars.lock | 7 ++++ 7 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestContact.daml create mode 100644 dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 70b5e670..cdf810fc 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.6 +version: 0.0.7 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml index 08d67ec3..ecf85ac4 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml @@ -31,7 +31,41 @@ data OcfPhone = OcfPhone deriving (Eq, Show) validateOcfPhone : OcfPhone -> Bool -validateOcfPhone phone = validateRequiredText phone.phone_number +validateOcfPhone phone = validateOcfPhoneNumber phone.phone_number + +validDigitsBetween : Int -> Int -> Text -> Bool +validDigitsBetween minLength maxLength digits = + let n = Text.length digits in + n >= minLength && n <= maxLength && all Text.isDigit (Text.explode digits) + +validDigitsAtLeast : Int -> Text -> Bool +validDigitsAtLeast minLength digits = + Text.length digits >= minLength && all Text.isDigit (Text.explode digits) + +validPhoneCountryCode : Text -> Bool +validPhoneCountryCode country = + case Text.stripPrefix "+" country of + Some digits -> validDigitsBetween 1 3 digits + None -> False + +validateOcfPhoneNumberParts : [Text] -> Bool +validateOcfPhoneNumberParts parts = + case parts of + [country, area, exchange, line] -> + validPhoneCountryCode country && + validDigitsBetween 2 3 area && + validDigitsBetween 2 3 exchange && + validDigitsBetween 4 4 line + [country, area, exchange, line, extensionMarker, extension] -> + validateOcfPhoneNumberParts [country, area, exchange, line] && + (extensionMarker == "ext." || extensionMarker == "extension") && + validDigitsAtLeast 1 extension + _ -> False + +validateOcfPhoneNumber : Text -> Bool +validateOcfPhoneNumber phoneNumber = + validateRequiredText phoneNumber && + validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber) -- Enum - Email Type -- Type of e-mail address diff --git a/Test/daml.yaml b/Test/daml.yaml index 0c90f894..0a7b7174 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.6.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.7.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestContact.daml b/Test/daml/OpenCapTable/TestContact.daml new file mode 100644 index 00000000..4d803a80 --- /dev/null +++ b/Test/daml/OpenCapTable/TestContact.daml @@ -0,0 +1,30 @@ +module OpenCapTable.TestContact where + +import Fairmint.OpenCapTable.Types.Contact (OcfPhone(..), OcfPhoneType(..), validateOcfPhone) +import Daml.Script + +assertPhoneValid : Text -> Script () +assertPhoneValid phoneNumber = + assertMsg (phoneNumber <> " should validate") (validateOcfPhone (phone phoneNumber)) + +assertPhoneInvalid : Text -> Script () +assertPhoneInvalid phoneNumber = + assertMsg (phoneNumber <> " should not validate") (not (validateOcfPhone (phone phoneNumber))) + +phone : Text -> OcfPhone +phone phoneNumber = OcfPhone with phone_type = OcfPhoneBusiness, phone_number = phoneNumber + +testPhoneNumberPattern = script do + assertPhoneValid "+1 555 123 0100" + assertPhoneValid "+123 12 34 5678" + assertPhoneValid "+1 555 123 0100 ext. 100" + assertPhoneValid "+1 555 123 0100 extension 100" + assertPhoneInvalid "1 555 123 0100" + assertPhoneInvalid "+1234 555 123 0100" + assertPhoneInvalid "+1 5 123 0100" + assertPhoneInvalid "+1 555 1 0100" + assertPhoneInvalid "+1 555 123 010" + assertPhoneInvalid "+1 555 0100" + assertPhoneInvalid "+1 555 123 0100 ext 100" + assertPhoneInvalid "+1 555 123 0100 ext. 10A" + assertPhoneInvalid "+1 555 123 0100" diff --git a/Test/daml/OpenCapTable/TestIssuer.daml b/Test/daml/OpenCapTable/TestIssuer.daml index c087322f..ce411385 100644 --- a/Test/daml/OpenCapTable/TestIssuer.daml +++ b/Test/daml/OpenCapTable/TestIssuer.daml @@ -74,7 +74,7 @@ testCreateIssuerWithOptionalsSome = script do country_subdivision_name_of_formation = None tax_ids = [ OcfTaxID with tax_id = "12-3456789", country = "US" ] email = Some (OcfEmail with email_type = OcfEmailTypeBusiness, email_address = "info@acme.example") - phone = Some (OcfPhone with phone_type = OcfPhoneBusiness, phone_number = "+1 555 0100") + phone = Some (OcfPhone with phone_type = OcfPhoneBusiness, phone_number = "+1 555 123 0100") address = Some (OcfAddress with address_type = OcfAddressTypeLegal, street_suite = Some "1 Market St", city = Some "SF", country_subdivision = Some "CA", country = "US", postal_code = Some "94105") initial_shares_authorized = Some (OcfInitialSharesNumeric 2000000.0) comments = ["Seed stage"] diff --git a/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar new file mode 100644 index 00000000..b7bb9e2b --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64b5f71d1108bd5924fe86e0bd62f9bac301a476d4ee53ae7743b103224c44dd +size 2628793 diff --git a/dars/dars.lock b/dars/dars.lock index 075231e7..7a8dbed8 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -45,6 +45,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T11:31:30.320Z", "networks": [] + }, + "OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar": { + "sha256": "64b5f71d1108bd5924fe86e0bd62f9bac301a476d4ee53ae7743b103224c44dd", + "size": 2628793, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T11:46:17.671Z", + "networks": [] } } } From 245e68e1ae1eecf28335ddda9c6bd9908c3a3284 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 08:00:45 -0400 Subject: [PATCH 19/50] Enforce OCF country subdivision codes --- OpenCapTable-v34/daml.yaml | 2 +- .../Fairmint/OpenCapTable/OCF/Issuer.daml | 9 +- .../Fairmint/OpenCapTable/Types/Monetary.daml | 4 +- .../OpenCapTable/Types/Validation.daml | 15 ++++ Test/daml.yaml | 2 +- .../TestCountrySubdivisionCode.daml | 88 +++++++++++++++++++ .../0.0.8/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 ++ 8 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml create mode 100644 dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index cdf810fc..92c3b68e 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.7 +version: 0.0.8 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml index 690f5200..673ce160 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml @@ -6,12 +6,11 @@ module Fairmint.OpenCapTable.OCF.Issuer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/Issuer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateOcfCountrySubdivisionCode, validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Contact (OcfEmail, OcfPhone, validateOcfEmail, validateOcfPhone) import Fairmint.OpenCapTable.Types.Monetary (OcfAddress, OcfTaxID, validateOcfAddress, validateOcfTaxID) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) import Fairmint.Shared.TypeHelpers (validateOptionalText) -import qualified DA.Text as Text template Issuer with @@ -65,12 +64,6 @@ data IssuerOcfData = IssuerOcfData phone: Optional OcfPhone deriving (Eq, Show) -validateOcfCountrySubdivisionCode : Text -> Bool -validateOcfCountrySubdivisionCode subdivisionCode = - subdivisionCode /= "" && - Text.length subdivisionCode >= 1 && - Text.length subdivisionCode <= 3 - validateOcfLegalName : Text -> Bool validateOcfLegalName name = name /= "" diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml index 3f4f2e84..aa2d4884 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml @@ -9,7 +9,7 @@ module Fairmint.OpenCapTable.Types.Monetary where import qualified DA.Text as Text import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) -import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode) +import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateOcfCountrySubdivisionCode) -- Type - Monetary -- Type representation of a money amount and currency @@ -93,6 +93,6 @@ validateOcfAddress address = validateRequiredText address.country && validateOcfCountryCode address.country && validateOptionalText address.city && - validateOptionalText address.country_subdivision && + optional True validateOcfCountrySubdivisionCode address.country_subdivision && validateOptionalText address.postal_code && validateOptionalText address.street_suite diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index ef67ff9a..d9d05f40 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -59,6 +59,21 @@ validateOcfCountryCode : Text -> Bool validateOcfCountryCode countryCode = Text.length countryCode == 2 && countryCode `elem` validCountryCodes +-- Country subdivision code +-- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountrySubdivisionCode.schema.json +validCountrySubdivisionCodeCharacters : [Text] +validCountrySubdivisionCodeCharacters = + [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" + , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" + , "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" + ] + +validateOcfCountrySubdivisionCode : Text -> Bool +validateOcfCountrySubdivisionCode subdivisionCode = + Text.length subdivisionCode >= 1 && + Text.length subdivisionCode <= 3 && + all (`elem` validCountrySubdivisionCodeCharacters) (Text.explode subdivisionCode) + -- Percentage -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/Percentage.schema.json validateOcfPercentage : Decimal -> Bool diff --git a/Test/daml.yaml b/Test/daml.yaml index 0a7b7174..b173c021 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.7.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.8.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml new file mode 100644 index 00000000..66f45480 --- /dev/null +++ b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml @@ -0,0 +1,88 @@ +module OpenCapTable.TestCountrySubdivisionCode where + +import Fairmint.OpenCapTable.OcpFactory +import Fairmint.OpenCapTable.OCF.Issuer +import Fairmint.OpenCapTable.IssuerAuthorization +import Fairmint.OpenCapTable.Types.Monetary (OcfAddress(..), OcfAddressType(..), validateOcfAddress) +import Fairmint.OpenCapTable.Types.Validation (validateOcfCountrySubdivisionCode) +import OpenCapTable.Setup +import Daml.Script +import DA.Date +import DA.Time + +assertSubdivisionValid : Text -> Script () +assertSubdivisionValid subdivision = + assertMsg (subdivision <> " should validate") (validateOcfCountrySubdivisionCode subdivision) + +assertSubdivisionInvalid : Text -> Script () +assertSubdivisionInvalid subdivision = + assertMsg (subdivision <> " should not validate") (not (validateOcfCountrySubdivisionCode subdivision)) + +ocfAddress : Optional Text -> OcfAddress +ocfAddress countrySubdivision = OcfAddress with + address_type = OcfAddressTypeLegal + country = "US" + city = Some "San Francisco" + country_subdivision = countrySubdivision + postal_code = Some "94105" + street_suite = Some "1 Market St" + +issuerData : Optional Text -> Optional OcfAddress -> IssuerOcfData +issuerData formationSubdivision addressOpt = IssuerOcfData with + id = "ISSUER_SUBDIVISION" + legal_name = "Subdivision Test Inc" + formation_date = time (DA.Date.date 2010 Jun 1) 12 0 0 + country_of_formation = "US" + dba = None + country_subdivision_of_formation = formationSubdivision + country_subdivision_name_of_formation = None + tax_ids = [] + email = None + phone = None + address = addressOpt + initial_shares_authorized = None + comments = [] + +createFactory : Party -> Script (ContractId OcpFactory) +createFactory systemOperator = + submit systemOperator do + createCmd OcpFactory with + system_operator = systemOperator + +testCountrySubdivisionCodePattern = script do + assertSubdivisionValid "CA" + assertSubdivisionValid "C1" + assertSubdivisionValid "1" + assertSubdivisionValid "ABC" + assertSubdivisionInvalid "" + assertSubdivisionInvalid "CALI" + assertSubdivisionInvalid "ca" + assertSubdivisionInvalid "C-" + assertSubdivisionInvalid "C A" + +testAddressCountrySubdivisionCodePattern = script do + assertMsg "address without subdivision should validate" (validateOcfAddress (ocfAddress None)) + assertMsg "uppercase subdivision should validate" (validateOcfAddress (ocfAddress (Some "CA"))) + assertMsg "lowercase subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "ca")))) + assertMsg "symbol subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "C-")))) + assertMsg "long subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "CALI")))) + +testIssuerCountrySubdivisionCodePatternRejectsInvalid = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + submitMustFail issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = issuerData (Some "ca") None + pure () + +testIssuerAddressCountrySubdivisionCodePatternRejectsInvalid = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + submitMustFail issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = issuerData None (Some (ocfAddress (Some "C-"))) + pure () diff --git a/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar new file mode 100644 index 00000000..6a599098 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67553898061019b37717fc99a3d8d712c2392738e426e1b640d577896c4c1321 +size 2630155 diff --git a/dars/dars.lock b/dars/dars.lock index 7a8dbed8..356fc1d0 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -52,6 +52,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T11:46:17.671Z", "networks": [] + }, + "OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar": { + "sha256": "67553898061019b37717fc99a3d8d712c2392738e426e1b640d577896c4c1321", + "size": 2630155, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T11:59:30.767Z", + "networks": [] } } } From 1a071c4b5662b043f3baceafc51b25e96cb10331 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 08:13:36 -0400 Subject: [PATCH 20/50] Enforce stock issuance share number ranges --- OpenCapTable-v34/daml.yaml | 2 +- .../OpenCapTable/OCF/StockIssuance.daml | 5 +- .../Fairmint/OpenCapTable/Types/Stock.daml | 20 ++++++++ Test/daml.yaml | 2 +- Test/daml/OpenCapTable/TestStockIssuance.daml | 46 +++++++++++++++++++ .../0.0.9/OpenCapTable-v34.dar | 3 ++ dars/dars.lock | 7 +++ 7 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 92c3b68e..b19ca2f9 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.8 +version: 0.0.9 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml index 18a72c4a..a38d698f 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml @@ -4,7 +4,7 @@ module Fairmint.OpenCapTable.OCF.StockIssuance where import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) -import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, OcfShareNumberRange, validateOcfSecurityExemption, validateOcfShareNumberRange) +import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, OcfShareNumberRange, validateOcfSecurityExemption, validateOcfShareNumberRangesForQuantity) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -90,7 +90,7 @@ validateOcfStockIssuanceData d = d.stock_class_id /= "" && -- Arrays element validation (allow empty arrays where schema does not require minItems) all validateOcfSecurityExemption d.security_law_exemptions && - all validateOcfShareNumberRange d.share_numbers_issued && + validateOcfShareNumberRangesForQuantity d.quantity d.share_numbers_issued && all validateOcfVesting d.vestings && validateNonEmptyStringsArray d.comments && -- Arrays per schema: schema requires at least 1 legend, but we allow empty @@ -109,4 +109,3 @@ validateOcfStockIssuanceData d = data OcfStockIssuanceType = OcfStockIssuanceRSA | OcfStockIssuanceFounders deriving (Eq, Show) - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml index 8dc55283..42147141 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml @@ -81,6 +81,26 @@ data OcfShareNumberRange = OcfShareNumberRange validateOcfShareNumberRange : OcfShareNumberRange -> Bool validateOcfShareNumberRange r = r.starting_share_number > (0.0) && r.ending_share_number >= r.starting_share_number +shareNumberRangeQuantity : OcfShareNumberRange -> Decimal +shareNumberRangeQuantity r = r.ending_share_number - r.starting_share_number + 1.0 + +shareNumberRangesOverlap : OcfShareNumberRange -> OcfShareNumberRange -> Bool +shareNumberRangesOverlap a b = + a.starting_share_number <= b.ending_share_number && + b.starting_share_number <= a.ending_share_number + +hasOverlappingShareNumberRanges : [OcfShareNumberRange] -> Bool +hasOverlappingShareNumberRanges ranges = case ranges of + [] -> False + range :: rest -> any (shareNumberRangesOverlap range) rest || hasOverlappingShareNumberRanges rest + +validateOcfShareNumberRangesForQuantity : Decimal -> [OcfShareNumberRange] -> Bool +validateOcfShareNumberRangesForQuantity quantity ranges = + null ranges || + (all validateOcfShareNumberRange ranges && + not (hasOverlappingShareNumberRanges ranges) && + sum (map shareNumberRangeQuantity ranges) == quantity) + -- Enum - Authorized Shares -- Special values for initial_shares_authorized -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/Issuer.schema.json diff --git a/Test/daml.yaml b/Test/daml.yaml index b173c021..180de8ef 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.8.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.9.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index bcaec868..2e4a63a2 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -12,6 +12,11 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption(..), OcfShareNumberRange(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) +shareRange : Decimal -> Decimal -> OcfShareNumberRange +shareRange start end = OcfShareNumberRange with + starting_share_number = start + ending_share_number = end + -- Test creating a basic stock issuance using helpers testCreateStockIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -81,3 +86,44 @@ testStockIssuance_OptionalsSome = script do _ <- createStockIssuanceWith issuer referenceResult.updatedCapTableCid issuance pure () + +testStockIssuanceShareNumberRangesCanMatchQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_OK" "SEC-SHARE-NUMBERS-OK" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 5.0, shareRange 10.0 14.0] + + _ <- createStockIssuanceWith issuer cap_table' issuance + pure () + +testStockIssuanceShareNumberRangesMustMatchQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_SHORT" "SEC-SHARE-NUMBERS-SHORT" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 9.0] + + submitMustFail issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCannotOverlap = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_OVERLAP" "SEC-SHARE-NUMBERS-OVERLAP" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 5.0, shareRange 5.0 9.0] + + submitMustFail issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar new file mode 100644 index 00000000..9c4b23ad --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b84a021f8862ad8d3f40b5c44d6769f145dd94163226ce43bed5beed23427fdf +size 2633877 diff --git a/dars/dars.lock b/dars/dars.lock index 356fc1d0..05227dfa 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -59,6 +59,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T11:59:30.767Z", "networks": [] + }, + "OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar": { + "sha256": "b84a021f8862ad8d3f40b5c44d6769f145dd94163226ce43bed5beed23427fdf", + "size": 2633877, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T12:11:45.359Z", + "networks": [] } } } From c925f25280120abb48c8bef2d095471b31a9682f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 09:08:48 -0400 Subject: [PATCH 21/50] Address schema constraint review edge cases --- OpenCapTable-v34/daml.yaml | 2 +- .../OpenCapTable/Types/Conversion.daml | 1 + Test/daml.yaml | 2 +- .../TestConversionMechanisms.daml | 22 +++++++++ Test/daml/OpenCapTable/TestStockIssuance.daml | 42 +++++++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 3 ++ dars/dars.lock | 7 +++ .../codegen/templates/CapTable.daml.template | 47 ++++++++++++++++++- 8 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 1e0a6abb..4654c331 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.10 +version: 0.0.11 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 92195699..e072866c 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -566,6 +566,7 @@ validateOcfStockClassConversionRight : OcfStockClassConversionRight -> Bool validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.type_ && validateRequiredText conversionRight.converts_to_stock_class_id && + validateOcfConversionTrigger conversionRight.conversion_trigger && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> optional False validateOcfRatio conversionRight.ratio && diff --git a/Test/daml.yaml b/Test/daml.yaml index d55d3574..6d66d15f 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.10.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.11.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 1767c27b..18f3e39c 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -311,6 +311,7 @@ invalidStockClassRights = [ ("RATIO_MISSING_FIELDS", stockClassRight OcfConversionMechanismRatioConversion) , ("RATIO_INVALID_PRICE", stockClassRightWith OcfConversionMechanismRatioConversion (Some invalidCurrency) None (Some validRatio) None None None None None) , ("RATIO_EXPIRES_AT", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with expires_at = Some testTime) + , ("RATIO_INVALID_TRIGGER_FIELDS", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = (ocfTriggerElectiveAtWill with type_ = OcfTriggerTypeTypeElectiveInRange)) , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None) , ("FIXED", stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None) , ("VALUATION", stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None) @@ -692,6 +693,27 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd stockControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass StockClassOcfData with + id = "SC_INVALID_STOCK_CLASS_TRIGGER_FIELDS" + name = "Invalid Stock Class Trigger Fields" + class_type = OcfStockClassTypePreferred + default_id_prefix = "ISCT-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = [(stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with + conversion_trigger = (ocfTriggerElectiveAtWill with type_ = OcfTriggerTypeTypeElectiveInRange)] + comments = []] + edits = [] + deletes = [] convertibleControl <- submit issuer do exerciseCmd stockControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index df2f6abf..cc7a6bd9 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -127,3 +127,45 @@ testStockIssuanceShareNumberRangesCannotOverlap = script do edits = [] deletes = [] pure () + +testStockIssuanceShareNumberRangesCannotOverlapAcrossIssuances = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_FIRST" "SEC-SHARE-NUMBERS-FIRST" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_SECOND" "SEC-SHARE-NUMBERS-SECOND" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 5.0 14.0] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance secondIssuance] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCanRepeatAcrossStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + (cap_table'', secondClassId) <- setupStockClassWithId issuer cap_table' "SC_SHARE_NUMBER_OTHER" + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CLASS_ONE" "SEC-SHARE-NUMBERS-CLASS-ONE" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CLASS_TWO" "SEC-SHARE-NUMBERS-CLASS-TWO" stakeholderId secondClassId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar new file mode 100644 index 00000000..fb1200f2 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a986bf9e681b9c597eea7a3b63b25486becefdc6a2b7c947c3b55ee485c6931 +size 2650487 diff --git a/dars/dars.lock b/dars/dars.lock index d5ddeceb..51f14493 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -18,6 +18,13 @@ "uploadedAt": "2026-07-09T13:01:59.033Z", "networks": [] }, + "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { + "sha256": "5a986bf9e681b9c597eea7a3b63b25486becefdc6a2b7c947c3b55ee485c6931", + "size": 2650487, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T13:07:44.980Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index bab78019..89534cd9 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -19,7 +19,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangesOverlap) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) @@ -1175,6 +1175,50 @@ validateStockIssuanceReferences maps = validateOptionalReference "Stock issuance vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.stock_issuances) +data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with + stock_class_id: Text + issuance_id: Text + share_number_range: OcfShareNumberRange + deriving (Eq, Show) + +stockIssuanceShareNumberLots : CapTableMaps -> Update [StockIssuanceShareNumberLot] +stockIssuanceShareNumberLots maps = + foldlA + (\lots issuanceCid -> do + issuance <- fetch issuanceCid + let issuanceLots = + map + (\shareNumberRange -> StockIssuanceShareNumberLot with + stock_class_id = issuance.issuance_data.stock_class_id + issuance_id = issuance.issuance_data.id + share_number_range = shareNumberRange) + issuance.issuance_data.share_numbers_issued + pure (issuanceLots ++ lots)) + [] + (Map.values maps.stock_issuances) + +validateStockIssuanceShareNumberLotDoesNotOverlap : + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + Update () +validateStockIssuanceShareNumberLotDoesNotOverlap lot other = + when (lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do + assertMsg + ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) + False + +validateStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () +validateStockIssuanceShareNumberLotsDoNotOverlap lots = case lots of + [] -> pure () + lot :: rest -> do + mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap lot) rest + validateStockIssuanceShareNumberLotsDoNotOverlap rest + +validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () +validateStockIssuanceShareNumberRanges maps = do + lots <- stockIssuanceShareNumberLots maps + validateStockIssuanceShareNumberLotsDoNotOverlap lots + validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () validateEquityCompensationIssuanceReferences maps = mapA_ @@ -1524,6 +1568,7 @@ template CapTable validateValuationStockClassReferences finalMaps validateStockPlanReferences finalMaps validateStockIssuanceReferences finalMaps + validateStockIssuanceShareNumberRanges finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps From 540664d9f4316be82d88a11689fb4ee08366dc2d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:50:35 -0400 Subject: [PATCH 22/50] Address schema constraints review feedback --- .../OpenCapTable/OCF/ConvertibleTransfer.daml | 6 +- .../OCF/EquityCompensationExercise.daml | 5 +- .../OCF/EquityCompensationRelease.daml | 5 +- .../OCF/EquityCompensationTransfer.daml | 6 +- .../OpenCapTable/OCF/StockConsolidation.daml | 5 +- .../OpenCapTable/OCF/StockReissuance.daml | 5 +- .../OpenCapTable/OCF/StockTransfer.daml | 6 +- .../OpenCapTable/OCF/WarrantExercise.daml | 5 +- .../OpenCapTable/OCF/WarrantTransfer.daml | 6 +- .../Fairmint/OpenCapTable/Types/Contact.daml | 10 +++- .../OpenCapTable/Types/Conversion.daml | 6 +- .../Fairmint/OpenCapTable/Types/Monetary.daml | 10 +--- .../OpenCapTable/Types/Validation.daml | 26 ++++---- Test/daml/OpenCapTable/TestContact.daml | 2 + .../TestConversionMechanisms.daml | 60 ++++++++++--------- Test/daml/OpenCapTable/TestDocument.daml | 27 +++++++++ .../TestStockClassAuthorizedShares.daml | 2 +- .../TestUniqueSecurityArrays.daml | 48 +++++++++++++++ .../codegen/templates/CapTable.daml.template | 38 ++++++++---- 19 files changed, 188 insertions(+), 90 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml index 465c1c80..59847220 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/ConvertibleTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -64,9 +64,7 @@ validateConvertibleTransferOcfData d = -- Consistent with: StockTransfer (quantity > 0), WarrantTransfer (quantity > 0), -- EquityCompensationTransfer (quantity > 0), and ConvertibleCancellation (amount > 0) d.amount.amount > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index c11a4ba1..65058220 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,4 +61,5 @@ validateOcfEquityCompensationExerciseData d = validateOptionalText d.consideration_text && -- Arrays element validation (allow empty arrays where schema does not require minItems) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml index cdd5a21f..9aba976d 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationRelease where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,7 +62,6 @@ validateEquityCompensationReleaseOcfData d = d.security_id /= "" && d.quantity > 0.0 && validateOcfMonetary d.release_price && - not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml index 66e3a9be..bd0094c5 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/EquityCompensationTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateEquityCompensationTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml index 43dff6b0..eb39544b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConsolidation where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/consolidation/StockConsolidation.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,6 +61,5 @@ validateStockConsolidationOcfData d = validateOptionalText d.reason_text && -- Arrays element validation (security_ids required per Consolidation base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.security_ids && - validateUniqueStringsArray d.security_ids && + validateNonEmptyUniqueStringsArray d.security_ids && not (null d.security_ids) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml index 2904841c..4cb53366 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockReissuance where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/reissuance/StockReissuance.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,5 +62,4 @@ validateStockReissuanceOcfData d = validateOptionalText d.split_transaction_id && -- Arrays element validation (resulting_security_ids required per Reissuance base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateNonEmptyUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml index 6ad3bf7d..779a2984 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/StockTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateStockTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml index 03358e82..60e605c3 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantExercise where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/exercise/WarrantExercise.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -66,5 +66,4 @@ validateWarrantExerciseOcfData d = validateOptionalDecimal d.quantity && -- Arrays element validation (resulting_security_ids required per Exercise base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateNonEmptyUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml index 6c7b1c44..adc4b42f 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/WarrantTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateWarrantTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml index ecf85ac4..3400297e 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml @@ -60,12 +60,20 @@ validateOcfPhoneNumberParts parts = validateOcfPhoneNumberParts [country, area, exchange, line] && (extensionMarker == "ext." || extensionMarker == "extension") && validDigitsAtLeast 1 extension + -- Empty parts from consecutive spaces are intentionally rejected here. _ -> False +validateCompactE164PhoneNumber : Text -> Bool +validateCompactE164PhoneNumber phoneNumber = + case Text.stripPrefix "+" phoneNumber of + Some digits -> validDigitsBetween 2 15 digits + None -> False + validateOcfPhoneNumber : Text -> Bool validateOcfPhoneNumber phoneNumber = validateRequiredText phoneNumber && - validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber) + (validateCompactE164PhoneNumber phoneNumber || + validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber)) -- Enum - Email Type -- Type of e-mail address diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 08bbd3f2..58383481 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -240,8 +240,8 @@ validateOcfSharePriceBasedConversionMechanism m = (Some _, Some _) -> False else case (m.discount_percentage, m.discount_amount) of - (None, None) -> True - _ -> False + (Some _, Some _) -> False + _ -> percentOk && amountOk -- Enum - Valuation Based Formula Type -- Formula for valuation-based conversion @@ -620,7 +620,7 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio OcfTriggerTypeTypeElectiveOnCondition -> hasCondition && noTriggerDate && noRange OcfTriggerTypeTypeElectiveInRange -> - hasStartDate && hasEndDate && noCondition && noTriggerDate + (noRange || (hasStartDate && hasEndDate)) && noCondition && noTriggerDate OcfTriggerTypeTypeElectiveAtWill -> noCondition && noTriggerDate && noRange OcfTriggerTypeTypeUnspecified -> diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml index aa2d4884..68cf0c25 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml @@ -9,7 +9,7 @@ module Fairmint.OpenCapTable.Types.Monetary where import qualified DA.Text as Text import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) -import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateOcfCountrySubdivisionCode) +import Fairmint.OpenCapTable.Types.Validation (validUppercaseLetters, validateOcfCountryCode, validateOcfCountrySubdivisionCode) -- Type - Monetary -- Type representation of a money amount and currency @@ -31,16 +31,10 @@ validateOcfMonetary monetary = validateRequiredText monetary.currency && validateOcfCurrencyCode monetary.currency -validCurrencyCodeCharacters : [Text] -validCurrencyCodeCharacters = - [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" - , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" - ] - validateOcfCurrencyCode : Text -> Bool validateOcfCurrencyCode currency = Text.length currency == 3 && - all (`elem` validCurrencyCodeCharacters) (Text.explode currency) + all (`elem` validUppercaseLetters) (Text.explode currency) -- Type - Tax ID -- Type representation of a tax identifier and issuing country diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index d9d05f40..b857044b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -8,21 +8,29 @@ module Fairmint.OpenCapTable.Types.Validation where import qualified DA.Text as Text import qualified DA.Crypto.Text as CryptoText +import qualified DA.List as List import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Shared Text array validator -- Ensures no element is an empty string; empty arrays are allowed +validUppercaseLetters : [Text] +validUppercaseLetters = + [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" + , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" + ] + validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs -hasDuplicateTexts : [Text] -> Bool -hasDuplicateTexts texts = case texts of - [] -> False - text :: rest -> text `elem` rest || hasDuplicateTexts rest - -- Ensures no element appears more than once; empty arrays are allowed validateUniqueStringsArray : [Text] -> Bool -validateUniqueStringsArray xs = not (hasDuplicateTexts xs) +validateUniqueStringsArray = List.unique + +validateNonEmptyUniqueStringsArray : [Text] -> Bool +validateNonEmptyUniqueStringsArray xs = + not (null xs) && + validateNonEmptyStringsArray xs && + validateUniqueStringsArray xs -- Country code -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountryCode.schema.json @@ -63,10 +71,8 @@ validateOcfCountryCode countryCode = -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountrySubdivisionCode.schema.json validCountrySubdivisionCodeCharacters : [Text] validCountrySubdivisionCodeCharacters = - [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" - , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" - , "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" - ] + validUppercaseLetters ++ + [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ] validateOcfCountrySubdivisionCode : Text -> Bool validateOcfCountrySubdivisionCode subdivisionCode = diff --git a/Test/daml/OpenCapTable/TestContact.daml b/Test/daml/OpenCapTable/TestContact.daml index 4d803a80..7e636d8b 100644 --- a/Test/daml/OpenCapTable/TestContact.daml +++ b/Test/daml/OpenCapTable/TestContact.daml @@ -15,6 +15,8 @@ phone : Text -> OcfPhone phone phoneNumber = OcfPhone with phone_type = OcfPhoneBusiness, phone_number = phoneNumber testPhoneNumberPattern = script do + assertPhoneValid "+14155552671" + assertPhoneValid "+442071838750" assertPhoneValid "+1 555 123 0100" assertPhoneValid "+123 12 34 5678" assertPhoneValid "+1 555 123 0100 ext. 100" diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 4cd61137..2bd1e5aa 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -92,14 +92,14 @@ invalidPpsMechanismDiscountDisabled = validPpsMechanism with discount_amount = Some (usd 1.0) discount_percentage = Some 0.20 -invalidPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism -invalidPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with +validPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with discount = False discount_amount = Some (usd 1.0) discount_percentage = None -invalidPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism -invalidPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with +validPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with discount = False discount_amount = None discount_percentage = Some 0.20 @@ -222,6 +222,16 @@ validWarrantMechanisms = discount = True discount_amount = None discount_percentage = Some 0.10) + , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Disabled discount with amount" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None) + , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Disabled discount with percentage" + discount = False + discount_amount = None + discount_percentage = Some 0.10) ] invalidWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -257,16 +267,6 @@ invalidWarrantMechanisms = discount = False discount_amount = Some (usd 1.0) discount_percentage = Some 0.10) - , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with - description = "Disabled discount with amount" - discount = False - discount_amount = Some (usd 1.0) - discount_percentage = None) - , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with - description = "Disabled discount with percentage" - discount = False - discount_amount = None - discount_percentage = Some 0.10) ] stockClassRightWith : @@ -306,13 +306,18 @@ validStockClassRights : [OcfStockClassConversionRight] validStockClassRights = [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None ] +invalidRangeStockClassConversionTrigger : OcfConversionTrigger +invalidRangeStockClassConversionTrigger = ocfTriggerElectiveAtWill with + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange + invalidStockClassRights : [(Text, OcfStockClassConversionRight)] invalidStockClassRights = [ ("RATIO_MISSING_FIELDS", stockClassRight OcfConversionMechanismRatioConversion) , ("RATIO_INVALID_PRICE", stockClassRightWith OcfConversionMechanismRatioConversion (Some invalidCurrency) None (Some validRatio) None None None None None) , ("RATIO_EXPIRES_AT", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with expires_at = Some testTime) , ("RATIO_FUTURE_ROUND", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True) - , ("RATIO_INVALID_TRIGGER_FIELDS", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = (ocfTriggerElectiveAtWill with type_ = OcfTriggerTypeTypeElectiveInRange)) + , ("RATIO_INVALID_TRIGGER_FIELDS", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger) , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None) , ("FIXED", stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None) , ("VALUATION", stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None) @@ -376,15 +381,15 @@ validConvertibleTriggers = start_date = Some testTime end_date = Some testTime type_ = OcfTriggerTypeTypeElectiveInRange + , (convertibleTrigger ("ELECTIVE-RANGE-NO-DATES", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeElectiveInRange , (convertibleTrigger ("UNSPECIFIED", OcfConvMechCustom validCustomMechanism)) with type_ = OcfTriggerTypeTypeUnspecified ] invalidConvertibleTriggers : [(Text, OcfConvertibleConversionTrigger)] invalidConvertibleTriggers = - [ ("ELECTIVE_RANGE_MISSING_DATES", (convertibleTrigger ("RANGE-MISSING", OcfConvMechCustom validCustomMechanism)) with - type_ = OcfTriggerTypeTypeElectiveInRange) - , ("ELECTIVE_RANGE_WITH_CONDITION", (convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + [ ("ELECTIVE_RANGE_WITH_CONDITION", (convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with start_date = Some testTime end_date = Some testTime trigger_condition = Some "Not allowed on range triggers" @@ -415,15 +420,15 @@ validWarrantTriggers = start_date = Some testTime end_date = Some testTime type_ = OcfTriggerTypeTypeElectiveInRange + , (warrantTrigger ("ELECTIVE-RANGE-NO-DATES", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeElectiveInRange , (warrantTrigger ("UNSPECIFIED", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with type_ = OcfTriggerTypeTypeUnspecified ] invalidWarrantTriggers : [(Text, OcfConversionTrigger)] invalidWarrantTriggers = - [ ("ELECTIVE_RANGE_MISSING_DATES", (warrantTrigger ("RANGE-MISSING", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with - type_ = OcfTriggerTypeTypeElectiveInRange) - , ("ELECTIVE_RANGE_WITH_CONDITION", (warrantTrigger ("RANGE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + [ ("ELECTIVE_RANGE_WITH_CONDITION", (warrantTrigger ("RANGE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with start_date = Some testTime end_date = Some testTime trigger_condition = Some "Not allowed on range triggers" @@ -558,8 +563,8 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "note mechanism" (validateOcfNoteConversionMechanism invalidNoteMechanism) assertInvalid "note mechanism invalid cap" (validateOcfNoteConversionMechanism invalidNoteMechanismCap) assertInvalid "pps mechanism disabled conflicting discounts" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabled) - assertInvalid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithAmount) - assertInvalid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithPercentage) + assertValid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithAmount) + assertValid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithPercentage) assertValidConvertibleMechanisms validConvertibleMechanisms assertInvalidConvertibleMechanisms invalidConvertibleMechanisms assertValidWarrantMechanisms validWarrantMechanisms @@ -711,7 +716,7 @@ testConversionMechanismRights_InvalidVariantsReject = script do liquidation_preference_multiple = None participation_cap_multiple = None conversion_rights = [(stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with - conversion_trigger = (ocfTriggerElectiveAtWill with type_ = OcfTriggerTypeTypeElectiveInRange)] + conversion_trigger = invalidRangeStockClassConversionTrigger] comments = []] edits = [] deletes = [] @@ -789,7 +794,8 @@ testConversionMechanismRights_InvalidVariantsReject = script do security_law_exemptions = [] investment_amount = usd 100_000.0 convertible_type = OcfConvertibleSecurity - conversion_triggers = [(convertibleTrigger ("RANGE-MISSING-DATES", OcfConvMechCustom validCustomMechanism)) with + conversion_triggers = [(convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Not allowed on range triggers" type_ = OcfTriggerTypeTypeElectiveInRange] pro_rata = None seniority = 1 @@ -883,9 +889,9 @@ testConversionMechanismRights_InvalidVariantsReject = script do exercise_price = Some (usd 1.0) purchase_price = usd 0.0 exercise_triggers = [warrantTrigger ("PPS-DISABLED-DISCOUNT", OcfWarrantMechanismPpsBased with - description = "Discount flag disabled but percentage present" + description = "Discount flag disabled with conflicting discount fields" discount = False - discount_amount = None + discount_amount = Some (usd 1.0) discount_percentage = Some 0.10)] warrant_expiration_date = None vesting_terms_id = None diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 87c3107d..8eaf7918 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -83,6 +83,17 @@ testDocumentCreateWithPathAndUriFails = script do edits = [] deletes = [] +testDocumentCreateWithNeitherPathNorUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument ((documentData "DOC_NO_PATH_OR_URI" []) with + path = None + uri = None)] + edits = [] + deletes = [] + testEditDocument = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add document via batch @@ -127,6 +138,22 @@ testDocumentEditWithPathAndUriFails = script do uri = Some "https://cdn.example.com/docs/acme/charter.pdf")] deletes = [] +testDocumentEditWithNeitherPathNorUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_NO_PATH_URI" [])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditDocument ((documentData "DOC_EDIT_NO_PATH_URI" []) with + path = None + uri = None)] + deletes = [] + testDocumentRelatedIssuerReferenceMustExist = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 51a3e646..e6b1b0ea 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -672,7 +672,7 @@ testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do edits = [] deletes = [] -testStockConversionWithNonRatioRightFailsForCeiling = script do +testStockClassWithNonRatioConversionRightRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NON_RATIO_COMMON" 100.0) diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 566e1112..385455d5 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -1,9 +1,13 @@ module OpenCapTable.TestUniqueSecurityArrays where import Fairmint.OpenCapTable.OCF.ConvertibleTransfer +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockReissuance import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantExercise import Fairmint.OpenCapTable.OCF.WarrantTransfer import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.TestHelpers (defaultTestDate) @@ -69,3 +73,47 @@ testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = comments = [] security_ids = ["SSEC-1", "SSEC-1"] reason_text = None + +testUniqueSecurityArrays_ResultingSecurityValidatorsRejectDuplicateIds = script do + assertInvalid "warrant exercise" $ + validateWarrantExerciseOcfData WarrantExerciseOcfData with + id = "TX_WARRANT_EXERCISE_DUP" + date = defaultTestDate + security_id = "WSEC-EXERCISE" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + quantity = Some 1.0 + + assertInvalid "equity compensation exercise" $ + validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with + id = "TX_EC_EXERCISE_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-EXERCISE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "equity compensation release" $ + validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_DUP" + date = defaultTestDate + quantity = 1.0 + release_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = "ESEC-RELEASE" + settlement_date = defaultTestDate + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "stock reissuance" $ + validateStockReissuanceOcfData StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_DUP" + date = defaultTestDate + security_id = "SSEC-REISSUANCE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + reason_text = None + split_transaction_id = None diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index bf1c6fa4..8f762963 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -683,10 +683,6 @@ stockPlanReservedSharesAt adjustmentHistory stockPlanData asOf = Some adjustment -> adjustment.shares_reserved None -> stockPlanData.initial_shares_reserved -nonNegativeDecimal : Decimal -> Decimal -nonNegativeDecimal n = - if n < 0.0 then 0.0 else n - issuerAuthorizedLimitAt : Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> IssuerOcfData -> @@ -1276,17 +1272,39 @@ validateStockIssuanceShareNumberLotDoesNotOverlap : StockIssuanceShareNumberLot -> Update () validateStockIssuanceShareNumberLotDoesNotOverlap lot other = - when (lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do + when (lot.issuance_id /= other.issuance_id && lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do assertMsg ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) False +addStockIssuanceShareNumberLot : StockIssuanceShareNumberLot -> Map Text [StockIssuanceShareNumberLot] -> Map Text [StockIssuanceShareNumberLot] +addStockIssuanceShareNumberLot lot index = + Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index + +stockIssuanceShareNumberLotIndex : [StockIssuanceShareNumberLot] -> Map Text [StockIssuanceShareNumberLot] +stockIssuanceShareNumberLotIndex lots = + foldl (\index lot -> addStockIssuanceShareNumberLot lot index) Map.empty lots + +stockIssuanceShareNumberLotSortKey : StockIssuanceShareNumberLot -> (Decimal, Decimal, Text) +stockIssuanceShareNumberLotSortKey lot = + ( lot.share_number_range.starting_share_number + , lot.share_number_range.ending_share_number + , lot.issuance_id + ) + +validateSortedStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () +validateSortedStockIssuanceShareNumberLotsDoNotOverlap lots = case lots of + lot :: other :: rest -> do + validateStockIssuanceShareNumberLotDoesNotOverlap lot other + validateSortedStockIssuanceShareNumberLotsDoNotOverlap (other :: rest) + _ -> pure () + validateStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () -validateStockIssuanceShareNumberLotsDoNotOverlap lots = case lots of - [] -> pure () - lot :: rest -> do - mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap lot) rest - validateStockIssuanceShareNumberLotsDoNotOverlap rest +validateStockIssuanceShareNumberLotsDoNotOverlap lots = + mapA_ + (\stockClassLots -> + validateSortedStockIssuanceShareNumberLotsDoNotOverlap (sortOn stockIssuanceShareNumberLotSortKey stockClassLots)) + (Map.values (stockIssuanceShareNumberLotIndex lots)) validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () validateStockIssuanceShareNumberRanges maps = do From 707c79898b7832c43654a70e70d6dd2fe6cd7aa6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:59:12 -0400 Subject: [PATCH 23/50] Update schema constraints DAR backup --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index c73402d7..0b99516b 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90dbfa552bca0ecea7a36d2071397eeaf6583852fa875989051b021716c9aa2e -size 2681321 +oid sha256:49a5d07e9a14124fe33408b2b29fc7e9415487bce2425618f941a12fa0545026 +size 2685860 diff --git a/dars/dars.lock b/dars/dars.lock index c7fb70e6..480af67e 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "90dbfa552bca0ecea7a36d2071397eeaf6583852fa875989051b021716c9aa2e", - "size": 2681321, + "sha256": "49a5d07e9a14124fe33408b2b29fc7e9415487bce2425618f941a12fa0545026", + "size": 2685860, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:45:48.958Z", "networks": [] From 28728c5bdefdec7cbbefe45f83fa9e0e662f3bc0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:26:59 -0400 Subject: [PATCH 24/50] Handle inactive stock share number ranges --- Test/daml/OpenCapTable/TestStockIssuance.daml | 30 ++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 47 +++++++++++++------ 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index cc7a6bd9..a64bd078 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -1,6 +1,7 @@ module OpenCapTable.TestStockIssuance where import Fairmint.OpenCapTable.OCF.StockIssuance +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -148,6 +149,35 @@ testStockIssuanceShareNumberRangesCannotOverlapAcrossIssuances = script do deletes = [] pure () +testStockIssuanceShareNumberRangesCanReuseRetractedRange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RETRACTED" "SEC-SHARE-NUMBERS-RETRACTED" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let retraction = StockRetractionOcfData with + id = "TX_STOCK_SHARE_NUMBERS_RETRACT" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted stock issuance for replacement" + comments = [] + let replacementIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REPLACEMENT" "SEC-SHARE-NUMBERS-REPLACEMENT" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesCanRepeatAcrossStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 0b99516b..21c066b7 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a5d07e9a14124fe33408b2b29fc7e9415487bce2425618f941a12fa0545026 -size 2685860 +oid sha256:3cef7553599f6acb663af98494747a99eb454f73f7b556ca608569a777091540 +size 2693029 diff --git a/dars/dars.lock b/dars/dars.lock index 1ff4247a..9eed58f4 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "49a5d07e9a14124fe33408b2b29fc7e9415487bce2425618f941a12fa0545026", - "size": 2685860, + "sha256": "3cef7553599f6acb663af98494747a99eb454f73f7b556ca608569a777091540", + "size": 2693029, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:45:48.958Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f374a15b..275a587e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1251,19 +1251,28 @@ data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with share_number_range: OcfShareNumberRange deriving (Eq, Show) -stockIssuanceShareNumberLots : CapTableMaps -> Update [StockIssuanceShareNumberLot] -stockIssuanceShareNumberLots maps = +stockIssuanceShareNumberLots : StockClassShareEventIndex -> CapTableMaps -> Update [StockIssuanceShareNumberLot] +stockIssuanceShareNumberLots eventIndex maps = foldlA (\lots issuanceCid -> do issuance <- fetch issuanceCid - let issuanceLots = - map - (\shareNumberRange -> StockIssuanceShareNumberLot with - stock_class_id = issuance.issuance_data.stock_class_id - issuance_id = issuance.issuance_data.id - share_number_range = shareNumberRange) - issuance.issuance_data.share_numbers_issued - pure (issuanceLots ++ lots)) + let shareLot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotCurrentQuantity eventIndex shareLot of + Some currentQuantity -> + if currentQuantity > 0.0 + then do + let issuanceLots = + map + (\shareNumberRange -> StockIssuanceShareNumberLot with + stock_class_id = issuance.issuance_data.stock_class_id + issuance_id = issuance.issuance_data.id + share_number_range = shareNumberRange) + issuance.issuance_data.share_numbers_issued + pure (issuanceLots ++ lots) + else pure lots + None -> do + assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> issuance.issuance_data.id) False + pure lots) [] (Map.values maps.stock_issuances) @@ -1292,11 +1301,20 @@ stockIssuanceShareNumberLotSortKey lot = , lot.issuance_id ) +stockIssuanceShareNumberLotWithLaterEnd : + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot +stockIssuanceShareNumberLotWithLaterEnd active candidate = + if candidate.share_number_range.ending_share_number > active.share_number_range.ending_share_number + then candidate + else active + validateSortedStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () validateSortedStockIssuanceShareNumberLotsDoNotOverlap lots = case lots of - lot :: other :: rest -> do - validateStockIssuanceShareNumberLotDoesNotOverlap lot other - validateSortedStockIssuanceShareNumberLotsDoNotOverlap (other :: rest) + active :: candidate :: rest -> do + validateStockIssuanceShareNumberLotDoesNotOverlap active candidate + validateSortedStockIssuanceShareNumberLotsDoNotOverlap (stockIssuanceShareNumberLotWithLaterEnd active candidate :: rest) _ -> pure () validateStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () @@ -1308,7 +1326,8 @@ validateStockIssuanceShareNumberLotsDoNotOverlap lots = validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () validateStockIssuanceShareNumberRanges maps = do - lots <- stockIssuanceShareNumberLots maps + eventIndex <- stockClassShareEventIndex maps + lots <- stockIssuanceShareNumberLots eventIndex maps validateStockIssuanceShareNumberLotsDoNotOverlap lots validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () From 9b06f8f62076d89b7eb3fc9027b9852343b7ee80 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:42:37 -0400 Subject: [PATCH 25/50] Refresh schema constraints DAR after ceiling update --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 21c066b7..004a8940 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cef7553599f6acb663af98494747a99eb454f73f7b556ca608569a777091540 -size 2693029 +oid sha256:d0882b16bfea66cff9c0bce78c840a50b2ab1a772789b0c93b8a5fa5d6fab3b1 +size 2693448 diff --git a/dars/dars.lock b/dars/dars.lock index cd855236..4f3fc3a2 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "3cef7553599f6acb663af98494747a99eb454f73f7b556ca608569a777091540", - "size": 2693029, + "sha256": "d0882b16bfea66cff9c0bce78c840a50b2ab1a772789b0c93b8a5fa5d6fab3b1", + "size": 2693448, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:45:48.958Z", "networks": [] From ca1c6c99a3b1a2dd8cf252d873f59eebaea0fc7d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 12:59:27 -0400 Subject: [PATCH 26/50] Address schema constraint review feedback --- .../Fairmint/OpenCapTable/Types/Contact.daml | 9 +- .../OpenCapTable/Types/Conversion.daml | 4 +- .../Fairmint/OpenCapTable/Types/Stock.daml | 15 ++- Test/daml/OpenCapTable/TestContact.daml | 4 +- .../TestConversionMechanisms.daml | 36 ++--- Test/daml/OpenCapTable/TestStockIssuance.daml | 31 +++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 127 ++++++++++++------ 9 files changed, 148 insertions(+), 88 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml index 3400297e..557eb0e7 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml @@ -63,17 +63,10 @@ validateOcfPhoneNumberParts parts = -- Empty parts from consecutive spaces are intentionally rejected here. _ -> False -validateCompactE164PhoneNumber : Text -> Bool -validateCompactE164PhoneNumber phoneNumber = - case Text.stripPrefix "+" phoneNumber of - Some digits -> validDigitsBetween 2 15 digits - None -> False - validateOcfPhoneNumber : Text -> Bool validateOcfPhoneNumber phoneNumber = validateRequiredText phoneNumber && - (validateCompactE164PhoneNumber phoneNumber || - validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber)) + validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber) -- Enum - Email Type -- Type of e-mail address diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 58383481..06eeb790 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -566,7 +566,6 @@ validateOcfStockClassConversionRight : OcfStockClassConversionRight -> Bool validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.type_ && validateRequiredText conversionRight.converts_to_stock_class_id && - validateOcfConversionTrigger conversionRight.conversion_trigger && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> optional False validateOcfRatio conversionRight.ratio && @@ -579,8 +578,7 @@ validateOcfStockClassConversionRight conversionRight = isNone conversionRight.custom_description && isNone conversionRight.floor_price_per_share && isNone conversionRight.ceiling_price_per_share && - isNone conversionRight.expires_at && - isNone conversionRight.converts_to_future_round + isNone conversionRight.expires_at _ -> False) -- ============================================================================= diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml index 42147141..fa6f3389 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml @@ -7,6 +7,7 @@ module Fairmint.OpenCapTable.Types.Stock where -- Stock-related types and enums. -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ +import DA.List (sortOn) import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Enum - Stock Class Type @@ -89,10 +90,18 @@ shareNumberRangesOverlap a b = a.starting_share_number <= b.ending_share_number && b.starting_share_number <= a.ending_share_number +shareNumberRangeSortKey : OcfShareNumberRange -> (Decimal, Decimal) +shareNumberRangeSortKey r = (r.starting_share_number, r.ending_share_number) + +hasOverlappingSortedShareNumberRanges : [OcfShareNumberRange] -> Bool +hasOverlappingSortedShareNumberRanges ranges = case ranges of + first :: second :: rest -> + shareNumberRangesOverlap first second || hasOverlappingSortedShareNumberRanges (second :: rest) + _ -> False + hasOverlappingShareNumberRanges : [OcfShareNumberRange] -> Bool -hasOverlappingShareNumberRanges ranges = case ranges of - [] -> False - range :: rest -> any (shareNumberRangesOverlap range) rest || hasOverlappingShareNumberRanges rest +hasOverlappingShareNumberRanges ranges = + hasOverlappingSortedShareNumberRanges (sortOn shareNumberRangeSortKey ranges) validateOcfShareNumberRangesForQuantity : Decimal -> [OcfShareNumberRange] -> Bool validateOcfShareNumberRangesForQuantity quantity ranges = diff --git a/Test/daml/OpenCapTable/TestContact.daml b/Test/daml/OpenCapTable/TestContact.daml index 7e636d8b..c029daf2 100644 --- a/Test/daml/OpenCapTable/TestContact.daml +++ b/Test/daml/OpenCapTable/TestContact.daml @@ -15,12 +15,12 @@ phone : Text -> OcfPhone phone phoneNumber = OcfPhone with phone_type = OcfPhoneBusiness, phone_number = phoneNumber testPhoneNumberPattern = script do - assertPhoneValid "+14155552671" - assertPhoneValid "+442071838750" assertPhoneValid "+1 555 123 0100" assertPhoneValid "+123 12 34 5678" assertPhoneValid "+1 555 123 0100 ext. 100" assertPhoneValid "+1 555 123 0100 extension 100" + assertPhoneInvalid "+14155552671" + assertPhoneInvalid "+442071838750" assertPhoneInvalid "1 555 123 0100" assertPhoneInvalid "+1234 555 123 0100" assertPhoneInvalid "+1 5 123 0100" diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 2bd1e5aa..8e85b105 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -302,22 +302,23 @@ stockClassRight : OcfConversionMechanism -> OcfStockClassConversionRight stockClassRight mechanism = stockClassRightWith mechanism None None None None None None None None -validStockClassRights : [OcfStockClassConversionRight] -validStockClassRights = - [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None ] - invalidRangeStockClassConversionTrigger : OcfConversionTrigger invalidRangeStockClassConversionTrigger = ocfTriggerElectiveAtWill with - trigger_condition = Some "Not allowed on range triggers" + trigger_condition = Some "Ignored for stock-class conversion rights" type_ = OcfTriggerTypeTypeElectiveInRange +validStockClassRights : [OcfStockClassConversionRight] +validStockClassRights = + [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None + , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True + , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger + ] + invalidStockClassRights : [(Text, OcfStockClassConversionRight)] invalidStockClassRights = [ ("RATIO_MISSING_FIELDS", stockClassRight OcfConversionMechanismRatioConversion) , ("RATIO_INVALID_PRICE", stockClassRightWith OcfConversionMechanismRatioConversion (Some invalidCurrency) None (Some validRatio) None None None None None) , ("RATIO_EXPIRES_AT", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with expires_at = Some testTime) - , ("RATIO_FUTURE_ROUND", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True) - , ("RATIO_INVALID_TRIGGER_FIELDS", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger) , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None) , ("FIXED", stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None) , ("VALUATION", stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None) @@ -699,27 +700,6 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] - submitMustFail issuer do - exerciseCmd stockControl.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_INVALID_STOCK_CLASS_TRIGGER_FIELDS" - name = "Invalid Stock Class Trigger Fields" - class_type = OcfStockClassTypePreferred - default_id_prefix = "ISCT-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = [(stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with - conversion_trigger = invalidRangeStockClassConversionTrigger] - comments = []] - edits = [] - deletes = [] convertibleControl <- submit issuer do exerciseCmd stockControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index a64bd078..7601a135 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -178,6 +178,37 @@ testStockIssuanceShareNumberRangesCanReuseRetractedRange = script do deletes = [] pure () +testStockIssuanceShareNumberRangesCannotOverlapBeforeRetraction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_HISTORICAL_FIRST" "SEC-SHARE-NUMBERS-HISTORICAL-FIRST" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_HISTORICAL_SECOND" "SEC-SHARE-NUMBERS-HISTORICAL-SECOND" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 5.0 14.0] + let laterRetraction = StockRetractionOcfData with + id = "TX_STOCK_SHARE_NUMBERS_HISTORICAL_RETRACT" + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + security_id = firstIssuance.security_id + reason_text = "Retracted after the overlapping issuance date" + comments = [] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance secondIssuance + , CT.OcfCreateStockRetraction laterRetraction + ] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesCanRepeatAcrossStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 004a8940..bd5121ee 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0882b16bfea66cff9c0bce78c840a50b2ab1a772789b0c93b8a5fa5d6fab3b1 -size 2693448 +oid sha256:83c4de67d3bc7be307c31fc39e1deb4a1e74578dfe3f6df5c295f17e672d6df3 +size 2741493 diff --git a/dars/dars.lock b/dars/dars.lock index cb4794e2..bc0835c4 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "d0882b16bfea66cff9c0bce78c840a50b2ab1a772789b0c93b8a5fa5d6fab3b1", - "size": 2693448, + "sha256": "83c4de67d3bc7be307c31fc39e1deb4a1e74578dfe3f6df5c295f17e672d6df3", + "size": 2741493, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T13:45:48.958Z", + "uploadedAt": "2026-07-09T16:58:54Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 210447f7..672e671e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -699,6 +699,12 @@ stockClassShareEventSortKey event = case event of StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority) StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority) +stockClassShareEventDate : StockClassShareEvent -> Time +stockClassShareEventDate event = case event of + StockClassShareDelta with event_date -> event_date + StockClassShareMultiplier with event_date -> event_date + StockClassShareRetraction with event_date -> event_date + stockClassShareEventOnOrAfter : Time -> StockClassShareEvent -> Bool stockClassShareEventOnOrAfter earliest event = case event of StockClassShareDelta with event_date -> event_date >= earliest @@ -1248,6 +1254,7 @@ validateStockIssuanceReferences maps = data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with stock_class_id: Text issuance_id: Text + share_lot: StockClassShareLot share_number_range: OcfShareNumberRange deriving (Eq, Show) @@ -1258,18 +1265,16 @@ stockIssuanceShareNumberLots eventIndex maps = issuance <- fetch issuanceCid let shareLot = stockIssuanceShareLot issuance.issuance_data case stockClassShareLotCurrentQuantity eventIndex shareLot of - Some currentQuantity -> - if currentQuantity > 0.0 - then do - let issuanceLots = - map - (\shareNumberRange -> StockIssuanceShareNumberLot with - stock_class_id = issuance.issuance_data.stock_class_id - issuance_id = issuance.issuance_data.id - share_number_range = shareNumberRange) - issuance.issuance_data.share_numbers_issued - pure (issuanceLots ++ lots) - else pure lots + Some _ -> do + let issuanceLots = + map + (\shareNumberRange -> StockIssuanceShareNumberLot with + stock_class_id = issuance.issuance_data.stock_class_id + issuance_id = issuance.issuance_data.id + share_lot = shareLot + share_number_range = shareNumberRange) + issuance.issuance_data.share_numbers_issued + pure (issuanceLots ++ lots) None -> do assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> issuance.issuance_data.id) False pure lots) @@ -1277,14 +1282,17 @@ stockIssuanceShareNumberLots eventIndex maps = (Map.values maps.stock_issuances) validateStockIssuanceShareNumberLotDoesNotOverlap : + StockClassShareEventIndex -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> Update () -validateStockIssuanceShareNumberLotDoesNotOverlap lot other = +validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot other = when (lot.issuance_id /= other.issuance_id && lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do - assertMsg - ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) - False + overlapsWhileActive <- stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other + when overlapsWhileActive do + assertMsg + ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) + False addStockIssuanceShareNumberLot : StockIssuanceShareNumberLot -> Map Text [StockIssuanceShareNumberLot] -> Map Text [StockIssuanceShareNumberLot] addStockIssuanceShareNumberLot lot index = @@ -1294,41 +1302,82 @@ stockIssuanceShareNumberLotIndex : [StockIssuanceShareNumberLot] -> Map Text [St stockIssuanceShareNumberLotIndex lots = foldl (\index lot -> addStockIssuanceShareNumberLot lot index) Map.empty lots -stockIssuanceShareNumberLotSortKey : StockIssuanceShareNumberLot -> (Decimal, Decimal, Text) -stockIssuanceShareNumberLotSortKey lot = - ( lot.share_number_range.starting_share_number - , lot.share_number_range.ending_share_number - , lot.issuance_id - ) +stockIssuanceShareNumberLotRelevantDates : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + [Time] +stockIssuanceShareNumberLotRelevantDates eventIndex lot other = + map + stockClassShareEventDate + (stockClassShareLotEvents eventIndex lot.share_lot ++ stockClassShareLotEvents eventIndex other.share_lot) + +stockIssuanceShareNumberLotActiveAt : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + Time -> + Optional Bool +stockIssuanceShareNumberLotActiveAt eventIndex lot asOf = + case stockClassShareLotQuantityAt eventIndex lot.share_lot asOf of + Some quantity -> Some (quantity > 0.0) + None -> None -stockIssuanceShareNumberLotWithLaterEnd : +stockIssuanceShareNumberLotsOverlapOnDate : + StockClassShareEventIndex -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> - StockIssuanceShareNumberLot -stockIssuanceShareNumberLotWithLaterEnd active candidate = - if candidate.share_number_range.ending_share_number > active.share_number_range.ending_share_number - then candidate - else active - -validateSortedStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () -validateSortedStockIssuanceShareNumberLotsDoNotOverlap lots = case lots of - active :: candidate :: rest -> do - validateStockIssuanceShareNumberLotDoesNotOverlap active candidate - validateSortedStockIssuanceShareNumberLotsDoNotOverlap (stockIssuanceShareNumberLotWithLaterEnd active candidate :: rest) + Time -> + Update Bool +stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf = + case (stockIssuanceShareNumberLotActiveAt eventIndex lot asOf, stockIssuanceShareNumberLotActiveAt eventIndex other asOf) of + (Some lotActive, Some otherActive) -> pure (lotActive && otherActive) + _ -> do + assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> lot.issuance_id <> " or " <> other.issuance_id) False + pure False + +stockIssuanceShareNumberLotsOverlapWhileActive : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + Update Bool +stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other = + foldlA + (\overlaps asOf -> + if overlaps + then pure True + else stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf) + False + (stockIssuanceShareNumberLotRelevantDates eventIndex lot other) + +validateStockIssuanceShareNumberLotAgainstRest : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + [StockIssuanceShareNumberLot] -> + Update () +validateStockIssuanceShareNumberLotAgainstRest eventIndex lot lots = + mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot) lots + +validateStockClassShareNumberLotsDoNotOverlap : + StockClassShareEventIndex -> + [StockIssuanceShareNumberLot] -> + Update () +validateStockClassShareNumberLotsDoNotOverlap eventIndex lots = case lots of + lot :: rest -> do + validateStockIssuanceShareNumberLotAgainstRest eventIndex lot rest + validateStockClassShareNumberLotsDoNotOverlap eventIndex rest _ -> pure () -validateStockIssuanceShareNumberLotsDoNotOverlap : [StockIssuanceShareNumberLot] -> Update () -validateStockIssuanceShareNumberLotsDoNotOverlap lots = +validateStockIssuanceShareNumberLotsDoNotOverlap : StockClassShareEventIndex -> [StockIssuanceShareNumberLot] -> Update () +validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots = mapA_ - (\stockClassLots -> - validateSortedStockIssuanceShareNumberLotsDoNotOverlap (sortOn stockIssuanceShareNumberLotSortKey stockClassLots)) + (validateStockClassShareNumberLotsDoNotOverlap eventIndex) (Map.values (stockIssuanceShareNumberLotIndex lots)) validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () validateStockIssuanceShareNumberRanges maps = do eventIndex <- stockClassShareEventIndex maps lots <- stockIssuanceShareNumberLots eventIndex maps - validateStockIssuanceShareNumberLotsDoNotOverlap lots + validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () validateEquityCompensationIssuanceReferences maps = From cd3581a08e9e7c8051cb1742c24c46adacd0d8b6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:47:07 -0400 Subject: [PATCH 27/50] Refresh schema constraints after reference update --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index bd5121ee..6bf5f57f 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83c4de67d3bc7be307c31fc39e1deb4a1e74578dfe3f6df5c295f17e672d6df3 -size 2741493 +oid sha256:06ebe2f73047e64a51e14720d8c05656346c01601ef41367a50a4f8abce933f3 +size 2763314 diff --git a/dars/dars.lock b/dars/dars.lock index 85a00037..8bcf3f76 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "83c4de67d3bc7be307c31fc39e1deb4a1e74578dfe3f6df5c295f17e672d6df3", - "size": 2741493, + "sha256": "06ebe2f73047e64a51e14720d8c05656346c01601ef41367a50a4f8abce933f3", + "size": 2763314, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T16:58:54Z", + "uploadedAt": "2026-07-09T17:46:05Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From bb5b091c405c3a52e3a6008cd0a173ceb95866d9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:03:24 -0400 Subject: [PATCH 28/50] Address schema constraint review feedback --- .../OCF/EquityCompensationExercise.daml | 7 +++---- .../Fairmint/OpenCapTable/Types/Conversion.daml | 16 ++++++++++------ .../OpenCapTable/TestConversionMechanisms.daml | 6 +++++- .../OpenCapTable/TestUniqueSecurityArrays.daml | 12 +++++++++++- .../OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index 65058220..8ec8b286 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,7 +59,6 @@ validateOcfEquityCompensationExerciseData d = -- Required numbers may be zero unless schema specifies otherwise -- Optional Text values, if present, must be non-empty validateOptionalText d.consideration_text && - -- Arrays element validation (allow empty arrays where schema does not require minItems) + -- Arrays element validation validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids + validateNonEmptyUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 06eeb790..8035d1a8 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -565,6 +565,7 @@ data OcfStockClassConversionRight = OcfStockClassConversionRight validateOcfStockClassConversionRight : OcfStockClassConversionRight -> Bool validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.type_ && + validateOcfConversionTriggerShape conversionRight.conversion_trigger && validateRequiredText conversionRight.converts_to_stock_class_id && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> @@ -605,14 +606,13 @@ validateOcfConversionTriggerFields : OcfConversionTriggerType -> Optional Time - validateOcfConversionTriggerFields triggerType startDate endDate triggerCondition triggerDate = let hasCondition = optional False validateRequiredText triggerCondition noCondition = isNone triggerCondition - hasTriggerDate = isSome triggerDate noTriggerDate = isNone triggerDate hasStartDate = isSome startDate hasEndDate = isSome endDate noRange = isNone startDate && isNone endDate in case triggerType of OcfTriggerTypeTypeAutomaticOnDate -> - hasTriggerDate && noCondition && noRange + noCondition && noRange OcfTriggerTypeTypeAutomaticOnCondition -> hasCondition && noTriggerDate && noRange OcfTriggerTypeTypeElectiveOnCondition -> @@ -656,8 +656,12 @@ data OcfConversionTrigger = OcfConversionTrigger validateOcfConversionTrigger : OcfConversionTrigger -> Bool validateOcfConversionTrigger t = + validateOcfConversionTriggerShape t && + validateOcfAnyConversionRight t.conversion_right + +validateOcfConversionTriggerShape : OcfConversionTrigger -> Bool +validateOcfConversionTriggerShape t = validateRequiredText t.trigger_id && - validateOptionalText t.nickname && - validateOptionalText t.trigger_description && - validateOcfAnyConversionRight t.conversion_right && - validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date + validateOptionalText t.nickname && + validateOptionalText t.trigger_description && + validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index 8e85b105..ec8596d3 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -311,7 +311,6 @@ validStockClassRights : [OcfStockClassConversionRight] validStockClassRights = [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True - , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger ] invalidStockClassRights : [(Text, OcfStockClassConversionRight)] @@ -326,6 +325,7 @@ invalidStockClassRights = , ("SAFE", stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None) , ("NOTE", stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None) , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion")) + , ("INVALID_TRIGGER", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger) ] convertibleRight : OcfConvertibleConversionMechanism -> OcfConvertibleConversionRight @@ -369,6 +369,8 @@ warrantTrigger (suffix, mechanism) = OcfConversionTrigger with validConvertibleTriggers : [OcfConvertibleConversionTrigger] validConvertibleTriggers = [ convertibleTrigger ("AT-WILL", OcfConvMechCustom validCustomMechanism) + , (convertibleTrigger ("AUTO-DATE-NO-DATE", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeAutomaticOnDate , (convertibleTrigger ("AUTO-DATE", OcfConvMechCustom validCustomMechanism)) with trigger_date = Some testTime type_ = OcfTriggerTypeTypeAutomaticOnDate @@ -408,6 +410,8 @@ invalidConvertibleTriggers = validWarrantTriggers : [OcfConversionTrigger] validWarrantTriggers = [ warrantTrigger ("AT-WILL", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant") + , (warrantTrigger ("AUTO-DATE-NO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeAutomaticOnDate , (warrantTrigger ("AUTO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with trigger_date = Some testTime type_ = OcfTriggerTypeTypeAutomaticOnDate diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 385455d5..5cb174df 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -18,7 +18,7 @@ duplicateResultingSecurityIds = ["SEC-DUP", "SEC-DUP"] assertInvalid : Text -> Bool -> Script () assertInvalid label validationResult = - assertMsg (label <> " should reject duplicate security IDs") (not validationResult) + assertMsg (label <> " should reject invalid security IDs") (not validationResult) testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = script do assertInvalid "stock transfer" $ @@ -96,6 +96,16 @@ testUniqueSecurityArrays_ResultingSecurityValidatorsRejectDuplicateIds = script resulting_security_ids = duplicateResultingSecurityIds consideration_text = None + assertInvalid "equity compensation exercise empty result array" $ + validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with + id = "TX_EC_EXERCISE_EMPTY" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-EXERCISE" + comments = [] + resulting_security_ids = [] + consideration_text = None + assertInvalid "equity compensation release" $ validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with id = "TX_EC_RELEASE_DUP" diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 6bf5f57f..40902624 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06ebe2f73047e64a51e14720d8c05656346c01601ef41367a50a4f8abce933f3 -size 2763314 +oid sha256:fa595d0ef523579d21c13e4a61e2bea1a5233826a8bb2654b3dcb68c797de3c4 +size 2763512 diff --git a/dars/dars.lock b/dars/dars.lock index 8bcf3f76..b0a2020c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "06ebe2f73047e64a51e14720d8c05656346c01601ef41367a50a4f8abce933f3", - "size": 2763314, + "sha256": "fa595d0ef523579d21c13e4a61e2bea1a5233826a8bb2654b3dcb68c797de3c4", + "size": 2763512, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:46:05Z", + "uploadedAt": "2026-07-09T18:02:44Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From ad7e87b8c889a4ffa64d018d82233711563a8a83 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:08:51 -0400 Subject: [PATCH 29/50] Refresh schema constraints after reference fix --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 40902624..1dc3f3b9 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa595d0ef523579d21c13e4a61e2bea1a5233826a8bb2654b3dcb68c797de3c4 -size 2763512 +oid sha256:13e0952c254a8129f4fd8e79ceba692618d01c9370beee45696d52a1804c93e8 +size 2765305 diff --git a/dars/dars.lock b/dars/dars.lock index 5c701bf9..2d6935cd 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "fa595d0ef523579d21c13e4a61e2bea1a5233826a8bb2654b3dcb68c797de3c4", - "size": 2763512, + "sha256": "13e0952c254a8129f4fd8e79ceba692618d01c9370beee45696d52a1804c93e8", + "size": 2765305, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:02:44Z", + "uploadedAt": "2026-07-09T18:07:53Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 13c60272218b3ee005a9013706d19be9e1e95f51 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:45:24 -0400 Subject: [PATCH 30/50] Refresh schema constraints DAR after reference fold --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 1dc3f3b9..7c16c6c7 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13e0952c254a8129f4fd8e79ceba692618d01c9370beee45696d52a1804c93e8 -size 2765305 +oid sha256:3ad3de0cee530e1eef1df03f2179ef3cf9e06d07b60949a39622a69516a4efd4 +size 2777078 diff --git a/dars/dars.lock b/dars/dars.lock index d41b559c..afb1ca48 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "13e0952c254a8129f4fd8e79ceba692618d01c9370beee45696d52a1804c93e8", - "size": 2765305, + "sha256": "3ad3de0cee530e1eef1df03f2179ef3cf9e06d07b60949a39622a69516a4efd4", + "size": 2777078, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:07:53Z", + "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From cfdd6fbe17744b091960dbf149dd155f0ff1accc Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:29:45 -0400 Subject: [PATCH 31/50] Address conversion schema review feedback --- .../OpenCapTable/Types/Conversion.daml | 8 ++-- .../TestConversionMechanisms.daml | 48 +++++++++---------- .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 8035d1a8..2258a2fc 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -240,8 +240,8 @@ validateOcfSharePriceBasedConversionMechanism m = (Some _, Some _) -> False else case (m.discount_percentage, m.discount_amount) of - (Some _, Some _) -> False - _ -> percentOk && amountOk + (None, None) -> True + _ -> False -- Enum - Valuation Based Formula Type -- Formula for valuation-based conversion @@ -612,13 +612,13 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio noRange = isNone startDate && isNone endDate in case triggerType of OcfTriggerTypeTypeAutomaticOnDate -> - noCondition && noRange + isSome triggerDate && noCondition && noRange OcfTriggerTypeTypeAutomaticOnCondition -> hasCondition && noTriggerDate && noRange OcfTriggerTypeTypeElectiveOnCondition -> hasCondition && noTriggerDate && noRange OcfTriggerTypeTypeElectiveInRange -> - (noRange || (hasStartDate && hasEndDate)) && noCondition && noTriggerDate + hasStartDate && hasEndDate && noCondition && noTriggerDate OcfTriggerTypeTypeElectiveAtWill -> noCondition && noTriggerDate && noRange OcfTriggerTypeTypeUnspecified -> diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index ec8596d3..a86e51f0 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -92,14 +92,14 @@ invalidPpsMechanismDiscountDisabled = validPpsMechanism with discount_amount = Some (usd 1.0) discount_percentage = Some 0.20 -validPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism -validPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with +invalidPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with discount = False discount_amount = Some (usd 1.0) discount_percentage = None -validPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism -validPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with +invalidPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with discount = False discount_amount = None discount_percentage = Some 0.20 @@ -222,16 +222,6 @@ validWarrantMechanisms = discount = True discount_amount = None discount_percentage = Some 0.10) - , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with - description = "Disabled discount with amount" - discount = False - discount_amount = Some (usd 1.0) - discount_percentage = None) - , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with - description = "Disabled discount with percentage" - discount = False - discount_amount = None - discount_percentage = Some 0.10) ] invalidWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -267,6 +257,16 @@ invalidWarrantMechanisms = discount = False discount_amount = Some (usd 1.0) discount_percentage = Some 0.10) + , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Disabled discount with amount" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None) + , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Disabled discount with percentage" + discount = False + discount_amount = None + discount_percentage = Some 0.10) ] stockClassRightWith : @@ -369,8 +369,6 @@ warrantTrigger (suffix, mechanism) = OcfConversionTrigger with validConvertibleTriggers : [OcfConvertibleConversionTrigger] validConvertibleTriggers = [ convertibleTrigger ("AT-WILL", OcfConvMechCustom validCustomMechanism) - , (convertibleTrigger ("AUTO-DATE-NO-DATE", OcfConvMechCustom validCustomMechanism)) with - type_ = OcfTriggerTypeTypeAutomaticOnDate , (convertibleTrigger ("AUTO-DATE", OcfConvMechCustom validCustomMechanism)) with trigger_date = Some testTime type_ = OcfTriggerTypeTypeAutomaticOnDate @@ -384,8 +382,6 @@ validConvertibleTriggers = start_date = Some testTime end_date = Some testTime type_ = OcfTriggerTypeTypeElectiveInRange - , (convertibleTrigger ("ELECTIVE-RANGE-NO-DATES", OcfConvMechCustom validCustomMechanism)) with - type_ = OcfTriggerTypeTypeElectiveInRange , (convertibleTrigger ("UNSPECIFIED", OcfConvMechCustom validCustomMechanism)) with type_ = OcfTriggerTypeTypeUnspecified ] @@ -405,13 +401,15 @@ invalidConvertibleTriggers = type_ = OcfTriggerTypeTypeAutomaticOnDate) , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (convertibleTrigger ("AUTO-CONDITION-MISSING", OcfConvMechCustom validCustomMechanism)) with type_ = OcfTriggerTypeTypeAutomaticOnCondition) + , ("AUTOMATIC_ON_DATE_MISSING_DATE", (convertibleTrigger ("AUTO-DATE-NO-DATE", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("ELECTIVE_RANGE_MISSING_DATES", (convertibleTrigger ("ELECTIVE-RANGE-NO-DATES", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeElectiveInRange) ] validWarrantTriggers : [OcfConversionTrigger] validWarrantTriggers = [ warrantTrigger ("AT-WILL", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant") - , (warrantTrigger ("AUTO-DATE-NO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with - type_ = OcfTriggerTypeTypeAutomaticOnDate , (warrantTrigger ("AUTO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with trigger_date = Some testTime type_ = OcfTriggerTypeTypeAutomaticOnDate @@ -425,8 +423,6 @@ validWarrantTriggers = start_date = Some testTime end_date = Some testTime type_ = OcfTriggerTypeTypeElectiveInRange - , (warrantTrigger ("ELECTIVE-RANGE-NO-DATES", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with - type_ = OcfTriggerTypeTypeElectiveInRange , (warrantTrigger ("UNSPECIFIED", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with type_ = OcfTriggerTypeTypeUnspecified ] @@ -446,6 +442,10 @@ invalidWarrantTriggers = type_ = OcfTriggerTypeTypeAutomaticOnDate) , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (warrantTrigger ("AUTO-CONDITION-MISSING", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with type_ = OcfTriggerTypeTypeAutomaticOnCondition) + , ("AUTOMATIC_ON_DATE_MISSING_DATE", (warrantTrigger ("AUTO-DATE-NO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("ELECTIVE_RANGE_MISSING_DATES", (warrantTrigger ("ELECTIVE-RANGE-NO-DATES", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeElectiveInRange) ] assertValid : Text -> Bool -> Script () @@ -568,8 +568,8 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "note mechanism" (validateOcfNoteConversionMechanism invalidNoteMechanism) assertInvalid "note mechanism invalid cap" (validateOcfNoteConversionMechanism invalidNoteMechanismCap) assertInvalid "pps mechanism disabled conflicting discounts" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabled) - assertValid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithAmount) - assertValid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithPercentage) + assertInvalid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithAmount) + assertInvalid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithPercentage) assertValidConvertibleMechanisms validConvertibleMechanisms assertInvalidConvertibleMechanisms invalidConvertibleMechanisms assertValidWarrantMechanisms validWarrantMechanisms diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 7c16c6c7..137aeefc 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ad3de0cee530e1eef1df03f2179ef3cf9e06d07b60949a39622a69516a4efd4 -size 2777078 +oid sha256:d94a2e853925c25f711c58688e56045b47c2925f42d7c4511cb100ba866d89f8 +size 2776939 diff --git a/dars/dars.lock b/dars/dars.lock index afb1ca48..7282f3b7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "3ad3de0cee530e1eef1df03f2179ef3cf9e06d07b60949a39622a69516a4efd4", - "size": 2777078, + "sha256": "d94a2e853925c25f711c58688e56045b47c2925f42d7c4511cb100ba866d89f8", + "size": 2776939, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From 09492e061b96591d51a225c11ec25dbd251ad2c4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:42:39 -0400 Subject: [PATCH 32/50] Refresh schema constraints DAR after reference updates --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 137aeefc..e7662a7d 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d94a2e853925c25f711c58688e56045b47c2925f42d7c4511cb100ba866d89f8 -size 2776939 +oid sha256:87b3df8f1d58962b9d8d57c2b244710a1e9bde4d7cd9a52ef897f305be70fefa +size 2771640 diff --git a/dars/dars.lock b/dars/dars.lock index 4c739130..54683529 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "d94a2e853925c25f711c58688e56045b47c2925f42d7c4511cb100ba866d89f8", - "size": 2776939, + "sha256": "87b3df8f1d58962b9d8d57c2b244710a1e9bde4d7cd9a52ef897f305be70fefa", + "size": 2771640, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From aaaf885cd8b8b1abec397329dad8db076460aa08 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:59:36 -0400 Subject: [PATCH 33/50] Align conversion validation with OCF schemas --- .../OCF/EquityCompensationExercise.daml | 5 ++-- .../OpenCapTable/Types/Conversion.daml | 2 +- .../TestEquityCompensationExercise.daml | 18 ++++++++++++++ ...stStockClassConversionRightReferences.daml | 24 +++++++++++++++++++ .../TestUniqueSecurityArrays.daml | 3 ++- .../0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 7 files changed, 52 insertions(+), 8 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index 8ec8b286..c3ca01bc 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,4 +61,5 @@ validateOcfEquityCompensationExerciseData d = validateOptionalText d.consideration_text && -- Arrays element validation validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 2258a2fc..46c6c069 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -565,7 +565,7 @@ data OcfStockClassConversionRight = OcfStockClassConversionRight validateOcfStockClassConversionRight : OcfStockClassConversionRight -> Bool validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.type_ && - validateOcfConversionTriggerShape conversionRight.conversion_trigger && + validateOcfConversionTrigger conversionRight.conversion_trigger && validateRequiredText conversionRight.converts_to_stock_class_id && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> diff --git a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml index 48b46d44..0053d3c8 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml @@ -46,6 +46,24 @@ testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do deletes = [] pure () +testEquityCompensationExerciseAllowsEmptyResultingSecurityIds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-EMPTY-RESULT" "EC-EMPTY-RESULT" "SH-1" + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX_EC_EX_EMPTY_RESULT" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "EC-EMPTY-RESULT" + quantity = 10.0 + consideration_text = None + resulting_security_ids = [] + comments = []] + edits = [] + deletes = [] + pure () + -- Optionals Some testEquityCompensationExercise_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml index 35623f9c..ff0a0380 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -87,6 +87,30 @@ testStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] +testStockClassConversionRightInvalidNestedTriggerFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass + ((preferredStockClassWithConversionRight "SC_PREFERRED_INVALID_NESTED_TRIGGER" "SC_COMMON_INVALID_NESTED_TRIGGER") with + conversion_rights = + [ (stockClassConversionRightTo "SC_COMMON_INVALID_NESTED_TRIGGER") with + conversion_trigger = + (ocfTriggerElectiveAtWill with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = None + type_ = "") + ) + ]) + , CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_INVALID_NESTED_TRIGGER") + ] + edits = [] + deletes = [] + testStockClassConversionRightNestedMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 5cb174df..0619db75 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -96,7 +96,8 @@ testUniqueSecurityArrays_ResultingSecurityValidatorsRejectDuplicateIds = script resulting_security_ids = duplicateResultingSecurityIds consideration_text = None - assertInvalid "equity compensation exercise empty result array" $ +testUniqueSecurityArrays_EquityCompensationExerciseAllowsEmptyResultingSecurityIds = script do + assertMsg "equity compensation exercise empty result array should be valid" $ validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with id = "TX_EC_EXERCISE_EMPTY" date = defaultTestDate diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index e7662a7d..ddbc3efb 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87b3df8f1d58962b9d8d57c2b244710a1e9bde4d7cd9a52ef897f305be70fefa -size 2771640 +oid sha256:699ea18ff8ab2645cc529cf267f7e2b7eb718c56af27c7d345db35ddca5c9dab +size 2771737 diff --git a/dars/dars.lock b/dars/dars.lock index 54683529..cdd0a766 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "87b3df8f1d58962b9d8d57c2b244710a1e9bde4d7cd9a52ef897f305be70fefa", - "size": 2771640, + "sha256": "699ea18ff8ab2645cc529cf267f7e2b7eb718c56af27c7d345db35ddca5c9dab", + "size": 2771737, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From 36f4c755f49e6e69c3cc7396684ca454b0593f92 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 10:40:05 -0400 Subject: [PATCH 34/50] fix: preserve numbered share lineage --- .../Fairmint/OpenCapTable/Types/Monetary.daml | 2 +- .../TestCountrySubdivisionCode.daml | 13 ++ Test/daml/OpenCapTable/TestStockIssuance.daml | 145 ++++++++++++++++++ .../codegen/templates/CapTable.daml.template | 139 ++++++++++++++--- 4 files changed, 276 insertions(+), 23 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml index 68cf0c25..f9c26a6d 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml @@ -74,7 +74,7 @@ data OcfAddress = OcfAddress -- City name city: Optional Text - -- Country subdivision/state/province code or name + -- Country subdivision/state/province code country_subdivision: Optional Text -- Postal or ZIP code postal_code: Optional Text diff --git a/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml index 66f45480..eec16b79 100644 --- a/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml +++ b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml @@ -59,6 +59,7 @@ testCountrySubdivisionCodePattern = script do assertSubdivisionInvalid "ca" assertSubdivisionInvalid "C-" assertSubdivisionInvalid "C A" + assertSubdivisionInvalid "California" testAddressCountrySubdivisionCodePattern = script do assertMsg "address without subdivision should validate" (validateOcfAddress (ocfAddress None)) @@ -66,6 +67,7 @@ testAddressCountrySubdivisionCodePattern = script do assertMsg "lowercase subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "ca")))) assertMsg "symbol subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "C-")))) assertMsg "long subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "CALI")))) + assertMsg "subdivision name in code field should not validate" (not (validateOcfAddress (ocfAddress (Some "California")))) testIssuerCountrySubdivisionCodePatternRejectsInvalid = script do TestOcp{system_operator, issuer} <- setupTestOcp @@ -86,3 +88,14 @@ testIssuerAddressCountrySubdivisionCodePatternRejectsInvalid = script do exerciseCmd authCid CreateCapTable with issuer_data = issuerData None (Some (ocfAddress (Some "C-"))) pure () + +testIssuerCountrySubdivisionNameAcceptsPlainName = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + _ <- submit issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = (issuerData None None) with + country_subdivision_name_of_formation = Some "California" + pure () diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 7601a135..d3b236ff 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -1,7 +1,9 @@ module OpenCapTable.TestStockIssuance where import Fairmint.OpenCapTable.OCF.StockIssuance +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -178,6 +180,149 @@ testStockIssuanceShareNumberRangesCanReuseRetractedRange = script do deletes = [] pure () +testStockIssuanceShareNumberRangesCanReuseFullyCancelledRange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CANCELLED" "SEC-SHARE-NUMBERS-CANCELLED" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let cancellation = StockCancellationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_CANCEL" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 10.0 + reason_text = "Cancelled before replacement" + security_id = firstIssuance.security_id + comments = [] + balance_security_id = None + let replacementIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_AFTER_CANCEL" "SEC-SHARE-NUMBERS-AFTER-CANCEL" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesFollowPartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_SOURCE" "SEC-SHARE-NUMBERS-TRANSFER-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_RECIPIENT" "SEC-SHARE-NUMBERS-TRANSFER-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_BALANCE" "SEC-SHARE-NUMBERS-TRANSFER-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_PARTIAL_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesRejectUnnumberedPartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_SOURCE" "SEC-SHARE-NUMBERS-EMPTY-BALANCE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_RECIPIENT" "SEC-SHARE-NUMBERS-EMPTY-BALANCE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE" "SEC-SHARE-NUMBERS-EMPTY-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesRejectUntrackedPartialTransferRemainder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNTRACKED_SOURCE" "SEC-SHARE-NUMBERS-UNTRACKED-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNTRACKED_RECIPIENT" "SEC-SHARE-NUMBERS-UNTRACKED-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_UNTRACKED_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = None + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + ] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesCannotOverlapBeforeRetraction = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index fc3832cb..f8219e0a 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1339,10 +1339,89 @@ validateStockIssuanceReferences maps = data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with stock_class_id: Text issuance_id: Text + security_id: Text share_lot: StockClassShareLot share_number_range: OcfShareNumberRange deriving (Eq, Show) +addStockShareNumberTerminalDate : Text -> Time -> Map Text [Time] -> Map Text [Time] +addStockShareNumberTerminalDate securityId eventDate index = + Map.insert securityId (eventDate :: fromOptional [] (Map.lookup securityId index)) index + +addStockShareNumberBalanceTerminalDate : + CapTableMaps -> + Text -> + Time -> + Optional Text -> + Map Text [Time] -> + Update (Map Text [Time]) +addStockShareNumberBalanceTerminalDate maps sourceSecurityId eventDate balanceSecurityId index = + case balanceSecurityId of + Some balanceSecurityId' -> + case (Map.lookup sourceSecurityId maps.stock_issuances_by_security_id, Map.lookup balanceSecurityId' maps.stock_issuances_by_security_id) of + (Some sourceCid, Some balanceCid) -> do + source <- fetch sourceCid + balance <- fetch balanceCid + case source.issuance_data.share_numbers_issued of + [] -> pure () + _ -> + assertMsg + ("Stock balance issuance must preserve share-number tracking for " <> sourceSecurityId) + (not (null balance.issuance_data.share_numbers_issued)) + pure (addStockShareNumberTerminalDate sourceSecurityId eventDate index) + _ -> pure index + None -> pure index + +stockShareNumberBalanceTerminalDateIndex : CapTableMaps -> Update (Map Text [Time]) +stockShareNumberBalanceTerminalDateIndex maps = do + cancellationIndex <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + addStockShareNumberBalanceTerminalDate + maps + cancellation.cancellation_data.security_id + cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id + index) + Map.empty + (Map.values maps.stock_cancellations) + + repurchaseIndex <- foldlA + (\index repurchaseCid -> do + repurchase <- fetch repurchaseCid + addStockShareNumberBalanceTerminalDate + maps + repurchase.repurchase_data.security_id + repurchase.repurchase_data.date + repurchase.repurchase_data.balance_security_id + index) + cancellationIndex + (Map.values maps.stock_repurchases) + + transferIndex <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addStockShareNumberBalanceTerminalDate + maps + transfer.transfer_data.security_id + transfer.transfer_data.date + transfer.transfer_data.balance_security_id + index) + repurchaseIndex + (Map.values maps.stock_transfers) + + foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + addStockShareNumberBalanceTerminalDate + maps + conversion.conversion_data.security_id + conversion.conversion_data.date + conversion.conversion_data.balance_security_id + index) + transferIndex + (Map.values maps.stock_conversions) + stockIssuanceShareNumberLots : StockClassShareEventIndex -> CapTableMaps -> Update [StockIssuanceShareNumberLot] stockIssuanceShareNumberLots eventIndex maps = foldlA @@ -1356,6 +1435,7 @@ stockIssuanceShareNumberLots eventIndex maps = (\shareNumberRange -> StockIssuanceShareNumberLot with stock_class_id = issuance.issuance_data.stock_class_id issuance_id = issuance.issuance_data.id + security_id = issuance.issuance_data.security_id share_lot = shareLot share_number_range = shareNumberRange) issuance.issuance_data.share_numbers_issued @@ -1368,12 +1448,13 @@ stockIssuanceShareNumberLots eventIndex maps = validateStockIssuanceShareNumberLotDoesNotOverlap : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> Update () -validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot other = +validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex terminalDateIndex lot other = when (lot.issuance_id /= other.issuance_id && lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do - overlapsWhileActive <- stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other + overlapsWhileActive <- stockIssuanceShareNumberLotsOverlapWhileActive eventIndex terminalDateIndex lot other when overlapsWhileActive do assertMsg ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) @@ -1389,32 +1470,42 @@ stockIssuanceShareNumberLotIndex lots = stockIssuanceShareNumberLotRelevantDates : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> [Time] -stockIssuanceShareNumberLotRelevantDates eventIndex lot other = +stockIssuanceShareNumberLotRelevantDates eventIndex terminalDateIndex lot other = map stockClassShareEventDate - (stockClassShareLotEvents eventIndex lot.share_lot ++ stockClassShareLotEvents eventIndex other.share_lot) + (stockClassShareLotEvents eventIndex lot.share_lot ++ stockClassShareLotEvents eventIndex other.share_lot) ++ + fromOptional [] (Map.lookup lot.security_id terminalDateIndex) ++ + fromOptional [] (Map.lookup other.security_id terminalDateIndex) stockIssuanceShareNumberLotActiveAt : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> Time -> Optional Bool -stockIssuanceShareNumberLotActiveAt eventIndex lot asOf = - case stockClassShareLotQuantityAt eventIndex lot.share_lot asOf of - Some quantity -> Some (quantity > 0.0) - None -> None +stockIssuanceShareNumberLotActiveAt eventIndex terminalDateIndex lot asOf = + let replacedByBalance = any + (\eventDate -> lot.share_lot.event_date <= eventDate && eventDate <= asOf) + (fromOptional [] (Map.lookup lot.security_id terminalDateIndex)) + in if replacedByBalance + then Some False + else case stockClassShareLotQuantityAt eventIndex lot.share_lot asOf of + Some quantity -> Some (quantity > 0.0) + None -> None stockIssuanceShareNumberLotsOverlapOnDate : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> Time -> Update Bool -stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf = - case (stockIssuanceShareNumberLotActiveAt eventIndex lot asOf, stockIssuanceShareNumberLotActiveAt eventIndex other asOf) of +stockIssuanceShareNumberLotsOverlapOnDate eventIndex terminalDateIndex lot other asOf = + case (stockIssuanceShareNumberLotActiveAt eventIndex terminalDateIndex lot asOf, stockIssuanceShareNumberLotActiveAt eventIndex terminalDateIndex other asOf) of (Some lotActive, Some otherActive) -> pure (lotActive && otherActive) _ -> do assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> lot.issuance_id <> " or " <> other.issuance_id) False @@ -1422,47 +1513,51 @@ stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf = stockIssuanceShareNumberLotsOverlapWhileActive : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> StockIssuanceShareNumberLot -> Update Bool -stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other = +stockIssuanceShareNumberLotsOverlapWhileActive eventIndex terminalDateIndex lot other = foldlA (\overlaps asOf -> if overlaps then pure True - else stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf) + else stockIssuanceShareNumberLotsOverlapOnDate eventIndex terminalDateIndex lot other asOf) False - (stockIssuanceShareNumberLotRelevantDates eventIndex lot other) + (stockIssuanceShareNumberLotRelevantDates eventIndex terminalDateIndex lot other) validateStockIssuanceShareNumberLotAgainstRest : StockClassShareEventIndex -> + Map Text [Time] -> StockIssuanceShareNumberLot -> [StockIssuanceShareNumberLot] -> Update () -validateStockIssuanceShareNumberLotAgainstRest eventIndex lot lots = - mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot) lots +validateStockIssuanceShareNumberLotAgainstRest eventIndex terminalDateIndex lot lots = + mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex terminalDateIndex lot) lots validateStockClassShareNumberLotsDoNotOverlap : StockClassShareEventIndex -> + Map Text [Time] -> [StockIssuanceShareNumberLot] -> Update () -validateStockClassShareNumberLotsDoNotOverlap eventIndex lots = case lots of +validateStockClassShareNumberLotsDoNotOverlap eventIndex terminalDateIndex lots = case lots of lot :: rest -> do - validateStockIssuanceShareNumberLotAgainstRest eventIndex lot rest - validateStockClassShareNumberLotsDoNotOverlap eventIndex rest + validateStockIssuanceShareNumberLotAgainstRest eventIndex terminalDateIndex lot rest + validateStockClassShareNumberLotsDoNotOverlap eventIndex terminalDateIndex rest _ -> pure () -validateStockIssuanceShareNumberLotsDoNotOverlap : StockClassShareEventIndex -> [StockIssuanceShareNumberLot] -> Update () -validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots = +validateStockIssuanceShareNumberLotsDoNotOverlap : StockClassShareEventIndex -> Map Text [Time] -> [StockIssuanceShareNumberLot] -> Update () +validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex terminalDateIndex lots = mapA_ - (validateStockClassShareNumberLotsDoNotOverlap eventIndex) + (validateStockClassShareNumberLotsDoNotOverlap eventIndex terminalDateIndex) (Map.values (stockIssuanceShareNumberLotIndex lots)) validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () validateStockIssuanceShareNumberRanges maps = do eventIndex <- stockClassShareEventIndex maps + terminalDateIndex <- stockShareNumberBalanceTerminalDateIndex maps lots <- stockIssuanceShareNumberLots eventIndex maps - validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots + validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex terminalDateIndex lots validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () validateEquityCompensationIssuanceReferences maps = From 066c649ace808ac4f0d7188d77eb1aaf5cc7f20e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 12:56:29 -0400 Subject: [PATCH 35/50] fix: align conversion schema edge cases --- .../OpenCapTable/Types/Conversion.daml | 8 ++-- .../TestConversionMechanisms.daml | 46 +++++++++++++------ ...stStockClassConversionRightReferences.daml | 10 ++-- .../TestWarrantConversionRightReferences.daml | 5 +- .../codegen/templates/CapTable.daml.template | 5 +- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 46c6c069..4340b5e5 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -241,7 +241,9 @@ validateOcfSharePriceBasedConversionMechanism m = else case (m.discount_percentage, m.discount_amount) of (None, None) -> True - _ -> False + (Some _, None) -> percentOk + (None, Some _) -> amountOk + (Some _, Some _) -> False -- Enum - Valuation Based Formula Type -- Formula for valuation-based conversion @@ -526,7 +528,8 @@ data OcfStockClassConversionRight = OcfStockClassConversionRight -- Mechanism by which conversion occurs conversion_mechanism: OcfConversionMechanism - -- Trigger that would cause conversion + -- Required by the DAML representation, but absent from the OCF stock-class + -- conversion-right schema and therefore ignored by OCF validation. conversion_trigger: OcfConversionTrigger -- Identifier of stock class to which this converts converts_to_stock_class_id: Text @@ -565,7 +568,6 @@ data OcfStockClassConversionRight = OcfStockClassConversionRight validateOcfStockClassConversionRight : OcfStockClassConversionRight -> Bool validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.type_ && - validateOcfConversionTrigger conversionRight.conversion_trigger && validateRequiredText conversionRight.converts_to_stock_class_id && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index a86e51f0..134be523 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -92,18 +92,26 @@ invalidPpsMechanismDiscountDisabled = validPpsMechanism with discount_amount = Some (usd 1.0) discount_percentage = Some 0.20 -invalidPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism -invalidPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with +validPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with discount = False discount_amount = Some (usd 1.0) discount_percentage = None -invalidPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism -invalidPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with +validPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with discount = False discount_amount = None discount_percentage = Some 0.20 +invalidPpsMechanismDiscountDisabledWithInvalidAmount : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithInvalidAmount = validPpsMechanismDiscountDisabledWithAmount with + discount_amount = Some invalidCurrency + +invalidPpsMechanismDiscountDisabledWithInvalidPercentage : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithInvalidPercentage = validPpsMechanismDiscountDisabledWithPercentage with + discount_percentage = Some 1.10 + validValuationMechanism : OcfValuationBasedConversionMechanism validValuationMechanism = OcfValuationBasedConversionMechanism with capitalization_definition = None @@ -222,6 +230,16 @@ validWarrantMechanisms = discount = True discount_amount = None discount_percentage = Some 0.10) + , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled with amount" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None) + , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled with percentage" + discount = False + discount_amount = None + discount_percentage = Some 0.10) ] invalidWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -257,16 +275,16 @@ invalidWarrantMechanisms = discount = False discount_amount = Some (usd 1.0) discount_percentage = Some 0.10) - , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with - description = "Disabled discount with amount" + , ("PPS_DISABLED_INVALID_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Disabled discount with invalid amount" discount = False - discount_amount = Some (usd 1.0) + discount_amount = Some invalidCurrency discount_percentage = None) - , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with - description = "Disabled discount with percentage" + , ("PPS_DISABLED_INVALID_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Disabled discount with invalid percentage" discount = False discount_amount = None - discount_percentage = Some 0.10) + discount_percentage = Some 1.10) ] stockClassRightWith : @@ -311,6 +329,7 @@ validStockClassRights : [OcfStockClassConversionRight] validStockClassRights = [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True + , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger ] invalidStockClassRights : [(Text, OcfStockClassConversionRight)] @@ -325,7 +344,6 @@ invalidStockClassRights = , ("SAFE", stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None) , ("NOTE", stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None) , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion")) - , ("INVALID_TRIGGER", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger) ] convertibleRight : OcfConvertibleConversionMechanism -> OcfConvertibleConversionRight @@ -568,8 +586,10 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "note mechanism" (validateOcfNoteConversionMechanism invalidNoteMechanism) assertInvalid "note mechanism invalid cap" (validateOcfNoteConversionMechanism invalidNoteMechanismCap) assertInvalid "pps mechanism disabled conflicting discounts" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabled) - assertInvalid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithAmount) - assertInvalid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithPercentage) + assertValid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithAmount) + assertValid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithPercentage) + assertInvalid "pps mechanism disabled with invalid amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithInvalidAmount) + assertInvalid "pps mechanism disabled with invalid percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithInvalidPercentage) assertValidConvertibleMechanisms validConvertibleMechanisms assertInvalidConvertibleMechanisms invalidConvertibleMechanisms assertValidWarrantMechanisms validWarrantMechanisms diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml index ff0a0380..90a0056d 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -87,10 +87,10 @@ testStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] -testStockClassConversionRightInvalidNestedTriggerFails = script do +testStockClassConversionRightPlaceholderTriggerShapeIgnored = script do TestOcp{issuer, cap_table} <- setupTestOcp - submitMustFail issuer do + _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass @@ -110,11 +110,12 @@ testStockClassConversionRightInvalidNestedTriggerFails = script do ] edits = [] deletes = [] + pure () -testStockClassConversionRightNestedMissingTargetFails = script do +testStockClassConversionRightPlaceholderTriggerReferenceIgnored = script do TestOcp{issuer, cap_table} <- setupTestOcp - submitMustFail issuer do + _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass (preferredStockClassWithNestedConversionRight "SC_PREFERRED_NESTED_MISSING" "SC_COMMON_OUTER_TARGET" (Some "SC_MISSING_NESTED_TARGET")) @@ -122,6 +123,7 @@ testStockClassConversionRightNestedMissingTargetFails = script do ] edits = [] deletes = [] + pure () testStockClassConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index dd76f51d..8de07d37 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -178,11 +178,11 @@ testWarrantStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] -testWarrantStockClassConversionRightNestedMissingTargetFails = script do +testWarrantStockClassConversionRightPlaceholderTriggerReferenceIgnored = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableWithStakeholder CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass (defaultStockClass "SC_WARRANT_OUTER_TARGET") @@ -194,6 +194,7 @@ testWarrantStockClassConversionRightNestedMissingTargetFails = script do ] edits = [] deletes = [] + pure () testWarrantConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f8219e0a..524aa57e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1272,9 +1272,10 @@ validateAnyConversionRightStockClassReferences label maps conversionRight = case validateOptionalStockClassReference (label <> " convertible conversion target") maps convertibleRight.converts_to_stock_class_id OcfRightWarrant warrantRight -> validateOptionalStockClassReference (label <> " warrant conversion target") maps warrantRight.converts_to_stock_class_id - OcfRightStockClass stockClassRight -> do + -- The DAML stock-class right carries a placeholder trigger that is absent + -- from the OCF schema, so only the actual stock-class target is meaningful. + OcfRightStockClass stockClassRight -> validateStockClassReference (label <> " stock class conversion target") maps stockClassRight.converts_to_stock_class_id - validateAnyConversionRightStockClassReferences (label <> " stock class nested trigger") maps stockClassRight.conversion_trigger.conversion_right validateConvertibleConversionRightReferences : CapTableMaps -> Update () validateConvertibleConversionRightReferences maps = From 104fb4ea4ad232149f3ba3424c98e2be34420ff7 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:10:08 -0400 Subject: [PATCH 36/50] fix: handle terminal share number events --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- scripts/codegen/templates/CapTable.daml.template | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index ddbc3efb..aba59359 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:699ea18ff8ab2645cc529cf267f7e2b7eb718c56af27c7d345db35ddca5c9dab -size 2771737 +oid sha256:734c851c2304e9e95f255db97541c3d8e433945075f440a1abbe0c38d288ec2e +size 2834208 diff --git a/dars/dars.lock b/dars/dars.lock index 6c5977b4..84deb2f5 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "699ea18ff8ab2645cc529cf267f7e2b7eb718c56af27c7d345db35ddca5c9dab", - "size": 2771737, + "sha256": "734c851c2304e9e95f255db97541c3d8e433945075f440a1abbe0c38d288ec2e", + "size": 2834208, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 5abca181..cf96058e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -739,6 +739,7 @@ stockClassShareEventSortKey event = case event of stockClassShareEventDate : StockClassShareEvent -> Time stockClassShareEventDate event = case event of StockClassShareDelta with event_date -> event_date + StockClassShareTerminalDelta with event_date -> event_date StockClassShareMultiplier with event_date -> event_date StockClassShareRetraction with event_date -> event_date From e63e9ac59b74363b6d8164f1d98f10c86db9e243 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:44:43 -0400 Subject: [PATCH 37/50] chore: refresh schema constraints DAR --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index aba59359..dca65df3 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:734c851c2304e9e95f255db97541c3d8e433945075f440a1abbe0c38d288ec2e -size 2834208 +oid sha256:02f76ddceab45ec570898ab1be61454ffdbde95aca2c66bfb19e282067c86492 +size 2892192 diff --git a/dars/dars.lock b/dars/dars.lock index 291cf4e2..4db7518f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "734c851c2304e9e95f255db97541c3d8e433945075f440a1abbe0c38d288ec2e", - "size": 2834208, + "sha256": "02f76ddceab45ec570898ab1be61454ffdbde95aca2c66bfb19e282067c86492", + "size": 2892192, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From dbcea4ded2780a9792d41e56a55f5c9ef512ed9b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 14:45:05 -0400 Subject: [PATCH 38/50] fix: preserve balance share-number lineage --- .../Fairmint/OpenCapTable/Types/Stock.daml | 12 ++++++ Test/daml/OpenCapTable/TestStockIssuance.daml | 38 +++++++++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 11 ++++-- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml index fa6f3389..98ee9e65 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml @@ -90,6 +90,18 @@ shareNumberRangesOverlap a b = a.starting_share_number <= b.ending_share_number && b.starting_share_number <= a.ending_share_number +shareNumberRangeIntersectionQuantity : OcfShareNumberRange -> OcfShareNumberRange -> Decimal +shareNumberRangeIntersectionQuantity a b = + let startingShareNumber = max a.starting_share_number b.starting_share_number + endingShareNumber = min a.ending_share_number b.ending_share_number + in if startingShareNumber <= endingShareNumber + then endingShareNumber - startingShareNumber + 1.0 + else 0.0 + +shareNumberRangeCoveredByRanges : [OcfShareNumberRange] -> OcfShareNumberRange -> Bool +shareNumberRangeCoveredByRanges coveringRanges candidate = + sum (map (shareNumberRangeIntersectionQuantity candidate) coveringRanges) == shareNumberRangeQuantity candidate + shareNumberRangeSortKey : OcfShareNumberRange -> (Decimal, Decimal) shareNumberRangeSortKey r = (r.starting_share_number, r.ending_share_number) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index f6c2a848..aa1dcc25 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -251,6 +251,44 @@ testStockIssuanceShareNumberRangesFollowPartialTransferBalance = script do deletes = [] pure () +testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_SOURCE" "SEC-SHARE-NUMBERS-LINEAGE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_RECIPIENT" "SEC-SHARE-NUMBERS-LINEAGE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_BALANCE" "SEC-SHARE-NUMBERS-LINEAGE-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_LINEAGE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + testStockIssuanceShareNumberRangesRejectUnnumberedPartialTransferBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index dca65df3..128bdc00 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f76ddceab45ec570898ab1be61454ffdbde95aca2c66bfb19e282067c86492 -size 2892192 +oid sha256:ebcfbcfd25684efd2166e3972e8d4ad348c6a857aa466bce7dc234aa84b8af80 +size 2894258 diff --git a/dars/dars.lock b/dars/dars.lock index 4db7518f..88467b1f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "02f76ddceab45ec570898ab1be61454ffdbde95aca2c66bfb19e282067c86492", - "size": 2892192, + "sha256": "ebcfbcfd25684efd2166e3972e8d4ad348c6a857aa466bce7dc234aa84b8af80", + "size": 2894258, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 0a7079a9..339b514a 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -19,7 +19,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangesOverlap) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) import qualified Fairmint.OpenCapTable.OCF.VestingTerms as VT @@ -1217,10 +1217,13 @@ validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQu (balanceIssuance.issuance_data.quantity == expectedQuantity) case sourceIssuanceData.share_numbers_issued of [] -> pure () - _ -> + sourceShareNumberRanges -> assertMsg - (label <> " balance result must preserve share-number tracking for " <> balanceSecurityId) - (not (null balanceIssuance.issuance_data.share_numbers_issued)) + (label <> " balance result share numbers are outside the source issuance range for " <> balanceSecurityId) + (not (null balanceIssuance.issuance_data.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + balanceIssuance.issuance_data.share_numbers_issued) None -> assertMsg (label <> " balance stock issuance not found: " <> balanceSecurityId) False From becb90862d23a4ac4abca89b2a7246d7f4bbe0fa Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:41:07 -0400 Subject: [PATCH 39/50] fix: preserve canonical conversion and share lineage --- .../OpenCapTable/TestStockConversion.daml | 40 +++++- Test/daml/OpenCapTable/TestStockIssuance.daml | 64 ++++++++++ .../codegen/templates/CapTable.daml.template | 118 ++++++++++-------- 3 files changed, 168 insertions(+), 54 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index c28abbbb..15e778c4 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -34,8 +34,8 @@ stockClassConversionRightTo stockClassId = type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None -addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) -addPreferredStockClass issuer capTableCid = do +addPreferredStockClassWithRight : Party -> ContractId CT.CapTable -> OcfStockClassConversionRight -> Script (ContractId CT.CapTable) +addPreferredStockClassWithRight issuer capTableCid conversionRight = do result <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = @@ -43,12 +43,16 @@ addPreferredStockClass issuer capTableCid = do class_type = OcfStockClassTypePreferred default_id_prefix = "PS-" seniority = 2.0 - conversion_rights = [stockClassConversionRightTo "SC_COMMON"]) + conversion_rights = [conversionRight]) ] edits = [] deletes = [] pure result.updatedCapTableCid +addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) +addPreferredStockClass issuer capTableCid = + addPreferredStockClassWithRight issuer capTableCid (stockClassConversionRightTo "SC_COMMON") + -- Create a stock issuance to convert (follows real workflow) createStockIssuance issuer capTableCid securityId quantity = submit issuer do @@ -99,6 +103,36 @@ testStockConversion_PreferredToCommon = script do deletes = [] pure () +-- StockClassConversionRight has no conversion_trigger property in OCF. The +-- Daml record still carries the field, so conversion calculations must ignore +-- whichever placeholder value an importer supplies. +testStockConversion_IgnoresNonOcfPlaceholderTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let placeholderTrigger = ocfTriggerElectiveAtWill with + type_ = OcfTriggerTypeTypeElectiveInRange + start_date = None + end_date = None + let conversionRight = (stockClassConversionRightTo "SC_COMMON") with + conversion_trigger = placeholderTrigger + capTableCid <- addPreferredStockClassWithRight issuer capTableCid conversionRight + let securityId = "SSEC-PREF-NON-OCF-TRIGGER" + result <- createStockIssuance issuer capTableCid securityId 100.0 + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_NON_OCF_TRIGGER" + date = DT.time (DA.date 2025 Dec 15) 0 0 0 + security_id = securityId + quantity_converted = 100.0 + resulting_security_ids = ["SSEC-COMMON-NON-OCF-TRIGGER"] + comments = [] + balance_security_id = None] + edits = [] + deletes = [] + pure () + -- Test 2: Partial conversion with balance security testStockConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index aa1dcc25..afd67c65 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -251,6 +251,32 @@ testStockIssuanceShareNumberRangesFollowPartialTransferBalance = script do deletes = [] pure () +testStockIssuanceShareNumberRangesAllowUnmaterializedTransferResults = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNMATERIALIZED_SOURCE" "SEC-SHARE-NUMBERS-UNMATERIALIZED-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_UNMATERIALIZED_TRANSFER" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 100.0 + security_id = sourceIssuance.security_id + resulting_security_ids = ["SEC-SHARE-NUMBERS-UNMATERIALIZED-RESULT"] + comments = [] + balance_security_id = None + consideration_text = None + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer transfer] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -289,6 +315,44 @@ testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferBalance = scr edits = [] deletes = [] +testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_SOURCE" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_RECIPIENT" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_BALANCE" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + testStockIssuanceShareNumberRangesRejectUnnumberedPartialTransferBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 339b514a..b7435e7b 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) @@ -337,20 +337,6 @@ stockClassConversionOutputQuantity maps sourceStockClass conversionRight convers None -> pure None _ -> pure None -stockClassConversionRightDateEligible : Time -> OcfStockClassConversionRight -> Bool -stockClassConversionRightDateEligible conversionDate conversionRight = - let trigger = conversionRight.conversion_trigger - beforeExpiration = case conversionRight.expires_at of - Some expiresAt -> conversionDate <= expiresAt - None -> True - triggerDateEligible = case trigger.type_ of - OcfTriggerTypeTypeAutomaticOnDate -> trigger.trigger_date == Some conversionDate - OcfTriggerTypeTypeElectiveInRange -> case (trigger.start_date, trigger.end_date) of - (Some startDate, Some endDate) -> startDate <= conversionDate && conversionDate <= endDate - _ -> False - _ -> True - in beforeExpiration && triggerDateEligible - stockConversionResultTerms : CapTableMaps -> Map Text StockClassOcfData -> @@ -361,24 +347,23 @@ stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionDat case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of Some sourceStockClass -> do terms <- foldlA - (\resultTerms conversionRight -> - if stockClassConversionRightDateEligible conversionData.date conversionRight - then do - quantityOpt <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData - case quantityOpt of - Some quantity -> do - assertMsg - ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) - (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) - pure ((StockConversionResultTerm with - stock_class_id = conversionRight.converts_to_stock_class_id - quantity = quantity) :: resultTerms) - None -> pure resultTerms - else pure resultTerms) + (\resultTerms conversionRight -> do + -- OCF StockClassConversionRight defines no trigger or expiration; + -- every schema-valid right is eligible for quantity calculation. + quantityOpt <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData + case quantityOpt of + Some quantity -> do + assertMsg + ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) + (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) + pure ((StockConversionResultTerm with + stock_class_id = conversionRight.converts_to_stock_class_id + quantity = quantity) :: resultTerms) + None -> pure resultTerms) [] sourceStockClass.conversion_rights assertMsg - ("Stock conversion has no computable date-eligible conversion right: " <> conversionData.id) + ("Stock conversion has no computable conversion right: " <> conversionData.id) (not (null terms)) pure terms None -> do @@ -402,7 +387,7 @@ stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversion [] -> pure None _ -> do assertMsg - ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are date-eligible: " <> conversionData.id) + ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are computable: " <> conversionData.id) False pure None @@ -550,7 +535,7 @@ validateStockConversionResultIssuances maps conversionData expectedTerms = do case actualStockClassIdOpt of Some actualStockClassId -> assertMsg - ("Stock conversion results do not match any date-eligible conversion right for " <> conversionData.id <> ": found class " <> actualStockClassId <> " and quantity " <> show actualQuantity) + ("Stock conversion results do not match any conversion right for " <> conversionData.id <> ": found class " <> actualStockClassId <> " and quantity " <> show actualQuantity) (any (\expectedTerm -> expectedTerm.stock_class_id == actualStockClassId && @@ -1194,6 +1179,41 @@ stockSecurityQuantityBeforeTerminal : stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) +validateStockSuccessorShareNumberRanges : + Text -> + StockIssuanceOcfData -> + StockIssuanceOcfData -> + Update () +validateStockSuccessorShareNumberRanges label sourceIssuanceData successorIssuanceData = + case sourceIssuanceData.share_numbers_issued of + [] -> pure () + sourceShareNumberRanges -> + assertMsg + (label <> " share numbers are outside the source issuance range for " <> successorIssuanceData.security_id) + (not (null successorIssuanceData.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + successorIssuanceData.share_numbers_issued) + +validateStockResultShareNumberRanges : + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + [Text] -> + Update () +validateStockResultShareNumberRanges maps label sourceIssuanceData resultSecurityIds = + mapA_ + (\resultSecurityId -> + case Map.lookup resultSecurityId maps.stock_issuances_by_security_id of + Some resultIssuanceCid -> do + resultIssuance <- fetch resultIssuanceCid + validateStockSuccessorShareNumberRanges label sourceIssuanceData resultIssuance.issuance_data + -- OCF lifecycle transactions may be imported without materializing + -- their resulting StockIssuance objects. Partial materialization is + -- rejected by the existing result-coverage validation. + None -> pure ()) + resultSecurityIds + validateStockBalanceSuccessor : CapTableMaps -> Text -> @@ -1215,15 +1235,7 @@ validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQu assertMsg (label <> " balance quantity mismatch for " <> balanceSecurityId <> ": expected " <> show expectedQuantity <> ", found " <> show balanceIssuance.issuance_data.quantity) (balanceIssuance.issuance_data.quantity == expectedQuantity) - case sourceIssuanceData.share_numbers_issued of - [] -> pure () - sourceShareNumberRanges -> - assertMsg - (label <> " balance result share numbers are outside the source issuance range for " <> balanceSecurityId) - (not (null balanceIssuance.issuance_data.share_numbers_issued) && - all - (shareNumberRangeCoveredByRanges sourceShareNumberRanges) - balanceIssuance.issuance_data.share_numbers_issued) + validateStockSuccessorShareNumberRanges label sourceIssuanceData balanceIssuance.issuance_data None -> assertMsg (label <> " balance stock issuance not found: " <> balanceSecurityId) False @@ -1297,16 +1309,20 @@ validateStockBalanceSuccessors eventIndex maps = do mapA_ (\transferCid -> do transfer <- fetch transferCid - case transfer.transfer_data.balance_security_id of - Some _ -> case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - validateStockTerminalBalanceSuccessor - eventIndex maps ("Stock transfer " <> transfer.transfer_data.id) - sourceIssuance.issuance_data transfer.transfer_data.date transfer.transfer_data.quantity - transfer.transfer_data.balance_security_id - None -> assertMsg ("Stock transfer source issuance not found: " <> transfer.transfer_data.security_id) False - None -> pure ()) + case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let label = "Stock transfer " <> transfer.transfer_data.id + validateStockResultShareNumberRanges + maps label sourceIssuance.issuance_data transfer.transfer_data.resulting_security_ids + case transfer.transfer_data.balance_security_id of + Some _ -> + validateStockTerminalBalanceSuccessor + eventIndex maps label + sourceIssuance.issuance_data transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id + None -> pure () + None -> assertMsg ("Stock transfer source issuance not found: " <> transfer.transfer_data.security_id) False) (Map.values maps.stock_transfers) addOptionalDecimal : Optional Decimal -> Optional Decimal -> Optional Decimal From 178798ca65027c2ed4319f66e4aa98a58d9a1f51 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:43:41 -0400 Subject: [PATCH 40/50] chore: refresh schema constraints DAR --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 128bdc00..add3d45a 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebcfbcfd25684efd2166e3972e8d4ad348c6a857aa466bce7dc234aa84b8af80 -size 2894258 +oid sha256:f6c43302dd253504c7ae5b47f6185370d6a5516f422d3625ed24ba099d65d8c7 +size 2894277 diff --git a/dars/dars.lock b/dars/dars.lock index 88467b1f..29cf968d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "ebcfbcfd25684efd2166e3972e8d4ad348c6a857aa466bce7dc234aa84b8af80", - "size": 2894258, + "sha256": "f6c43302dd253504c7ae5b47f6185370d6a5516f422d3625ed24ba099d65d8c7", + "size": 2894277, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From f183decf74ac45a5ae615520f5a2589bb7a34ce4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:59:57 -0400 Subject: [PATCH 41/50] test: align ceiling fixtures with schema constraints --- .../TestStockClassAuthorizedShares.daml | 28 ------------------- .../0.0.11/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index fcf326a7..44c45a2d 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1173,34 +1173,6 @@ testStockClassWithNonRatioConversionRightRejected = script do edits = [] deletes = [] -testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script do - TestOcp{issuer, cap_table} <- setupTestOcp - - (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MIXED_COMMON" 100.0) - let preferredClassId = "SC_CONVERSION_MIXED_SOURCE" - let ppsRight = (stockClassConversionRightTo commonClassId) with - conversion_mechanism = OcfConversionMechanismPpsBasedConversion - ratio = None - reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - let preferredClass = (limitedStockClass preferredClassId 100.0) with - conversion_rights = [ppsRight, stockClassConversionRightTo commonClassId] - (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MIXED" "Conversion Mixed Rights Holder" - capTableIssued <- createStockIssuanceWith issuer capTableReady - ((defaultStockIssuance "TX_CONVERSION_MIXED_SOURCE" "SEC_CONVERSION_MIXED_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) - let conversion = stockConversion "TX_CONVERSION_MIXED" "SEC_CONVERSION_MIXED_SOURCE" 50.0 - let resultIssuance = (defaultStockIssuance "TX_CONVERSION_MIXED_RESULT" "TX_CONVERSION_MIXED_RESULT" stakeholderId commonClassId) with quantity = 50.0 - - _ <- submit issuer do - exerciseCmd capTableIssued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockConversion conversion - , CT.OcfCreateStockIssuance resultIssuance - ] - edits = [] - deletes = [] - pure () - testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 5e915d85..8d1e8682 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a6a2383199cbf04434aac01b955fca8e5bfb7fb00abfa502845234bd82b1c73 -size 2955293 +oid sha256:566902c446347e9e9c1a7ec674b184ed133db07f504f6851675e68f4c538627e +size 2976057 diff --git a/dars/dars.lock b/dars/dars.lock index 3f4c74eb..89a1d063 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "5a6a2383199cbf04434aac01b955fca8e5bfb7fb00abfa502845234bd82b1c73", - "size": 2955293, + "sha256": "566902c446347e9e9c1a7ec674b184ed133db07f504f6851675e68f4c538627e", + "size": 2976057, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From e724cdf1b286c5477bf640fff601a60bb11f4ce7 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 19:53:35 -0400 Subject: [PATCH 42/50] fix: match OCF result array cardinality --- .../OCF/EquityCompensationExercise.daml | 5 ++- .../OCF/EquityCompensationRelease.daml | 4 +-- .../OpenCapTable/OCF/StockReissuance.daml | 4 +-- .../OpenCapTable/OCF/WarrantExercise.daml | 4 +-- .../TestUniqueSecurityArrays.daml | 31 ++++++------------- .../0.0.11/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- 7 files changed, 22 insertions(+), 34 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index c3ca01bc..ffb4968a 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,5 +61,4 @@ validateOcfEquityCompensationExerciseData d = validateOptionalText d.consideration_text && -- Arrays element validation validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - validateUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml index 9aba976d..33f4dd60 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationRelease where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -63,5 +63,5 @@ validateEquityCompensationReleaseOcfData d = d.quantity > 0.0 && validateOcfMonetary d.release_price && validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids && + validateNonEmptyStringsArray d.resulting_security_ids && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml index 4cb53366..f5199d16 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockReissuance where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/reissuance/StockReissuance.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,4 @@ validateStockReissuanceOcfData d = validateOptionalText d.split_transaction_id && -- Arrays element validation (resulting_security_ids required per Reissuance base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml index 60e605c3..3ce8fecf 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantExercise where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/exercise/WarrantExercise.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -66,4 +66,4 @@ validateWarrantExerciseOcfData d = validateOptionalDecimal d.quantity && -- Arrays element validation (resulting_security_ids required per Exercise base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 0619db75..6ac5af24 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -74,29 +74,18 @@ testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = security_ids = ["SSEC-1", "SSEC-1"] reason_text = None -testUniqueSecurityArrays_ResultingSecurityValidatorsRejectDuplicateIds = script do - assertInvalid "warrant exercise" $ +testUniqueSecurityArrays_NonCardinalityResultArraysAllowEmpty = script do + assertMsg "warrant exercise empty result array should be valid" $ validateWarrantExerciseOcfData WarrantExerciseOcfData with - id = "TX_WARRANT_EXERCISE_DUP" + id = "TX_WARRANT_EXERCISE_EMPTY" date = defaultTestDate security_id = "WSEC-EXERCISE" trigger_id = "TRIGGER-1" comments = [] - resulting_security_ids = duplicateResultingSecurityIds + resulting_security_ids = [] consideration_text = None quantity = Some 1.0 - assertInvalid "equity compensation exercise" $ - validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with - id = "TX_EC_EXERCISE_DUP" - date = defaultTestDate - quantity = 1.0 - security_id = "ESEC-EXERCISE" - comments = [] - resulting_security_ids = duplicateResultingSecurityIds - consideration_text = None - -testUniqueSecurityArrays_EquityCompensationExerciseAllowsEmptyResultingSecurityIds = script do assertMsg "equity compensation exercise empty result array should be valid" $ validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with id = "TX_EC_EXERCISE_EMPTY" @@ -107,24 +96,24 @@ testUniqueSecurityArrays_EquityCompensationExerciseAllowsEmptyResultingSecurityI resulting_security_ids = [] consideration_text = None - assertInvalid "equity compensation release" $ + assertMsg "equity compensation release empty result array should be valid" $ validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_DUP" + id = "TX_EC_RELEASE_EMPTY" date = defaultTestDate quantity = 1.0 release_price = OcfMonetary with amount = 1.0, currency = "USD" security_id = "ESEC-RELEASE" settlement_date = defaultTestDate comments = [] - resulting_security_ids = duplicateResultingSecurityIds + resulting_security_ids = [] consideration_text = None - assertInvalid "stock reissuance" $ + assertMsg "stock reissuance empty result array should be valid" $ validateStockReissuanceOcfData StockReissuanceOcfData with - id = "TX_STOCK_REISSUANCE_DUP" + id = "TX_STOCK_REISSUANCE_EMPTY" date = defaultTestDate security_id = "SSEC-REISSUANCE" comments = [] - resulting_security_ids = duplicateResultingSecurityIds + resulting_security_ids = [] reason_text = None split_transaction_id = None diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 2a3ea632..34792833 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c3ea55aa8b8b1c2add9bf389c4f6afa9a654e6381ea3d4977df8c723e4b0099 -size 3073040 +oid sha256:a78b43381a2e7af1684ca07e32cc84453f5814419adc08a91d0af2bbc90c8641 +size 3072573 diff --git a/dars/dars.lock b/dars/dars.lock index 71818b5b..15bbd327 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "2c3ea55aa8b8b1c2add9bf389c4f6afa9a654e6381ea3d4977df8c723e4b0099", - "size": 3073040, + "sha256": "a78b43381a2e7af1684ca07e32cc84453f5814419adc08a91d0af2bbc90c8641", + "size": 3072573, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From c70d96f57ef2898605e013aa64308ccefde0aae2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:18:34 -0400 Subject: [PATCH 43/50] chore: refresh deterministic schema constraints DAR --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 34792833..9f82cc10 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a78b43381a2e7af1684ca07e32cc84453f5814419adc08a91d0af2bbc90c8641 -size 3072573 +oid sha256:bbb09f0e4d9847ed251c5c94aaad96a1286ef860a788c76d61df6cfab517ba25 +size 3083368 diff --git a/dars/dars.lock b/dars/dars.lock index b3f94933..4db7cdeb 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "a78b43381a2e7af1684ca07e32cc84453f5814419adc08a91d0af2bbc90c8641", - "size": 3072573, + "sha256": "bbb09f0e4d9847ed251c5c94aaad96a1286ef860a788c76d61df6cfab517ba25", + "size": 3083368, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:44:11.166Z", "networks": [] From 01d8cc58f12d640c7fdda7864b68c839c7806bd1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:15:53 -0400 Subject: [PATCH 44/50] fix: preserve successor share number lineage --- Test/daml/OpenCapTable/TestStockIssuance.daml | 99 +++++++++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 41 ++++++-- 4 files changed, 139 insertions(+), 11 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index ffeb1002..351cc324 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -4,6 +4,8 @@ import Fairmint.OpenCapTable.OCF.StockIssuance import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -432,6 +434,103 @@ testStockIssuanceShareNumberRangesRejectUntrackedPartialTransferRemainder = scri deletes = [] pure () +testStockIssuanceShareNumberRangesPreservedAcrossReissuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_SOURCE" "SEC-SHARE-NUMBERS-REISSUANCE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + let reissuanceDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let reissuance = StockReissuanceOcfData with + id = "TX_STOCK_SHARE_NUMBERS_REISSUANCE" + date = reissuanceDate + security_id = sourceIssuance.security_id + resulting_security_ids = ["SEC-SHARE-NUMBERS-REISSUANCE-FIRST", "SEC-SHARE-NUMBERS-REISSUANCE-SECOND"] + comments = [] + reason_text = Some "Replacement certificates" + split_transaction_id = None + let firstResult = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_FIRST" "SEC-SHARE-NUMBERS-REISSUANCE-FIRST" stakeholderId classId) with + date = reissuanceDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let invalidSecondResult = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_SECOND" "SEC-SHARE-NUMBERS-REISSUANCE-SECOND" stakeholderId classId) with + date = reissuanceDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance reissuance + , CT.OcfCreateStockIssuance firstResult + , CT.OcfCreateStockIssuance invalidSecondResult + ] + edits = [] + deletes = [] + + let validSecondResult = invalidSecondResult with share_numbers_issued = [shareRange 51.0 100.0] + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance reissuance + , CT.OcfCreateStockIssuance firstResult + , CT.OcfCreateStockIssuance validSecondResult + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesPreservedAcrossConsolidation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let firstSource = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION_FIRST" "SEC-SHARE-NUMBERS-CONSOLIDATION-FIRST" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let secondSource = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION_SECOND" "SEC-SHARE-NUMBERS-CONSOLIDATION-SECOND" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + sources <- submit issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance firstSource + , CT.OcfCreateStockIssuance secondSource + ] + edits = [] + deletes = [] + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultSecurityId = "SEC-SHARE-NUMBERS-CONSOLIDATED" + let consolidation = StockConsolidationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION" + date = consolidationDate + security_ids = [firstSource.security_id, secondSource.security_id] + resulting_security_id = resultSecurityId + comments = [] + reason_text = Some "Combined certificates" + let resultIssuance ranges = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATED" resultSecurityId stakeholderId classId) with + date = consolidationDate + quantity = 100.0 + share_numbers_issued = ranges + + submitMustFail issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1_001.0 1_100.0]) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1.0 100.0]) + ] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesCannotOverlapBeforeRetraction = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 9f82cc10..5784443b 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbb09f0e4d9847ed251c5c94aaad96a1286ef860a788c76d61df6cfab517ba25 -size 3083368 +oid sha256:5594fde4b16403eff5b24f54b0362f18725732435b67a133dbd96b9019f60849 +size 3092163 diff --git a/dars/dars.lock b/dars/dars.lock index a923ee8d..6ba1715f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "bbb09f0e4d9847ed251c5c94aaad96a1286ef860a788c76d61df6cfab517ba25", - "size": 3083368, + "sha256": "5594fde4b16403eff5b24f54b0362f18725732435b67a133dbd96b9019f60849", + "size": 3092163, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:44:11.166Z", + "uploadedAt": "2026-07-11T02:13:47Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 468ca0f1..0456298d 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -1926,6 +1926,23 @@ validateStockResultShareNumberRanges maps label sourceIssuanceData resultSecurit None -> pure ()) resultSecurityIds +validateStockConsolidationShareNumberRanges : + Text -> + [StockIssuanceOcfData] -> + StockIssuanceOcfData -> + Update () +validateStockConsolidationShareNumberRanges label sourceIssuanceData resultIssuanceData = + let sourceShareNumberRanges = concatMap (\source -> source.share_numbers_issued) sourceIssuanceData + in case sourceShareNumberRanges of + [] -> pure () + _ -> + assertMsg + (label <> " share numbers are outside the consolidated source ranges for " <> resultIssuanceData.security_id) + (not (null resultIssuanceData.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + resultIssuanceData.share_numbers_issued) + stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = case balanceSecurityId of @@ -2187,6 +2204,14 @@ validateStockReissuanceResultIssuances eventIndex maps = if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids then do sourceIssuance <- fetch sourceIssuanceCid + case reissuance.reissuance_data.split_transaction_id of + Some _ -> pure () + None -> + validateStockResultShareNumberRanges + maps + label + sourceIssuance.issuance_data + reissuance.reissuance_data.resulting_security_ids let targetSortKey = (reissuance.reissuance_data.date, 3, 0) sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex @@ -2226,8 +2251,8 @@ validateStockConsolidationResultIssuances eventIndex maps = (label <> " result date mismatch for " <> consolidation.consolidation_data.resulting_security_id <> ": expected " <> show consolidation.consolidation_data.date <> ", found " <> show resultIssuance.issuance_data.date) (resultIssuance.issuance_data.date == consolidation.consolidation_data.date) let targetSortKey = (consolidation.consolidation_data.date, 3, 3) - sourceQuantity <- foldlA - (\total sourceSecurityId -> + (sourceQuantity, sourceIssuanceData) <- foldlA + (\(total, sourceData) sourceSecurityId -> case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid @@ -2239,12 +2264,16 @@ validateStockConsolidationResultIssuances eventIndex maps = maps sourceIssuance.issuance_data targetSortKey - pure (total + quantity) + pure (total + quantity, sourceIssuance.issuance_data :: sourceData) None -> do assertMsg (label <> " source stock issuance not found: " <> sourceSecurityId) False - pure total) - 0.0 + pure (total, sourceData)) + (0.0, []) consolidation.consolidation_data.security_ids + validateStockConsolidationShareNumberRanges + label + sourceIssuanceData + resultIssuance.issuance_data resultQuantity <- stockIssuanceQuantityAtSortKey maps resultIssuance.issuance_data targetSortKey assertMsg (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultQuantity) From 39d9d29d2bb4ae024d427a2a09d51c13f7a97935 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:53:47 -0400 Subject: [PATCH 45/50] fix: align ceiling tests with schema constraints --- .../TestStockClassAuthorizedShares.daml | 15 ++++++--------- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 7e7f4f1d..ef246e42 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1244,17 +1244,15 @@ testStockClassWithNonRatioConversionRightRejected = script do edits = [] deletes = [] -testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script do +testStockConversionSelectsMatchingRatioRight = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MIXED_COMMON" 100.0) let preferredClassId = "SC_CONVERSION_MIXED_SOURCE" - let ppsRight = (stockClassConversionRightTo commonClassId) with - conversion_mechanism = OcfConversionMechanismPpsBasedConversion - ratio = None - reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + let nonMatchingRight = (stockClassConversionRightTo commonClassId) with + ratio = Some (OcfRatio with numerator = 2.0, denominator = 1.0) let preferredClass = (limitedStockClass preferredClassId 100.0) with - conversion_rights = [ppsRight, stockClassConversionRightTo commonClassId] + conversion_rights = [nonMatchingRight, stockClassConversionRightTo commonClassId] (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MIXED" "Conversion Mixed Rights Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady @@ -1479,7 +1477,7 @@ testConvertibleConversionValidatesConvertedUnitBounds = script do edits = [] deletes = [] -testRatioConvertibleConversionEnforcesCeiling = script do +testRatioConvertibleConversionMechanismRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RATIO" 100.0) @@ -1525,7 +1523,7 @@ testRatioConvertibleConversionEnforcesCeiling = script do boundaryIssuance.security_id (Some 50.0) ["SEC-CONVERTIBLE-RATIO-BOUNDARY-RESULT"] - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateConvertibleIssuance boundaryIssuance @@ -1540,7 +1538,6 @@ testRatioConvertibleConversionEnforcesCeiling = script do ] edits = [] deletes = [] - pure () testIndeterminateConvertibleConversionRequiresMaterializedResult = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 5784443b..3735c086 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5594fde4b16403eff5b24f54b0362f18725732435b67a133dbd96b9019f60849 -size 3092163 +oid sha256:8a958870ed4876b3b962cb57d5ce762b45e2931a1898350f9dcefc17ba7518a2 +size 3106745 diff --git a/dars/dars.lock b/dars/dars.lock index 66a1976e..48c8d5d1 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "5594fde4b16403eff5b24f54b0362f18725732435b67a133dbd96b9019f60849", - "size": 3092163, + "sha256": "8a958870ed4876b3b962cb57d5ce762b45e2931a1898350f9dcefc17ba7518a2", + "size": 3106745, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:13:47Z", + "uploadedAt": "2026-07-11T02:47:53Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 5384e4223208a6a66582ee23073018fb29ac395e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:14:44 -0400 Subject: [PATCH 46/50] chore: refresh schema constraints DAR after parent fix --- dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 3735c086..d4a65698 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a958870ed4876b3b962cb57d5ce762b45e2931a1898350f9dcefc17ba7518a2 -size 3106745 +oid sha256:fd634e7744655e32abd35a84a7862b7848daf1671359eaaca2927bc25c7cd9ab +size 3106698 diff --git a/dars/dars.lock b/dars/dars.lock index e6698372..1dcc6299 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "8a958870ed4876b3b962cb57d5ce762b45e2931a1898350f9dcefc17ba7518a2", - "size": 3106745, + "sha256": "fd634e7744655e32abd35a84a7862b7848daf1671359eaaca2927bc25c7cd9ab", + "size": 3106698, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:47:53Z", + "uploadedAt": "2026-07-11T03:12:10Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From b283bd8757865e1a62fa901f464cb7b4798ed3a2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:57:27 -0400 Subject: [PATCH 47/50] fix: reject duplicate successor security ids --- .../OCF/ConvertibleConversion.daml | 5 +- .../OCF/EquityCompensationExercise.daml | 5 +- .../OCF/EquityCompensationRelease.daml | 3 +- .../OpenCapTable/OCF/StockConversion.daml | 5 +- .../OpenCapTable/OCF/StockReissuance.daml | 5 +- .../OpenCapTable/OCF/WarrantExercise.daml | 5 +- .../TestUniqueSecurityArrays.daml | 69 +++++++++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- 9 files changed, 89 insertions(+), 18 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml index 853fc544..2a412278 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/ConvertibleConversion.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.OpenCapTable.Types.Conversion (OcfCapitalizationDefinition, validateOcfCapitalizationDefinition) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -74,5 +74,4 @@ validateConvertibleConversionOcfData d = optional True validateOcfCapitalizationDefinition d.capitalization_definition && -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateNonEmptyUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index ffb4968a..c3ca01bc 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,4 +61,5 @@ validateOcfEquityCompensationExerciseData d = validateOptionalText d.consideration_text && -- Arrays element validation validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml index 33f4dd60..56ddca61 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationRelease where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -64,4 +64,5 @@ validateEquityCompensationReleaseOcfData d = validateOcfMonetary d.release_price && validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml index 5bce698b..0110c385 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/StockConversion.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,5 +62,4 @@ validateStockConversionOcfData d = validateOptionalText d.balance_security_id && -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateNonEmptyUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml index f5199d16..e385236a 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockReissuance where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/reissuance/StockReissuance.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,5 @@ validateStockReissuanceOcfData d = validateOptionalText d.split_transaction_id && -- Arrays element validation (resulting_security_ids required per Reissuance base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml index 3ce8fecf..548bd337 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantExercise where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/exercise/WarrantExercise.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -66,4 +66,5 @@ validateWarrantExerciseOcfData d = validateOptionalDecimal d.quantity && -- Arrays element validation (resulting_security_ids required per Exercise base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 6ac5af24..3f2275cc 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -1,10 +1,12 @@ module OpenCapTable.TestUniqueSecurityArrays where +import Fairmint.OpenCapTable.OCF.ConvertibleConversion import Fairmint.OpenCapTable.OCF.ConvertibleTransfer import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.OCF.StockReissuance import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.OCF.WarrantExercise @@ -74,6 +76,73 @@ testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = security_ids = ["SSEC-1", "SSEC-1"] reason_text = None +testUniqueSecurityArrays_NonTransferValidatorsRejectDuplicateResultingSecurityIds = script do + assertInvalid "warrant exercise" $ + validateWarrantExerciseOcfData WarrantExerciseOcfData with + id = "TX_WARRANT_EXERCISE_DUP" + date = defaultTestDate + security_id = "WSEC-EXERCISE" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + quantity = Some 1.0 + + assertInvalid "equity compensation exercise" $ + validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with + id = "TX_EC_EXERCISE_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-EXERCISE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "equity compensation release" $ + validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_DUP" + date = defaultTestDate + quantity = 1.0 + release_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = "ESEC-RELEASE" + settlement_date = defaultTestDate + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "stock reissuance" $ + validateStockReissuanceOcfData StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_DUP" + date = defaultTestDate + security_id = "SSEC-REISSUANCE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + reason_text = None + split_transaction_id = None + + assertInvalid "stock conversion" $ + validateStockConversionOcfData StockConversionOcfData with + id = "TX_STOCK_CONVERSION_DUP" + date = defaultTestDate + quantity_converted = 1.0 + security_id = "SSEC-CONVERSION" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + + assertInvalid "convertible conversion" $ + validateConvertibleConversionOcfData ConvertibleConversionOcfData with + id = "TX_CONVERTIBLE_CONVERSION_DUP" + date = defaultTestDate + reason_text = "Test duplicate successor IDs" + security_id = "CSEC-CONVERSION" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + capitalization_definition = None + quantity_converted = None + testUniqueSecurityArrays_NonCardinalityResultArraysAllowEmpty = script do assertMsg "warrant exercise empty result array should be valid" $ validateWarrantExerciseOcfData WarrantExerciseOcfData with diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index d4a65698..e4cdda3f 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd634e7744655e32abd35a84a7862b7848daf1671359eaaca2927bc25c7cd9ab -size 3106698 +oid sha256:c325b0409f8dcc7b5dbbe5bfc0578bc8516c3887d4b48a1433d09cf315104ffe +size 3107019 diff --git a/dars/dars.lock b/dars/dars.lock index 1dcc6299..1bbdbcae 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "fd634e7744655e32abd35a84a7862b7848daf1671359eaaca2927bc25c7cd9ab", - "size": 3106698, + "sha256": "c325b0409f8dcc7b5dbbe5bfc0578bc8516c3887d4b48a1433d09cf315104ffe", + "size": 3107019, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T03:12:10Z", + "uploadedAt": "2026-07-11T03:56:23Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From e387336476c20201715af78950cc24358194c18d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 00:02:40 -0400 Subject: [PATCH 48/50] fix: preserve conversion result cardinality --- .../OCF/ConvertibleConversion.daml | 5 ++-- .../OpenCapTable/OCF/StockConversion.daml | 5 ++-- .../TestUniqueSecurityArrays.daml | 23 +++++++++++++++++++ .../0.0.11/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 ++--- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml index 2a412278..a0b0e564 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/ConvertibleConversion.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.OpenCapTable.Types.Conversion (OcfCapitalizationDefinition, validateOcfCapitalizationDefinition) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -74,4 +74,5 @@ validateConvertibleConversionOcfData d = optional True validateOcfCapitalizationDefinition d.capitalization_definition && -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml index 0110c385..2433d243 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/StockConversion.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,5 @@ validateStockConversionOcfData d = validateOptionalText d.balance_security_id && -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyUniqueStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml index 3f2275cc..466f0911 100644 --- a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -186,3 +186,26 @@ testUniqueSecurityArrays_NonCardinalityResultArraysAllowEmpty = script do resulting_security_ids = [] reason_text = None split_transaction_id = None + + assertMsg "stock conversion empty result array should be valid" $ + validateStockConversionOcfData StockConversionOcfData with + id = "TX_STOCK_CONVERSION_EMPTY" + date = defaultTestDate + quantity_converted = 1.0 + security_id = "SSEC-CONVERSION" + comments = [] + resulting_security_ids = [] + balance_security_id = None + + assertMsg "convertible conversion empty result array should be valid" $ + validateConvertibleConversionOcfData ConvertibleConversionOcfData with + id = "TX_CONVERTIBLE_CONVERSION_EMPTY" + date = defaultTestDate + reason_text = "Test schema-valid empty successor array" + security_id = "CSEC-CONVERSION" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = None diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index e4cdda3f..3f3e970c 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c325b0409f8dcc7b5dbbe5bfc0578bc8516c3887d4b48a1433d09cf315104ffe -size 3107019 +oid sha256:57a568c669ca298df5f39baec849eb2026acbf4aa6dabc5779eb777e9cd7426b +size 3107297 diff --git a/dars/dars.lock b/dars/dars.lock index 1bbdbcae..20594323 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "c325b0409f8dcc7b5dbbe5bfc0578bc8516c3887d4b48a1433d09cf315104ffe", - "size": 3107019, + "sha256": "57a568c669ca298df5f39baec849eb2026acbf4aa6dabc5779eb777e9cd7426b", + "size": 3107297, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T03:56:23Z", + "uploadedAt": "2026-07-11T04:01:53Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 66c1b9bf4be98f5cca64fea5be68ee134703fbc5 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 01:04:11 -0400 Subject: [PATCH 49/50] fix: preserve stock lifecycle continuity --- Test/daml/OpenCapTable/TestStockIssuance.daml | 73 +++++++++++++++ .../OpenCapTable/TestStockReissuance.daml | 19 ++++ .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 89 +++++++++++++++++-- 5 files changed, 177 insertions(+), 12 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 351cc324..87aa5577 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -214,6 +214,28 @@ testStockIssuanceShareNumberRangesCanReuseFullyCancelledRange = script do deletes = [] pure () +testStockIssuanceShareNumberRangesRequireBalanceForPartialCancellation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_PARTIAL_CANCEL_SOURCE" "SEC-SHARE-NUMBERS-PARTIAL-CANCEL-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_PARTIAL_CANCEL" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 40.0 + reason_text = "Partial cancellation without a traceable remainder" + security_id = sourceIssuance.security_id + comments = [] + balance_security_id = None] + edits = [] + deletes = [] + testStockIssuanceShareNumberRangesFollowPartialTransferBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -531,6 +553,57 @@ testStockIssuanceShareNumberRangesPreservedAcrossConsolidation = script do deletes = [] pure () +testStockIssuanceShareNumberRangesAllowMixedConsolidationAsUnnumbered = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let numberedSource = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION_NUMBERED" "SEC-STOCK-MIXED-CONSOLIDATION-NUMBERED" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let unnumberedSource = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION_UNNUMBERED" "SEC-STOCK-MIXED-CONSOLIDATION-UNNUMBERED" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [] + sources <- submit issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance numberedSource + , CT.OcfCreateStockIssuance unnumberedSource + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + resultSecurityId = "SEC-STOCK-MIXED-CONSOLIDATION-RESULT" + consolidation = StockConsolidationOcfData with + id = "TX_STOCK_MIXED_CONSOLIDATION" + date = consolidationDate + security_ids = [numberedSource.security_id, unnumberedSource.security_id] + resulting_security_id = resultSecurityId + comments = [] + reason_text = Some "Combine numbered and unnumbered certificates" + resultIssuance ranges = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION-RESULT" resultSecurityId stakeholderId classId) with + date = consolidationDate + quantity = 100.0 + share_numbers_issued = ranges + + submitMustFail issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1.0 100.0]) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance []) + ] + edits = [] + deletes = [] + pure () + testStockIssuanceShareNumberRangesCannotOverlapBeforeRetraction = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockReissuance.daml b/Test/daml/OpenCapTable/TestStockReissuance.daml index 77a23781..84477672 100644 --- a/Test/daml/OpenCapTable/TestStockReissuance.daml +++ b/Test/daml/OpenCapTable/TestStockReissuance.daml @@ -107,3 +107,22 @@ testStockReissuance_MultipleResulting = script do edits = [] deletes = [] pure () + +testStockReissuance_RequiresResultingSecurityOnCapTable = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SSEC-EMPTY-REISSUANCE" + result <- createStockIssuance issuer capTableCid securityId 100.0 + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_EMPTY_RESULT" + date = DT.time (DA.date 2025 Sep 01) 0 0 0 + security_id = securityId + resulting_security_ids = [] + comments = [] + reason_text = Some "Invalid reissuance without a successor" + split_transaction_id = None] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index 3f3e970c..bf4202fd 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57a568c669ca298df5f39baec849eb2026acbf4aa6dabc5779eb777e9cd7426b -size 3107297 +oid sha256:ec7f0d74a651e8d4dc82f7efe4dcca056e8d04a0222ae8700b98c253e01863b7 +size 3113926 diff --git a/dars/dars.lock b/dars/dars.lock index 20594323..faa5092c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "57a568c669ca298df5f39baec849eb2026acbf4aa6dabc5779eb777e9cd7426b", - "size": 3107297, + "sha256": "ec7f0d74a651e8d4dc82f7efe4dcca056e8d04a0222ae8700b98c253e01863b7", + "size": 3113926, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T04:01:53Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 5ae7d91f..95ae0167 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1981,15 +1981,22 @@ validateStockConsolidationShareNumberRanges : Update () validateStockConsolidationShareNumberRanges label sourceIssuanceData resultIssuanceData = let sourceShareNumberRanges = concatMap (\source -> source.share_numbers_issued) sourceIssuanceData - in case sourceShareNumberRanges of - [] -> pure () - _ -> + anyNumberedSource = any (\source -> not (null source.share_numbers_issued)) sourceIssuanceData + allSourcesNumbered = all (\source -> not (null source.share_numbers_issued)) sourceIssuanceData + in if not anyNumberedSource + then pure () + else if allSourcesNumbered + then + assertMsg + (label <> " share numbers are outside the consolidated source ranges for " <> resultIssuanceData.security_id) + (not (null resultIssuanceData.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + resultIssuanceData.share_numbers_issued) + else assertMsg - (label <> " share numbers are outside the consolidated source ranges for " <> resultIssuanceData.security_id) - (not (null resultIssuanceData.share_numbers_issued) && - all - (shareNumberRangeCoveredByRanges sourceShareNumberRanges) - resultIssuanceData.share_numbers_issued) + (label <> " cannot assign share numbers because not every consolidated source has share number ranges") + (null resultIssuanceData.share_numbers_issued) stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = @@ -1997,6 +2004,68 @@ stockReductionSortKey eventDate balanceSecurityId = Some _ -> (eventDate, 2, 1) None -> (eventDate, 1, 0) +validateNumberedStockPartialReduction : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Decimal -> + Optional Text -> + Update () +validateNumberedStockPartialReduction eventIndex maps label sourceSecurityId eventDate reductionQuantity balanceSecurityId = + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + when (not (null sourceIssuance.issuance_data.share_numbers_issued) && isNone balanceSecurityId) do + currentQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + (stockReductionSortKey eventDate balanceSecurityId) + assertMsg + (label <> " partially reduces a numbered stock issuance without a balance security") + (reductionQuantity >= currentQuantity) + None -> pure () + +validateNumberedStockPartialReductions : StockClassShareEventIndex -> CapTableMaps -> Update () +validateNumberedStockPartialReductions eventIndex maps = do + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id repurchase.repurchase_data.date repurchase.repurchase_data.quantity + repurchase.repurchase_data.balance_security_id) + (Map.values maps.stock_repurchases) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id conversion.conversion_data.date conversion.conversion_data.quantity_converted + conversion.conversion_data.balance_security_id) + (Map.values maps.stock_conversions) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id) + (Map.values maps.stock_transfers) + stockClassQuantityAtSortKey : CapTableMaps -> Text -> @@ -2246,6 +2315,9 @@ validateStockReissuanceResultIssuances eventIndex maps = (\reissuanceCid -> do reissuance <- fetch reissuanceCid let label = "Stock reissuance " <> reissuance.reissuance_data.id + assertMsg + (label <> " has no resulting stock issuance") + (not (null reissuance.reissuance_data.resulting_security_ids)) assertNoPartialResultingStockIssuanceCoverage label maps reissuance.reissuance_data.resulting_security_ids case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> @@ -2883,6 +2955,7 @@ validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots = validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () validateStockIssuanceShareNumberRanges maps = do eventIndex <- stockClassShareEventIndex maps + validateNumberedStockPartialReductions eventIndex maps lots <- stockIssuanceShareNumberLots eventIndex maps validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots From e9c512bf0ab2c5772450856ccf0864938973a706 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 10:48:01 -0400 Subject: [PATCH 50/50] fix: validate optional monetary issuance fields --- .../OpenCapTable/OCF/StockIssuance.daml | 2 +- .../OpenCapTable/OCF/WarrantIssuance.daml | 1 + Test/daml/OpenCapTable/TestStockIssuance.daml | 26 +++++++ .../OpenCapTable/TestWarrantIssuance.daml | 70 ++++++++++++++----- .../0.0.11/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- 6 files changed, 84 insertions(+), 25 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml index a38d698f..c08016ad 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml @@ -97,6 +97,7 @@ validateOcfStockIssuanceData d = validateNonEmptyStringsArray d.stock_legend_ids && -- Required amounts validateOcfMonetary d.share_price && + optional True validateOcfMonetary d.cost_basis && d.quantity > (0.0) && -- Optional Text values, if present, must be non-empty validateOptionalText d.consideration_text && @@ -108,4 +109,3 @@ validateOcfStockIssuanceData d = -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/enums/StockIssuanceType.schema.json data OcfStockIssuanceType = OcfStockIssuanceRSA | OcfStockIssuanceFounders deriving (Eq, Show) - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml index 8c12c063..a70f783b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml @@ -92,6 +92,7 @@ validateOcfWarrantIssuanceDataTx d = -- Required amounts and lists -- quantity is optional per schema; exercise_price is optional per schema validateOcfMonetary d.purchase_price && + optional True validateOcfMonetary d.exercise_price && validateOptionalText d.consideration_text && -- exercise_triggers: OCF schema requires array but no minItems, so empty array is valid all validateOcfConversionTrigger d.exercise_triggers && diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 87aa5577..0dd5e1cc 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -92,6 +92,32 @@ testStockIssuance_OptionalsSome = script do _ <- createStockIssuanceWith issuer referenceResult.updatedCapTableCid issuance pure () +testStockIssuance_InvalidCostBasisRejectedOnCreate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_STOCK_INVALID_COST_BASIS_CREATE" "SEC-STOCK-INVALID-COST-BASIS-CREATE" stakeholderId classId) with + cost_basis = Some (OcfMonetary with amount = 100.0, currency = "usd") + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testStockIssuance_InvalidCostBasisRejectedOnEdit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_STOCK_INVALID_COST_BASIS_EDIT" "SEC-STOCK-INVALID-COST-BASIS-EDIT" stakeholderId classId) with + cost_basis = Some (OcfMonetary with amount = 100.0, currency = "USD") + capTableIssued <- createStockIssuanceWith issuer capTableReady issuance + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (issuance with + cost_basis = Some (OcfMonetary with amount = -1.0, currency = "USD"))] + deletes = [] + testStockIssuanceShareNumberRangesCanMatchQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantIssuance.daml b/Test/daml/OpenCapTable/TestWarrantIssuance.daml index 19d9a8a3..ffd74da1 100644 --- a/Test/daml/OpenCapTable/TestWarrantIssuance.daml +++ b/Test/daml/OpenCapTable/TestWarrantIssuance.daml @@ -12,28 +12,31 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +defaultWarrantIssuance : Text -> Text -> WarrantIssuanceOcfData +defaultWarrantIssuance issuanceId securityId = WarrantIssuanceOcfData with + id = issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = securityId + custom_id = "W-" <> issuanceId + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = [ocfTriggerElectiveAtWill] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = [] + createWarrantIssuance issuer capTableCid = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISSUANCE_1" - date = DT.time (DA.date 2024 Jan 01) 0 0 0 - security_id = "WSEC-1" - custom_id = "W-1" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - quantity = Some 100.0 - quantity_source = Some OcfQuantityUnspecified - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - purchase_price = OcfMonetary with amount = 0.0, currency = "USD" - exercise_triggers = [ ocfTriggerElectiveAtWill ] - warrant_expiration_date = None - vesting_terms_id = None - vestings = [] - comments = []] + creates = [CT.OcfCreateWarrantIssuance (defaultWarrantIssuance "TX_WARRANT_ISSUANCE_1" "WSEC-1")] edits = [] deletes = [] @@ -49,6 +52,35 @@ testCreateAndArchiveWarrantIssuanceBuiltIn = script do _ <- createWarrantIssuance issuer capTableCid pure () +testWarrantIssuance_InvalidExercisePriceRejectedOnCreate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let issuance = (defaultWarrantIssuance "TX_WARRANT_INVALID_EXERCISE_PRICE_CREATE" "WSEC-INVALID-EXERCISE-PRICE-CREATE") with + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "usd") + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + +testWarrantIssuance_InvalidExercisePriceRejectedOnEdit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let issuance = defaultWarrantIssuance "TX_WARRANT_INVALID_EXERCISE_PRICE_EDIT" "WSEC-INVALID-EXERCISE-PRICE-EDIT" + created <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd created.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (issuance with + exercise_price = Some (OcfMonetary with amount = -1.0, currency = "USD"))] + deletes = [] + -- Base helper already covers None/empty where applicable -- Optionals Some and arrays non-empty diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar index bf4202fd..29b804d6 100644 --- a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec7f0d74a651e8d4dc82f7efe4dcca056e8d04a0222ae8700b98c253e01863b7 -size 3113926 +oid sha256:23fc9940acf68156ce2594da14b0783ce38c9195d9aaf15e113fb5124f05d8ee +size 3114372 diff --git a/dars/dars.lock b/dars/dars.lock index faa5092c..5e6343c1 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,10 +19,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { - "sha256": "ec7f0d74a651e8d4dc82f7efe4dcca056e8d04a0222ae8700b98c253e01863b7", - "size": 3113926, + "sha256": "23fc9940acf68156ce2594da14b0783ce38c9195d9aaf15e113fb5124f05d8ee", + "size": 3114372, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T04:01:53Z", + "uploadedAt": "2026-07-11T14:46:26Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": {