Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a808014
Validate generated DAML reader payloads
HardlyDifficult Jul 10, 2026
5392517
Merge branch 'codex/typed-ocf-manifest' into codex/decoder-backed-spe…
HardlyDifficult Jul 10, 2026
f6ddf01
Merge remote-tracking branch 'origin/codex/typed-ocf-manifest' into c…
HardlyDifficult Jul 10, 2026
deed110
Handle omitted vesting condition portions
HardlyDifficult Jul 10, 2026
3518bf5
Merge typed OCF manifest
HardlyDifficult Jul 10, 2026
695b49e
Merge typed OCF manifest and harden decoder readers
HardlyDifficult Jul 11, 2026
e6c50df
Merge commit '7495009427daad10db24f4da0e813aafb85fe72d' into codex/de…
HardlyDifficult Jul 11, 2026
6e8fd95
Merge commit 'e272e9ceaa34dec551334391a0126ded29f0e0b9' into codex/de…
HardlyDifficult Jul 11, 2026
64ddcd6
Merge centralized trigger validation into decoder readers
HardlyDifficult Jul 11, 2026
a97d077
Merge commit '764832f3ed99594ac29a9a788e003a78eaae1fa3' into codex/de…
HardlyDifficult Jul 11, 2026
ffc94dd
Merge commit '9b96159700a507cd40e760a654e65218760632e1' into codex/de…
HardlyDifficult Jul 11, 2026
d7621ca
Auto-fix: build changes
github-actions[bot] Jul 11, 2026
2e097d0
Merge commit 'c918488aa9a613e26e78c6e36670f4974407266c' into codex/de…
HardlyDifficult Jul 11, 2026
02e89ce
Merge commit 'da0ee8f820af88953496336b3538bb49dcf9c703' into codex/de…
HardlyDifficult Jul 11, 2026
cb32617
Merge commit '1bada08d73af25d45279e0e2d88b412749442821' into codex/de…
HardlyDifficult Jul 11, 2026
d947afa
Merge commit '5d4c72d3d270fa88f2790055df7a694abd031088' into codex/de…
HardlyDifficult Jul 11, 2026
3b3494a
Merge commit '94bcbc62b1cfab3b19b518bc2dda6964fb622662' into codex/de…
HardlyDifficult Jul 11, 2026
4a9ecdf
Merge remote-tracking branch 'origin/codex/typed-ocf-manifest' into c…
HardlyDifficult Jul 11, 2026
634104d
Merge commit '6da7e1fc8dd5e7469860aa1fe789b231a0a02850' into codex/de…
HardlyDifficult Jul 11, 2026
818053e
Merge commit '44101eb152664bb4923f3707fae1ebe4f0c91eed' into codex/de…
HardlyDifficult Jul 11, 2026
de48abd
Harden generated DAML reader boundaries
HardlyDifficult Jul 12, 2026
97603e8
Merge commit 'e54dbe3d36e303e87dd6d18ebafeec4bedee9ef6' into codex/de…
HardlyDifficult Jul 12, 2026
c448a7e
Merge commit 'cd3e7dabe494b86b17a0ee1ded20456616650ab0' into codex/de…
HardlyDifficult Jul 12, 2026
894ec46
fix: harden generated entity read boundaries
HardlyDifficult Jul 12, 2026
31e0c06
Merge commit '505805fee7a4c3c16626b8dc5d1e3eb2b1e97b24' into codex/de…
HardlyDifficult Jul 12, 2026
bad975b
Merge remote-tracking branch 'origin/codex/typed-ocf-manifest' into c…
HardlyDifficult Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 314 additions & 0 deletions src/functions/OpenCapTable/capTable/damlEntityData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import { Fairmint } from '@fairmint/open-captable-protocol-daml-js';
import { OcpErrorCodes, OcpParseError } from '../../../errors';
import { extractGeneratedCreateArgumentData, requireGeneratedRecord } from '../../../utils/generatedDamlValidation';
import { initialSharesAuthorizedFromDaml } from '../../../utils/typeConversions';
import { projectDamlIssuerDataToNative } from '../issuer/getIssuerAsOcf';
import { parseDamlSafeInteger } from '../shared/damlIntegers';
import { assertCanonicalJsonGraph, requireDecimalString } from '../shared/ocfValues';
import { damlOptionalStakeholderRelationshipToNative } from '../stakeholderRelationshipChangeEvent/damlToOcf';
import {
ENTITY_DATA_FIELD_MAP,
ENTITY_TEMPLATE_ID_MAP,
isOcfEntityType,
type DamlDataTypeFor,
type OcfEntityType,
} from './batchTypes';
import { decodeLosslessGeneratedDamlValue } from './damlCodecLosslessness';

interface EntityDataCodec<T> {
readonly decoder: {
run(
input: unknown
):
| { readonly ok: true; readonly result: T }
| { readonly ok: false; readonly error: { readonly at: string; readonly message: string } };
runWithException(input: unknown): T;
};
encode(value: T): unknown;
}

type EntityDataDecoder<T> = (input: unknown) => T;

type EntityDataDecoderMap = {
readonly [EntityType in OcfEntityType]: EntityDataDecoder<DamlDataTypeFor<EntityType>>;
};

/** Reject untyped entity kinds before indexing any correlated entity registry. */
export function assertSupportedOcfEntityType(value: unknown, source: string): asserts value is OcfEntityType {
if (typeof value === 'string' && isOcfEntityType(value)) return;
const detail = typeof value === 'string' ? `: ${value.slice(0, 128)}` : '';
throw new OcpParseError(`Unsupported OCF entity type${detail}`, {
source,
code: OcpErrorCodes.UNKNOWN_ENTITY_TYPE,
classification: 'unsupported_entity_type',
context: { receivedType: value === null ? 'null' : typeof value },
});
}

function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function hasOwnField(record: object, field: PropertyKey): boolean {
return Object.prototype.hasOwnProperty.call(record, field);
}

function preflightSemanticDamlEntityData(entityType: OcfEntityType, input: unknown): void {
if (!isRecord(input)) return;

if (entityType === 'issuer') {
projectDamlIssuerDataToNative(input);
} else if (entityType === 'stakeholderRelationshipChangeEvent') {
const rootPath = `damlToOcf.${entityType}`;
const relationship = requireGeneratedRecord(input, rootPath);
for (const field of ['relationship_started', 'relationship_ended'] as const) {
damlOptionalStakeholderRelationshipToNative(relationship[field], `${rootPath}.${field}`);
}
} else if (entityType === 'stockClass' && hasOwnField(input, 'initial_shares_authorized')) {
initialSharesAuthorizedFromDaml(input.initial_shares_authorized, 'stockClass.initial_shares_authorized');
} else if (entityType === 'convertibleIssuance' && hasOwnField(input, 'seniority')) {
parseDamlSafeInteger(input.seniority, 'convertibleIssuance.seniority');
} else if (
entityType === 'convertibleConversion' &&
hasOwnField(input, 'quantity_converted') &&
input.quantity_converted !== null
) {
requireDecimalString(input.quantity_converted, 'convertibleConversion.quantity_converted');
}
}

function createEntityDataDecoder<const EntityType extends OcfEntityType>(
entityType: EntityType,
codec: EntityDataCodec<DamlDataTypeFor<EntityType>>
): EntityDataDecoder<DamlDataTypeFor<EntityType>> {
return (input) => {
assertCanonicalJsonGraph(input, entityType);
preflightSemanticDamlEntityData(entityType, input);
const options = {
rootPath: entityType,
description: entityType,
decodeSource: `damlToOcf.${entityType}`,
context: { entityType, expectedTemplateId: ENTITY_TEMPLATE_ID_MAP[entityType] },
} as const;
const decoded = codec.decoder.run(input);
if (!decoded.ok) {
const { at: decoderPath, message: decoderMessage } = decoded.error;
throw new OcpParseError(`Invalid generated DAML ${entityType} at ${decoderPath}: ${decoderMessage}`, {
source: `damlToOcf.${entityType}`,
code: OcpErrorCodes.SCHEMA_MISMATCH,
classification: 'invalid_generated_daml_data',
context: {
...options.context,
decoderPath,
decoderMessage,
},
});
}

return decodeLosslessGeneratedDamlValue(codec, decoded.result, options, { raw: input });
};
}

/** Generated payload decoders keyed by their correlated SDK entity kind. */
const ENTITY_DATA_DECODER_MAP = {
convertibleAcceptance: createEntityDataDecoder(
'convertibleAcceptance',
Fairmint.OpenCapTable.OCF.ConvertibleAcceptance.ConvertibleAcceptanceOcfData
),
convertibleCancellation: createEntityDataDecoder(
'convertibleCancellation',
Fairmint.OpenCapTable.OCF.ConvertibleCancellation.ConvertibleCancellationOcfData
),
convertibleConversion: createEntityDataDecoder(
'convertibleConversion',
Fairmint.OpenCapTable.OCF.ConvertibleConversion.ConvertibleConversionOcfData
),
convertibleIssuance: createEntityDataDecoder(
'convertibleIssuance',
Fairmint.OpenCapTable.OCF.ConvertibleIssuance.ConvertibleIssuanceOcfData
),
convertibleRetraction: createEntityDataDecoder(
'convertibleRetraction',
Fairmint.OpenCapTable.OCF.ConvertibleRetraction.ConvertibleRetractionOcfData
),
convertibleTransfer: createEntityDataDecoder(
'convertibleTransfer',
Fairmint.OpenCapTable.OCF.ConvertibleTransfer.ConvertibleTransferOcfData
),
document: createEntityDataDecoder('document', Fairmint.OpenCapTable.OCF.Document.DocumentOcfData),
equityCompensationAcceptance: createEntityDataDecoder(
'equityCompensationAcceptance',
Fairmint.OpenCapTable.OCF.EquityCompensationAcceptance.EquityCompensationAcceptanceOcfData
),
equityCompensationCancellation: createEntityDataDecoder(
'equityCompensationCancellation',
Fairmint.OpenCapTable.OCF.EquityCompensationCancellation.EquityCompensationCancellationOcfData
),
equityCompensationExercise: createEntityDataDecoder(
'equityCompensationExercise',
Fairmint.OpenCapTable.OCF.EquityCompensationExercise.EquityCompensationExerciseOcfData
),
equityCompensationIssuance: createEntityDataDecoder(
'equityCompensationIssuance',
Fairmint.OpenCapTable.OCF.EquityCompensationIssuance.EquityCompensationIssuanceOcfData
),
equityCompensationRelease: createEntityDataDecoder(
'equityCompensationRelease',
Fairmint.OpenCapTable.OCF.EquityCompensationRelease.EquityCompensationReleaseOcfData
),
equityCompensationRepricing: createEntityDataDecoder(
'equityCompensationRepricing',
Fairmint.OpenCapTable.OCF.EquityCompensationRepricing.EquityCompensationRepricingOcfData
),
equityCompensationRetraction: createEntityDataDecoder(
'equityCompensationRetraction',
Fairmint.OpenCapTable.OCF.EquityCompensationRetraction.EquityCompensationRetractionOcfData
),
equityCompensationTransfer: createEntityDataDecoder(
'equityCompensationTransfer',
Fairmint.OpenCapTable.OCF.EquityCompensationTransfer.EquityCompensationTransferOcfData
),
issuer: createEntityDataDecoder('issuer', Fairmint.OpenCapTable.OCF.Issuer.IssuerOcfData),
issuerAuthorizedSharesAdjustment: createEntityDataDecoder(
'issuerAuthorizedSharesAdjustment',
Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment.IssuerAuthorizedSharesAdjustmentOcfData
),
stakeholder: createEntityDataDecoder('stakeholder', Fairmint.OpenCapTable.OCF.Stakeholder.StakeholderOcfData),
stakeholderRelationshipChangeEvent: createEntityDataDecoder(
'stakeholderRelationshipChangeEvent',
Fairmint.OpenCapTable.OCF.StakeholderRelationshipChangeEvent.StakeholderRelationshipChangeEventOcfData
),
stakeholderStatusChangeEvent: createEntityDataDecoder(
'stakeholderStatusChangeEvent',
Fairmint.OpenCapTable.OCF.StakeholderStatusChangeEvent.StakeholderStatusChangeEventOcfData
),
stockAcceptance: createEntityDataDecoder(
'stockAcceptance',
Fairmint.OpenCapTable.OCF.StockAcceptance.StockAcceptanceOcfData
),
stockCancellation: createEntityDataDecoder(
'stockCancellation',
Fairmint.OpenCapTable.OCF.StockCancellation.StockCancellationOcfData
),
stockClass: createEntityDataDecoder('stockClass', Fairmint.OpenCapTable.OCF.StockClass.StockClassOcfData),
stockClassAuthorizedSharesAdjustment: createEntityDataDecoder(
'stockClassAuthorizedSharesAdjustment',
Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment.StockClassAuthorizedSharesAdjustmentOcfData
),
stockClassConversionRatioAdjustment: createEntityDataDecoder(
'stockClassConversionRatioAdjustment',
Fairmint.OpenCapTable.OCF.StockClassConversionRatioAdjustment.StockClassConversionRatioAdjustmentOcfData
),
stockClassSplit: createEntityDataDecoder(
'stockClassSplit',
Fairmint.OpenCapTable.OCF.StockClassSplit.StockClassSplitOcfData
),
stockConsolidation: createEntityDataDecoder(
'stockConsolidation',
Fairmint.OpenCapTable.OCF.StockConsolidation.StockConsolidationOcfData
),
stockConversion: createEntityDataDecoder(
'stockConversion',
Fairmint.OpenCapTable.OCF.StockConversion.StockConversionOcfData
),
stockIssuance: createEntityDataDecoder('stockIssuance', Fairmint.OpenCapTable.OCF.StockIssuance.StockIssuanceOcfData),
stockLegendTemplate: createEntityDataDecoder(
'stockLegendTemplate',
Fairmint.OpenCapTable.OCF.StockLegendTemplate.StockLegendTemplateOcfData
),
stockPlan: createEntityDataDecoder('stockPlan', Fairmint.OpenCapTable.OCF.StockPlan.StockPlanOcfData),
stockPlanPoolAdjustment: createEntityDataDecoder(
'stockPlanPoolAdjustment',
Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment.StockPlanPoolAdjustmentOcfData
),
stockPlanReturnToPool: createEntityDataDecoder(
'stockPlanReturnToPool',
Fairmint.OpenCapTable.OCF.StockPlanReturnToPool.StockPlanReturnToPoolOcfData
),
stockReissuance: createEntityDataDecoder(
'stockReissuance',
Fairmint.OpenCapTable.OCF.StockReissuance.StockReissuanceOcfData
),
stockRepurchase: createEntityDataDecoder(
'stockRepurchase',
Fairmint.OpenCapTable.OCF.StockRepurchase.StockRepurchaseOcfData
),
stockRetraction: createEntityDataDecoder(
'stockRetraction',
Fairmint.OpenCapTable.OCF.StockRetraction.StockRetractionOcfData
),
stockTransfer: createEntityDataDecoder('stockTransfer', Fairmint.OpenCapTable.OCF.StockTransfer.StockTransferOcfData),
valuation: createEntityDataDecoder('valuation', Fairmint.OpenCapTable.OCF.Valuation.ValuationOcfData),
vestingAcceleration: createEntityDataDecoder(
'vestingAcceleration',
Fairmint.OpenCapTable.OCF.VestingAcceleration.VestingAccelerationOcfData
),
vestingEvent: createEntityDataDecoder('vestingEvent', Fairmint.OpenCapTable.OCF.VestingEvent.VestingEventOcfData),
vestingStart: createEntityDataDecoder('vestingStart', Fairmint.OpenCapTable.OCF.VestingStart.VestingStartOcfData),
vestingTerms: createEntityDataDecoder('vestingTerms', Fairmint.OpenCapTable.OCF.VestingTerms.VestingTermsOcfData),
warrantAcceptance: createEntityDataDecoder(
'warrantAcceptance',
Fairmint.OpenCapTable.OCF.WarrantAcceptance.WarrantAcceptanceOcfData
),
warrantCancellation: createEntityDataDecoder(
'warrantCancellation',
Fairmint.OpenCapTable.OCF.WarrantCancellation.WarrantCancellationOcfData
),
warrantExercise: createEntityDataDecoder(
'warrantExercise',
Fairmint.OpenCapTable.OCF.WarrantExercise.WarrantExerciseOcfData
),
warrantIssuance: createEntityDataDecoder(
'warrantIssuance',
Fairmint.OpenCapTable.OCF.WarrantIssuance.WarrantIssuanceOcfData
),
warrantRetraction: createEntityDataDecoder(
'warrantRetraction',
Fairmint.OpenCapTable.OCF.WarrantRetraction.WarrantRetractionOcfData
),
warrantTransfer: createEntityDataDecoder(
'warrantTransfer',
Fairmint.OpenCapTable.OCF.WarrantTransfer.WarrantTransferOcfData
),
} as const satisfies EntityDataDecoderMap;

/** Extract the entity-specific data object from a ledger create argument. */
export function extractEntityData(entityType: OcfEntityType, createArgument: unknown): Record<string, unknown> {
assertSupportedOcfEntityType(entityType, 'damlToOcf.extractEntityData.entityType');
const rootPath = `damlToOcf.${entityType}.createArgument`;
const dataFieldName = ENTITY_DATA_FIELD_MAP[entityType];
if (
entityType === 'document' ||
entityType === 'issuer' ||
entityType === 'stockClassConversionRatioAdjustment' ||
entityType === 'stockPlan' ||
entityType === 'vestingTerms'
) {
return extractGeneratedCreateArgumentData(createArgument, rootPath, {
dataField: dataFieldName,
});
}

return extractGeneratedCreateArgumentData(createArgument, rootPath, {
dataField: dataFieldName,
missingDataFieldSource: rootPath,
});
}

/** Decode unknown ledger JSON into the exact generated DAML payload for an entity kind. */
export function decodeDamlEntityData<const EntityType extends OcfEntityType>(
entityType: EntityType,
input: unknown
): DamlDataTypeFor<EntityType>;
export function decodeDamlEntityData(entityType: OcfEntityType, input: unknown): DamlDataTypeFor<OcfEntityType> {
assertSupportedOcfEntityType(entityType, 'damlToOcf.decodeDamlEntityData.entityType');
return ENTITY_DATA_DECODER_MAP[entityType](input);
}

/** Extract and decode one correlated generated DAML entity payload from a ledger create argument. */
export function extractAndDecodeDamlEntityData<const EntityType extends OcfEntityType>(
entityType: EntityType,
createArgument: unknown
): DamlDataTypeFor<EntityType> {
return decodeDamlEntityData(entityType, extractEntityData(entityType, createArgument));
}
Loading