From e4d7070043d82f578c355cc0d09a4e118d01c65c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 05:51:18 -0400 Subject: [PATCH 1/6] Validate acceptance reader payloads --- .../getConvertibleAcceptanceAsOcf.ts | 46 +--- .../getEquityCompensationAcceptanceAsOcf.ts | 46 +--- .../getStockAcceptanceAsOcf.ts | 46 +--- .../getWarrantAcceptanceAsOcf.ts | 46 +--- test/declarations/acceptanceReaders.types.ts | 58 +++++ test/functions/acceptanceReaders.test.ts | 224 ++++++++++++++++++ test/types/acceptanceReaders.types.ts | 58 +++++ 7 files changed, 368 insertions(+), 156 deletions(-) create mode 100644 test/declarations/acceptanceReaders.types.ts create mode 100644 test/functions/acceptanceReaders.test.ts create mode 100644 test/types/acceptanceReaders.types.ts diff --git a/src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf.ts b/src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf.ts index 8c13f6b6..e20fa140 100644 --- a/src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf.ts +++ b/src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf.ts @@ -1,16 +1,15 @@ import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; -import { OcpErrorCodes, OcpParseError } from '../../../errors'; import type { OcfConvertibleAcceptance } from '../../../types'; import type { GetByContractIdParams } from '../../../types/common'; -import { damlTimeToDateString } from '../../../utils/typeConversions'; +import { ENTITY_TEMPLATE_ID_MAP } from '../capTable/batchTypes'; +import { extractAndDecodeDamlEntityData } from '../capTable/damlEntityData'; import { readSingleContract } from '../shared/singleContractRead'; +import { damlConvertibleAcceptanceToNative } from './convertibleAcceptanceDataToDaml'; /** * OCF Convertible Acceptance event with object_type discriminator. */ -export interface OcfConvertibleAcceptanceEvent extends OcfConvertibleAcceptance { - object_type: 'TX_CONVERTIBLE_ACCEPTANCE'; -} +export type OcfConvertibleAcceptanceEvent = OcfConvertibleAcceptance; export type GetConvertibleAcceptanceAsOcfParams = GetByContractIdParams; @@ -19,23 +18,6 @@ export interface GetConvertibleAcceptanceAsOcfResult { contractId: string; } -/** - * DAML ConvertibleAcceptance contract createArgument structure. - */ -interface ConvertibleAcceptanceCreateArgument { - acceptance_data: { - id: string; - date: string; - security_id: string; - comments: string[]; - }; -} - -function hasConvertibleAcceptanceData(arg: unknown): arg is ConvertibleAcceptanceCreateArgument { - const record = arg as { acceptance_data?: unknown }; - return typeof record.acceptance_data === 'object' && record.acceptance_data !== null; -} - /** * Retrieve a Convertible Acceptance contract and convert to OCF format. * @@ -49,24 +31,10 @@ export async function getConvertibleAcceptanceAsOcf( ): Promise { const { createArgument } = await readSingleContract(client, params, { operation: 'getConvertibleAcceptanceAsOcf', + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP.convertibleAcceptance, }); - if (!hasConvertibleAcceptanceData(createArgument)) { - throw new OcpParseError('ConvertibleAcceptance data not found in contract create argument', { - source: 'ConvertibleAcceptance.createArgument', - code: OcpErrorCodes.SCHEMA_MISMATCH, - }); - } - - const contract = createArgument; - const data = contract.acceptance_data; - - const event: OcfConvertibleAcceptanceEvent = { - object_type: 'TX_CONVERTIBLE_ACCEPTANCE', - id: data.id, - date: damlTimeToDateString(data.date), - security_id: data.security_id, - ...(Array.isArray(data.comments) && data.comments.length ? { comments: data.comments } : {}), - }; + const data = extractAndDecodeDamlEntityData('convertibleAcceptance', createArgument); + const event = damlConvertibleAcceptanceToNative(data); return { event, contractId: params.contractId }; } diff --git a/src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf.ts b/src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf.ts index af14203f..fb665acb 100644 --- a/src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf.ts +++ b/src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf.ts @@ -1,16 +1,15 @@ import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; -import { OcpErrorCodes, OcpParseError } from '../../../errors'; import type { OcfEquityCompensationAcceptance } from '../../../types'; import type { GetByContractIdParams } from '../../../types/common'; -import { damlTimeToDateString } from '../../../utils/typeConversions'; +import { ENTITY_TEMPLATE_ID_MAP } from '../capTable/batchTypes'; +import { extractAndDecodeDamlEntityData } from '../capTable/damlEntityData'; import { readSingleContract } from '../shared/singleContractRead'; +import { damlEquityCompensationAcceptanceToNative } from './equityCompensationAcceptanceDataToDaml'; /** * OCF Equity Compensation Acceptance event with object_type discriminator. */ -export interface OcfEquityCompensationAcceptanceEvent extends OcfEquityCompensationAcceptance { - object_type: 'TX_EQUITY_COMPENSATION_ACCEPTANCE'; -} +export type OcfEquityCompensationAcceptanceEvent = OcfEquityCompensationAcceptance; export type GetEquityCompensationAcceptanceAsOcfParams = GetByContractIdParams; @@ -19,23 +18,6 @@ export interface GetEquityCompensationAcceptanceAsOcfResult { contractId: string; } -/** - * DAML EquityCompensationAcceptance contract createArgument structure. - */ -interface EquityCompensationAcceptanceCreateArgument { - acceptance_data: { - id: string; - date: string; - security_id: string; - comments: string[]; - }; -} - -function hasEquityCompensationAcceptanceData(arg: unknown): arg is EquityCompensationAcceptanceCreateArgument { - const record = arg as { acceptance_data?: unknown }; - return typeof record.acceptance_data === 'object' && record.acceptance_data !== null; -} - /** * Retrieve an Equity Compensation Acceptance contract and convert to OCF format. * @@ -49,24 +31,10 @@ export async function getEquityCompensationAcceptanceAsOcf( ): Promise { const { createArgument } = await readSingleContract(client, params, { operation: 'getEquityCompensationAcceptanceAsOcf', + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP.equityCompensationAcceptance, }); - if (!hasEquityCompensationAcceptanceData(createArgument)) { - throw new OcpParseError('EquityCompensationAcceptance data not found in contract create argument', { - source: 'EquityCompensationAcceptance.createArgument', - code: OcpErrorCodes.SCHEMA_MISMATCH, - }); - } - - const contract = createArgument; - const data = contract.acceptance_data; - - const event: OcfEquityCompensationAcceptanceEvent = { - object_type: 'TX_EQUITY_COMPENSATION_ACCEPTANCE', - id: data.id, - date: damlTimeToDateString(data.date), - security_id: data.security_id, - ...(Array.isArray(data.comments) && data.comments.length ? { comments: data.comments } : {}), - }; + const data = extractAndDecodeDamlEntityData('equityCompensationAcceptance', createArgument); + const event = damlEquityCompensationAcceptanceToNative(data); return { event, contractId: params.contractId }; } diff --git a/src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf.ts b/src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf.ts index 68f5359a..cd0ff2db 100644 --- a/src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf.ts +++ b/src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf.ts @@ -1,16 +1,15 @@ import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; -import { OcpErrorCodes, OcpParseError } from '../../../errors'; import type { OcfStockAcceptance } from '../../../types'; import type { GetByContractIdParams } from '../../../types/common'; -import { damlTimeToDateString } from '../../../utils/typeConversions'; +import { ENTITY_TEMPLATE_ID_MAP } from '../capTable/batchTypes'; +import { extractAndDecodeDamlEntityData } from '../capTable/damlEntityData'; import { readSingleContract } from '../shared/singleContractRead'; +import { damlStockAcceptanceToNative } from './stockAcceptanceDataToDaml'; /** * OCF Stock Acceptance event with object_type discriminator. */ -export interface OcfStockAcceptanceEvent extends OcfStockAcceptance { - object_type: 'TX_STOCK_ACCEPTANCE'; -} +export type OcfStockAcceptanceEvent = OcfStockAcceptance; export type GetStockAcceptanceAsOcfParams = GetByContractIdParams; @@ -19,23 +18,6 @@ export interface GetStockAcceptanceAsOcfResult { contractId: string; } -/** - * DAML StockAcceptance contract createArgument structure. - */ -interface StockAcceptanceCreateArgument { - acceptance_data: { - id: string; - date: string; - security_id: string; - comments: string[]; - }; -} - -function hasStockAcceptanceData(arg: unknown): arg is StockAcceptanceCreateArgument { - const record = arg as { acceptance_data?: unknown }; - return typeof record.acceptance_data === 'object' && record.acceptance_data !== null; -} - /** * Retrieve a Stock Acceptance contract and convert to OCF format. * @@ -49,24 +31,10 @@ export async function getStockAcceptanceAsOcf( ): Promise { const { createArgument } = await readSingleContract(client, params, { operation: 'getStockAcceptanceAsOcf', + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP.stockAcceptance, }); - if (!hasStockAcceptanceData(createArgument)) { - throw new OcpParseError('StockAcceptance data not found in contract create argument', { - source: 'StockAcceptance.createArgument', - code: OcpErrorCodes.SCHEMA_MISMATCH, - }); - } - - const contract = createArgument; - const data = contract.acceptance_data; - - const event: OcfStockAcceptanceEvent = { - object_type: 'TX_STOCK_ACCEPTANCE', - id: data.id, - date: damlTimeToDateString(data.date), - security_id: data.security_id, - ...(Array.isArray(data.comments) && data.comments.length ? { comments: data.comments } : {}), - }; + const data = extractAndDecodeDamlEntityData('stockAcceptance', createArgument); + const event = damlStockAcceptanceToNative(data); return { event, contractId: params.contractId }; } diff --git a/src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf.ts b/src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf.ts index 0c4516ff..d1bedeff 100644 --- a/src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf.ts +++ b/src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf.ts @@ -1,16 +1,15 @@ import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; -import { OcpErrorCodes, OcpParseError } from '../../../errors'; import type { OcfWarrantAcceptance } from '../../../types'; import type { GetByContractIdParams } from '../../../types/common'; -import { damlTimeToDateString } from '../../../utils/typeConversions'; +import { ENTITY_TEMPLATE_ID_MAP } from '../capTable/batchTypes'; +import { extractAndDecodeDamlEntityData } from '../capTable/damlEntityData'; import { readSingleContract } from '../shared/singleContractRead'; +import { damlWarrantAcceptanceToNative } from './warrantAcceptanceDataToDaml'; /** * OCF Warrant Acceptance event with object_type discriminator. */ -export interface OcfWarrantAcceptanceEvent extends OcfWarrantAcceptance { - object_type: 'TX_WARRANT_ACCEPTANCE'; -} +export type OcfWarrantAcceptanceEvent = OcfWarrantAcceptance; export type GetWarrantAcceptanceAsOcfParams = GetByContractIdParams; @@ -19,23 +18,6 @@ export interface GetWarrantAcceptanceAsOcfResult { contractId: string; } -/** - * DAML WarrantAcceptance contract createArgument structure. - */ -interface WarrantAcceptanceCreateArgument { - acceptance_data: { - id: string; - date: string; - security_id: string; - comments: string[]; - }; -} - -function hasWarrantAcceptanceData(arg: unknown): arg is WarrantAcceptanceCreateArgument { - const record = arg as { acceptance_data?: unknown }; - return typeof record.acceptance_data === 'object' && record.acceptance_data !== null; -} - /** * Retrieve a Warrant Acceptance contract and convert to OCF format. * @@ -49,24 +31,10 @@ export async function getWarrantAcceptanceAsOcf( ): Promise { const { createArgument } = await readSingleContract(client, params, { operation: 'getWarrantAcceptanceAsOcf', + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP.warrantAcceptance, }); - if (!hasWarrantAcceptanceData(createArgument)) { - throw new OcpParseError('WarrantAcceptance data not found in contract create argument', { - source: 'WarrantAcceptance.createArgument', - code: OcpErrorCodes.SCHEMA_MISMATCH, - }); - } - - const contract = createArgument; - const data = contract.acceptance_data; - - const event: OcfWarrantAcceptanceEvent = { - object_type: 'TX_WARRANT_ACCEPTANCE', - id: data.id, - date: damlTimeToDateString(data.date), - security_id: data.security_id, - ...(Array.isArray(data.comments) && data.comments.length ? { comments: data.comments } : {}), - }; + const data = extractAndDecodeDamlEntityData('warrantAcceptance', createArgument); + const event = damlWarrantAcceptanceToNative(data); return { event, contractId: params.contractId }; } diff --git a/test/declarations/acceptanceReaders.types.ts b/test/declarations/acceptanceReaders.types.ts new file mode 100644 index 00000000..4d8471d2 --- /dev/null +++ b/test/declarations/acceptanceReaders.types.ts @@ -0,0 +1,58 @@ +/** Built-declaration contracts for exact acceptance-reader result families. */ + +import type { GetConvertibleAcceptanceAsOcfResult } from '../../dist/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf'; +import type { GetEquityCompensationAcceptanceAsOcfResult } from '../../dist/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf'; +import type { GetStockAcceptanceAsOcfResult } from '../../dist/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf'; +import type { GetWarrantAcceptanceAsOcfResult } from '../../dist/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf'; +import type { + OcfConvertibleAcceptance, + OcfEquityCompensationAcceptance, + OcfStockAcceptance, + OcfWarrantAcceptance, +} from '../../dist/types/native'; + +type Assert = T; +type IsAny = 0 extends 1 & T ? true : false; +type IsExactly = [A] extends [B] ? ([B] extends [A] ? true : false) : false; + +type StockEvent = GetStockAcceptanceAsOcfResult['event']; +type ConvertibleEvent = GetConvertibleAcceptanceAsOcfResult['event']; +type EquityCompensationEvent = GetEquityCompensationAcceptanceAsOcfResult['event']; +type WarrantEvent = GetWarrantAcceptanceAsOcfResult['event']; + +const stockIsExact: Assert> = true; +const convertibleIsExact: Assert> = true; +const equityCompensationIsExact: Assert> = true; +const warrantIsExact: Assert> = true; + +const stockIsNotAny: Assert, false>> = true; +const convertibleIsNotAny: Assert, false>> = true; +const equityCompensationIsNotAny: Assert, false>> = true; +const warrantIsNotAny: Assert, false>> = true; + +declare const stockResult: GetStockAcceptanceAsOcfResult; +declare const convertibleResult: GetConvertibleAcceptanceAsOcfResult; +declare const equityCompensationResult: GetEquityCompensationAcceptanceAsOcfResult; +declare const warrantResult: GetWarrantAcceptanceAsOcfResult; + +// @ts-expect-error built stock acceptance cannot be used as warrant acceptance +const wrongWarrant: OcfWarrantAcceptance = stockResult.event; +// @ts-expect-error built convertible acceptance cannot be used as stock acceptance +const wrongStock: OcfStockAcceptance = convertibleResult.event; +// @ts-expect-error built equity-compensation acceptance cannot be used as convertible acceptance +const wrongConvertible: OcfConvertibleAcceptance = equityCompensationResult.event; +// @ts-expect-error built warrant acceptance cannot be used as equity-compensation acceptance +const wrongEquityCompensation: OcfEquityCompensationAcceptance = warrantResult.event; + +void stockIsExact; +void convertibleIsExact; +void equityCompensationIsExact; +void warrantIsExact; +void stockIsNotAny; +void convertibleIsNotAny; +void equityCompensationIsNotAny; +void warrantIsNotAny; +void wrongWarrant; +void wrongStock; +void wrongConvertible; +void wrongEquityCompensation; diff --git a/test/functions/acceptanceReaders.test.ts b/test/functions/acceptanceReaders.test.ts new file mode 100644 index 00000000..c68e75a5 --- /dev/null +++ b/test/functions/acceptanceReaders.test.ts @@ -0,0 +1,224 @@ +/** Direct ledger-reader contracts shared by all four OCF acceptance families. */ + +import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; +import { OcpContractError, OcpErrorCodes, OcpParseError } from '../../src/errors'; +import { + ENTITY_DATA_FIELD_MAP, + ENTITY_TEMPLATE_ID_MAP, + type OcfEntityType, +} from '../../src/functions/OpenCapTable/capTable/batchTypes'; +import { convertibleAcceptanceDataToDaml } from '../../src/functions/OpenCapTable/convertibleAcceptance/convertibleAcceptanceDataToDaml'; +import { getConvertibleAcceptanceAsOcf } from '../../src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf'; +import { equityCompensationAcceptanceDataToDaml } from '../../src/functions/OpenCapTable/equityCompensationAcceptance/equityCompensationAcceptanceDataToDaml'; +import { getEquityCompensationAcceptanceAsOcf } from '../../src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf'; +import { getStockAcceptanceAsOcf } from '../../src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf'; +import { stockAcceptanceDataToDaml } from '../../src/functions/OpenCapTable/stockAcceptance/stockAcceptanceDataToDaml'; +import { getWarrantAcceptanceAsOcf } from '../../src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf'; +import { warrantAcceptanceDataToDaml } from '../../src/functions/OpenCapTable/warrantAcceptance/warrantAcceptanceDataToDaml'; +import type { + OcfConvertibleAcceptance, + OcfEquityCompensationAcceptance, + OcfStockAcceptance, + OcfWarrantAcceptance, +} from '../../src/types/native'; + +type AcceptanceEntityType = Extract< + OcfEntityType, + 'convertibleAcceptance' | 'equityCompensationAcceptance' | 'stockAcceptance' | 'warrantAcceptance' +>; +type AcceptanceEvent = + | OcfConvertibleAcceptance + | OcfEquityCompensationAcceptance + | OcfStockAcceptance + | OcfWarrantAcceptance; + +interface AcceptanceReaderCase { + readonly entityType: AcceptanceEntityType; + readonly contractId: string; + readonly validData: () => Record; + readonly expectedEvent: AcceptanceEvent; + readonly invoke: ( + client: LedgerJsonApiClient + ) => Promise<{ readonly event: AcceptanceEvent; readonly contractId: string }>; +} + +const acceptanceReaderCases: readonly AcceptanceReaderCase[] = [ + { + entityType: 'stockAcceptance', + contractId: 'stock-acceptance-cid', + validData: () => + stockAcceptanceDataToDaml({ + object_type: 'TX_STOCK_ACCEPTANCE', + id: 'stock-acceptance-1', + date: '2026-07-10', + security_id: 'stock-security-1', + comments: ['accepted'], + }), + expectedEvent: { + object_type: 'TX_STOCK_ACCEPTANCE', + id: 'stock-acceptance-1', + date: '2026-07-10', + security_id: 'stock-security-1', + comments: ['accepted'], + }, + invoke: async (client) => getStockAcceptanceAsOcf(client, { contractId: 'stock-acceptance-cid' }), + }, + { + entityType: 'convertibleAcceptance', + contractId: 'convertible-acceptance-cid', + validData: () => + convertibleAcceptanceDataToDaml({ + object_type: 'TX_CONVERTIBLE_ACCEPTANCE', + id: 'convertible-acceptance-1', + date: '2026-07-10', + security_id: 'convertible-security-1', + comments: ['accepted'], + }), + expectedEvent: { + object_type: 'TX_CONVERTIBLE_ACCEPTANCE', + id: 'convertible-acceptance-1', + date: '2026-07-10', + security_id: 'convertible-security-1', + comments: ['accepted'], + }, + invoke: async (client) => getConvertibleAcceptanceAsOcf(client, { contractId: 'convertible-acceptance-cid' }), + }, + { + entityType: 'equityCompensationAcceptance', + contractId: 'equity-compensation-acceptance-cid', + validData: () => + equityCompensationAcceptanceDataToDaml({ + object_type: 'TX_EQUITY_COMPENSATION_ACCEPTANCE', + id: 'equity-compensation-acceptance-1', + date: '2026-07-10', + security_id: 'equity-compensation-security-1', + comments: ['accepted'], + }), + expectedEvent: { + object_type: 'TX_EQUITY_COMPENSATION_ACCEPTANCE', + id: 'equity-compensation-acceptance-1', + date: '2026-07-10', + security_id: 'equity-compensation-security-1', + comments: ['accepted'], + }, + invoke: async (client) => + getEquityCompensationAcceptanceAsOcf(client, { contractId: 'equity-compensation-acceptance-cid' }), + }, + { + entityType: 'warrantAcceptance', + contractId: 'warrant-acceptance-cid', + validData: () => + warrantAcceptanceDataToDaml({ + object_type: 'TX_WARRANT_ACCEPTANCE', + id: 'warrant-acceptance-1', + date: '2026-07-10', + security_id: 'warrant-security-1', + comments: ['accepted'], + }), + expectedEvent: { + object_type: 'TX_WARRANT_ACCEPTANCE', + id: 'warrant-acceptance-1', + date: '2026-07-10', + security_id: 'warrant-security-1', + comments: ['accepted'], + }, + invoke: async (client) => getWarrantAcceptanceAsOcf(client, { contractId: 'warrant-acceptance-cid' }), + }, +]; + +function createMockClient( + testCase: AcceptanceReaderCase, + data: Record, + templateId: string = ENTITY_TEMPLATE_ID_MAP[testCase.entityType] +): LedgerJsonApiClient { + return { + getEventsByContractId: jest.fn().mockResolvedValue({ + created: { + createdEvent: { + contractId: testCase.contractId, + templateId, + createArgument: { + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: data, + }, + }, + }, + }), + } as unknown as LedgerJsonApiClient; +} + +describe('decoder-backed acceptance readers', () => { + it.each(acceptanceReaderCases)('$entityType returns the exact canonical event shape', async (testCase) => { + const client = createMockClient(testCase, testCase.validData()); + + await expect(testCase.invoke(client)).resolves.toEqual({ + event: testCase.expectedEvent, + contractId: testCase.contractId, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects malformed required fields', async (testCase) => { + for (const [field, malformedData] of [ + ['id', { ...testCase.validData(), id: 17 }], + [ + 'security_id', + Object.fromEntries(Object.entries(testCase.validData()).filter(([key]) => key !== 'security_id')), + ], + ] as const) { + const client = createMockClient(testCase, malformedData); + + try { + await testCase.invoke(client); + throw new Error(`Expected ${testCase.entityType} reader to reject malformed ${field}`); + } catch (error: unknown) { + expect(error).toBeInstanceOf(OcpParseError); + expect(error).toMatchObject({ + code: OcpErrorCodes.SCHEMA_MISMATCH, + context: { + entityType: testCase.entityType, + decoderPath: expect.any(String), + decoderMessage: expect.any(String), + }, + }); + const parseError = error as OcpParseError; + expect(`${String(parseError.context?.decoderPath)} ${String(parseError.context?.decoderMessage)}`).toContain( + field + ); + } + } + }); + + it.each(acceptanceReaderCases)('$entityType rejects malformed comment elements', async (testCase) => { + const client = createMockClient(testCase, { + ...testCase.validData(), + comments: ['valid comment', 17], + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + context: { + entityType: testCase.entityType, + decoderPath: expect.stringContaining('comments'), + decoderMessage: expect.any(String), + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects a contract from the wrong template', async (testCase) => { + const wrongTemplateId = ENTITY_TEMPLATE_ID_MAP.document; + const client = createMockClient(testCase, testCase.validData(), wrongTemplateId); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpContractError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + classification: 'module_entity_mismatch', + contractId: testCase.contractId, + templateId: wrongTemplateId, + context: { + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP[testCase.entityType], + actualTemplateId: wrongTemplateId, + }, + }); + await expect(testCase.invoke(client)).rejects.toBeInstanceOf(OcpContractError); + }); +}); diff --git a/test/types/acceptanceReaders.types.ts b/test/types/acceptanceReaders.types.ts new file mode 100644 index 00000000..26b63140 --- /dev/null +++ b/test/types/acceptanceReaders.types.ts @@ -0,0 +1,58 @@ +/** Compile-time contracts for exact acceptance-reader result families. */ + +import type { GetConvertibleAcceptanceAsOcfResult } from '../../src/functions/OpenCapTable/convertibleAcceptance/getConvertibleAcceptanceAsOcf'; +import type { GetEquityCompensationAcceptanceAsOcfResult } from '../../src/functions/OpenCapTable/equityCompensationAcceptance/getEquityCompensationAcceptanceAsOcf'; +import type { GetStockAcceptanceAsOcfResult } from '../../src/functions/OpenCapTable/stockAcceptance/getStockAcceptanceAsOcf'; +import type { GetWarrantAcceptanceAsOcfResult } from '../../src/functions/OpenCapTable/warrantAcceptance/getWarrantAcceptanceAsOcf'; +import type { + OcfConvertibleAcceptance, + OcfEquityCompensationAcceptance, + OcfStockAcceptance, + OcfWarrantAcceptance, +} from '../../src/types/native'; + +type Assert = T; +type IsAny = 0 extends 1 & T ? true : false; +type IsExactly = [A] extends [B] ? ([B] extends [A] ? true : false) : false; + +type StockEvent = GetStockAcceptanceAsOcfResult['event']; +type ConvertibleEvent = GetConvertibleAcceptanceAsOcfResult['event']; +type EquityCompensationEvent = GetEquityCompensationAcceptanceAsOcfResult['event']; +type WarrantEvent = GetWarrantAcceptanceAsOcfResult['event']; + +const stockIsExact: Assert> = true; +const convertibleIsExact: Assert> = true; +const equityCompensationIsExact: Assert> = true; +const warrantIsExact: Assert> = true; + +const stockIsNotAny: Assert, false>> = true; +const convertibleIsNotAny: Assert, false>> = true; +const equityCompensationIsNotAny: Assert, false>> = true; +const warrantIsNotAny: Assert, false>> = true; + +declare const stockResult: GetStockAcceptanceAsOcfResult; +declare const convertibleResult: GetConvertibleAcceptanceAsOcfResult; +declare const equityCompensationResult: GetEquityCompensationAcceptanceAsOcfResult; +declare const warrantResult: GetWarrantAcceptanceAsOcfResult; + +// @ts-expect-error stock acceptance cannot be used as warrant acceptance +const wrongWarrant: OcfWarrantAcceptance = stockResult.event; +// @ts-expect-error convertible acceptance cannot be used as stock acceptance +const wrongStock: OcfStockAcceptance = convertibleResult.event; +// @ts-expect-error equity-compensation acceptance cannot be used as convertible acceptance +const wrongConvertible: OcfConvertibleAcceptance = equityCompensationResult.event; +// @ts-expect-error warrant acceptance cannot be used as equity-compensation acceptance +const wrongEquityCompensation: OcfEquityCompensationAcceptance = warrantResult.event; + +void stockIsExact; +void convertibleIsExact; +void equityCompensationIsExact; +void warrantIsExact; +void stockIsNotAny; +void convertibleIsNotAny; +void equityCompensationIsNotAny; +void warrantIsNotAny; +void wrongWarrant; +void wrongStock; +void wrongConvertible; +void wrongEquityCompensation; From 8a38a04e490e74f9318e1dd1f6564644c8a4b45b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 00:33:39 -0400 Subject: [PATCH 2/6] fix: validate acceptance contract wrappers --- .../capTable/acceptanceContractData.ts | 142 ++++++++++ .../OpenCapTable/capTable/damlEntityData.ts | 11 +- test/batch/damlToOcfDispatcher.test.ts | 32 +++ test/functions/acceptanceReaders.test.ts | 253 +++++++++++++++++- 4 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 src/functions/OpenCapTable/capTable/acceptanceContractData.ts diff --git a/src/functions/OpenCapTable/capTable/acceptanceContractData.ts b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts new file mode 100644 index 00000000..45ec1a83 --- /dev/null +++ b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts @@ -0,0 +1,142 @@ +import { Fairmint } from '@fairmint/open-captable-protocol-daml-js'; +import { OcpErrorCodes, OcpParseError } from '../../../errors'; +import { ENTITY_TEMPLATE_ID_MAP, type OcfEntityType } from './batchTypes'; + +type AcceptanceEntityType = Extract< + OcfEntityType, + 'convertibleAcceptance' | 'equityCompensationAcceptance' | 'stockAcceptance' | 'warrantAcceptance' +>; + +export function isAcceptanceEntityType(entityType: OcfEntityType): entityType is AcceptanceEntityType { + return ( + entityType === 'convertibleAcceptance' || + entityType === 'equityCompensationAcceptance' || + entityType === 'stockAcceptance' || + entityType === 'warrantAcceptance' + ); +} + +interface AcceptanceCreateArgumentMap { + convertibleAcceptance: Fairmint.OpenCapTable.OCF.ConvertibleAcceptance.ConvertibleAcceptance; + equityCompensationAcceptance: Fairmint.OpenCapTable.OCF.EquityCompensationAcceptance.EquityCompensationAcceptance; + stockAcceptance: Fairmint.OpenCapTable.OCF.StockAcceptance.StockAcceptance; + warrantAcceptance: Fairmint.OpenCapTable.OCF.WarrantAcceptance.WarrantAcceptance; +} + +interface DecoderError { + readonly at: string; + readonly message: string; +} + +interface AcceptanceCreateArgumentDecoder { + run(input: unknown): { readonly ok: true; readonly result: T } | { readonly ok: false; readonly error: DecoderError }; +} + +type AcceptanceCreateArgumentDecoderMap = { + readonly [EntityType in AcceptanceEntityType]: AcceptanceCreateArgumentDecoder< + AcceptanceCreateArgumentMap[EntityType] + >; +}; + +/** Generated template decoders correlated with each supported acceptance family. */ +const ACCEPTANCE_CREATE_ARGUMENT_DECODER_MAP = { + convertibleAcceptance: Fairmint.OpenCapTable.OCF.ConvertibleAcceptance.ConvertibleAcceptance.decoder, + equityCompensationAcceptance: + Fairmint.OpenCapTable.OCF.EquityCompensationAcceptance.EquityCompensationAcceptance.decoder, + stockAcceptance: Fairmint.OpenCapTable.OCF.StockAcceptance.StockAcceptance.decoder, + warrantAcceptance: Fairmint.OpenCapTable.OCF.WarrantAcceptance.WarrantAcceptance.decoder, +} as const satisfies AcceptanceCreateArgumentDecoderMap; + +type AcceptanceDataFor = + AcceptanceCreateArgumentMap[EntityType]['acceptance_data']; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function hasOwnField(record: object, field: string): boolean { + return Object.prototype.hasOwnProperty.call(record, field); +} + +function ownField(record: Record, field: string): unknown { + return hasOwnField(record, field) ? record[field] : undefined; +} + +function acceptanceDecodeError( + entityType: AcceptanceEntityType, + decoderPath: string, + decoderMessage: string +): OcpParseError { + return new OcpParseError(`Invalid DAML create argument for ${entityType} at ${decoderPath}: ${decoderMessage}`, { + source: `damlAcceptanceCreateArgument.${entityType}`, + code: OcpErrorCodes.SCHEMA_MISMATCH, + context: { + entityType, + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP[entityType], + decoderPath, + decoderMessage, + }, + }); +} + +function requireOwnFields( + entityType: AcceptanceEntityType, + record: Record, + fields: readonly string[], + decoderPath: string +): void { + for (const field of fields) { + if (!hasOwnField(record, field)) { + throw acceptanceDecodeError(entityType, decoderPath, `the key '${field}' is required as an own property`); + } + } +} + +function validateAcceptanceOwnProperties(entityType: AcceptanceEntityType, createArgument: unknown): void { + if (!isRecord(createArgument)) return; + + requireOwnFields(entityType, createArgument, ['context', 'acceptance_data'], 'input'); + + const context = ownField(createArgument, 'context'); + if (isRecord(context)) { + requireOwnFields(entityType, context, ['issuer', 'system_operator'], 'input.context'); + } + + const acceptanceData = ownField(createArgument, 'acceptance_data'); + if (!isRecord(acceptanceData)) return; + + requireOwnFields(entityType, acceptanceData, ['id', 'date', 'security_id', 'comments'], 'input.acceptance_data'); + + const comments = ownField(acceptanceData, 'comments'); + if (!Array.isArray(comments)) return; + + for (let index = 0; index < comments.length; index++) { + if (!hasOwnField(comments, String(index))) { + throw acceptanceDecodeError( + entityType, + `input.acceptance_data.comments[${index}]`, + 'list element is missing or inherited rather than an own property' + ); + } + } +} + +/** Decode the full generated contract wrapper and return its recursively decoded acceptance payload. */ +export function extractAndDecodeAcceptanceData( + entityType: EntityType, + createArgument: unknown +): AcceptanceDataFor; +export function extractAndDecodeAcceptanceData( + entityType: AcceptanceEntityType, + createArgument: unknown +): AcceptanceDataFor { + validateAcceptanceOwnProperties(entityType, createArgument); + const decoded = ACCEPTANCE_CREATE_ARGUMENT_DECODER_MAP[entityType].run(createArgument); + + if (!decoded.ok) { + const { at: decoderPath, message: decoderMessage } = decoded.error; + throw acceptanceDecodeError(entityType, decoderPath, decoderMessage); + } + + return decoded.result.acceptance_data; +} diff --git a/src/functions/OpenCapTable/capTable/damlEntityData.ts b/src/functions/OpenCapTable/capTable/damlEntityData.ts index f71a251f..a501d87a 100644 --- a/src/functions/OpenCapTable/capTable/damlEntityData.ts +++ b/src/functions/OpenCapTable/capTable/damlEntityData.ts @@ -1,5 +1,6 @@ import { Fairmint } from '@fairmint/open-captable-protocol-daml-js'; import { OcpErrorCodes, OcpParseError } from '../../../errors'; +import { extractAndDecodeAcceptanceData, isAcceptanceEntityType } from './acceptanceContractData'; import { ENTITY_DATA_FIELD_FALLBACK_MAP, ENTITY_DATA_FIELD_MAP, @@ -160,6 +161,14 @@ export function decodeDamlEntityData(entityType: OcfEntityType, input: unknown): export function extractAndDecodeDamlEntityData( entityType: EntityType, createArgument: unknown -): DamlDataTypeFor { +): DamlDataTypeFor; +export function extractAndDecodeDamlEntityData( + entityType: OcfEntityType, + createArgument: unknown +): DamlDataTypeFor { + if (isAcceptanceEntityType(entityType)) { + return extractAndDecodeAcceptanceData(entityType, createArgument); + } + return decodeDamlEntityData(entityType, extractEntityData(entityType, createArgument)); } diff --git a/test/batch/damlToOcfDispatcher.test.ts b/test/batch/damlToOcfDispatcher.test.ts index 1472fbe1..49e56e0a 100644 --- a/test/batch/damlToOcfDispatcher.test.ts +++ b/test/batch/damlToOcfDispatcher.test.ts @@ -132,6 +132,34 @@ describe('damlToOcf dispatcher', () => { classification: 'module_entity_mismatch', }); }); + + it('enforces the full generated wrapper for generic acceptance reads', async () => { + const getEventsByContractId = jest.fn().mockResolvedValue( + buildCreatedEventsResponse( + { + acceptance_data: { + id: 'acceptance-1', + date: '2025-01-01T00:00:00Z', + security_id: 'security-1', + comments: [], + }, + }, + Fairmint.OpenCapTable.OCF.StockAcceptance.StockAcceptance.templateId + ) + ); + const mockClient = { getEventsByContractId } as unknown as LedgerJsonApiClient; + + await expect(getEntityAsOcf(mockClient, 'stockAcceptance', 'acceptance-cid')).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: 'damlAcceptanceCreateArgument.stockAcceptance', + context: { + entityType: 'stockAcceptance', + decoderPath: 'input', + decoderMessage: expect.stringContaining("key 'context' is required"), + }, + }); + }); }); describe('ENTITY_TEMPLATE_ID_MAP', () => { @@ -210,6 +238,10 @@ describe('damlToOcf dispatcher', () => { getEntityAsOcf(client, 'stockAcceptance', 'stock-acceptance-cid', { readAs: ['issuer::p'] }), buildCreatedEventsResponse( { + context: { + issuer: 'issuer::p', + system_operator: 'system-operator::p', + }, acceptance_data: { id: 'acc-1', date: '2025-01-01T00:00:00Z', diff --git a/test/functions/acceptanceReaders.test.ts b/test/functions/acceptanceReaders.test.ts index c68e75a5..c399b342 100644 --- a/test/functions/acceptanceReaders.test.ts +++ b/test/functions/acceptanceReaders.test.ts @@ -1,7 +1,7 @@ /** Direct ledger-reader contracts shared by all four OCF acceptance families. */ import type { LedgerJsonApiClient } from '@fairmint/canton-node-sdk'; -import { OcpContractError, OcpErrorCodes, OcpParseError } from '../../src/errors'; +import { OcpContractError, OcpErrorCodes, OcpParseError, OcpValidationError } from '../../src/errors'; import { ENTITY_DATA_FIELD_MAP, ENTITY_TEMPLATE_ID_MAP, @@ -32,6 +32,11 @@ type AcceptanceEvent = | OcfStockAcceptance | OcfWarrantAcceptance; +const VALID_CONTEXT = { + issuer: 'issuer::party', + system_operator: 'system-operator::party', +} as const; + interface AcceptanceReaderCase { readonly entityType: AcceptanceEntityType; readonly contractId: string; @@ -126,18 +131,26 @@ const acceptanceReaderCases: readonly AcceptanceReaderCase[] = [ }, ]; +interface MockContractOptions { + readonly templateId?: string; + readonly packageName?: string; + readonly createArgument?: Record; +} + function createMockClient( testCase: AcceptanceReaderCase, data: Record, - templateId: string = ENTITY_TEMPLATE_ID_MAP[testCase.entityType] + options: MockContractOptions = {} ): LedgerJsonApiClient { return { getEventsByContractId: jest.fn().mockResolvedValue({ created: { createdEvent: { contractId: testCase.contractId, - templateId, - createArgument: { + templateId: options.templateId ?? ENTITY_TEMPLATE_ID_MAP[testCase.entityType], + ...(options.packageName !== undefined ? { packageName: options.packageName } : {}), + createArgument: options.createArgument ?? { + context: VALID_CONTEXT, [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: data, }, }, @@ -159,10 +172,12 @@ describe('decoder-backed acceptance readers', () => { it.each(acceptanceReaderCases)('$entityType rejects malformed required fields', async (testCase) => { for (const [field, malformedData] of [ ['id', { ...testCase.validData(), id: 17 }], + ['date', { ...testCase.validData(), date: null }], [ 'security_id', Object.fromEntries(Object.entries(testCase.validData()).filter(([key]) => key !== 'security_id')), ], + ['comments', { ...testCase.validData(), comments: null }], ] as const) { const client = createMockClient(testCase, malformedData); @@ -198,15 +213,217 @@ describe('decoder-backed acceptance readers', () => { code: OcpErrorCodes.SCHEMA_MISMATCH, context: { entityType: testCase.entityType, - decoderPath: expect.stringContaining('comments'), + decoderPath: 'input.acceptance_data.comments[1]', decoderMessage: expect.any(String), }, }); }); + it.each(acceptanceReaderCases)('$entityType rejects a createArgument missing generated context', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: testCase.validData(), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + expectedTemplateId: ENTITY_TEMPLATE_ID_MAP[testCase.entityType], + decoderPath: 'input', + decoderMessage: expect.stringContaining("key 'context' is required"), + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects an inherited generated wrapper', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: Object.create({ + context: VALID_CONTEXT, + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: testCase.validData(), + }), + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input', + decoderMessage: expect.stringContaining("key 'context' is required as an own property"), + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects malformed generated context fields', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + context: { issuer: 17, system_operator: VALID_CONTEXT.system_operator }, + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: testCase.validData(), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input.context.issuer', + decoderMessage: 'expected a string, got a number', + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects inherited generated context fields', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + context: Object.create(VALID_CONTEXT), + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: testCase.validData(), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input.context', + decoderMessage: expect.stringContaining("key 'issuer' is required as an own property"), + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects inherited required payload fields', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + context: VALID_CONTEXT, + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: Object.create(testCase.validData()), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input.acceptance_data', + decoderMessage: expect.stringContaining("key 'id' is required as an own property"), + }, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects a wrong generated wrapper variant', async (testCase) => { + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + context: VALID_CONTEXT, + cancellation_data: testCase.validData(), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input', + decoderMessage: expect.stringContaining("key 'acceptance_data' is required"), + }, + }); + }); + + it.each(acceptanceReaderCases)( + '$entityType rejects a malformed lexical date with its exact field path', + async (testCase) => { + const client = createMockClient(testCase, { + ...testCase.validData(), + date: 'not-a-date', + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpValidationError', + code: OcpErrorCodes.INVALID_FORMAT, + fieldPath: `${testCase.entityType}.date`, + receivedValue: 'not-a-date', + }); + await expect(testCase.invoke(client)).rejects.toBeInstanceOf(OcpValidationError); + } + ); + + it.each(acceptanceReaderCases)('$entityType omits only the canonical empty comments list', async (testCase) => { + const client = createMockClient(testCase, { + ...testCase.validData(), + comments: [], + }); + const expectedEvent = { ...testCase.expectedEvent }; + delete expectedEvent.comments; + + await expect(testCase.invoke(client)).resolves.toEqual({ + event: expectedEvent, + contractId: testCase.contractId, + }); + }); + + it.each(acceptanceReaderCases)('$entityType rejects sparse comments without dropping indexes', async (testCase) => { + const comments = new Array(2); + comments[1] = 'second comment'; + const client = createMockClient(testCase, { + ...testCase.validData(), + comments, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input.acceptance_data.comments[0]', + decoderMessage: 'list element is missing or inherited rather than an own property', + }, + }); + }); + + it.each(acceptanceReaderCases)( + '$entityType rejects comments inherited through an array prototype', + async (testCase) => { + class InheritedComments extends Array {} + Object.defineProperty(InheritedComments.prototype, 0, { + configurable: true, + value: 'inherited comment', + }); + const comments = new InheritedComments(1); + const client = createMockClient(testCase, { + ...testCase.validData(), + comments, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpParseError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + source: `damlAcceptanceCreateArgument.${testCase.entityType}`, + context: { + entityType: testCase.entityType, + decoderPath: 'input.acceptance_data.comments[0]', + decoderMessage: 'list element is missing or inherited rather than an own property', + }, + }); + } + ); + it.each(acceptanceReaderCases)('$entityType rejects a contract from the wrong template', async (testCase) => { - const wrongTemplateId = ENTITY_TEMPLATE_ID_MAP.document; - const client = createMockClient(testCase, testCase.validData(), wrongTemplateId); + const wrongTemplateId = + testCase.entityType === 'stockAcceptance' + ? ENTITY_TEMPLATE_ID_MAP.warrantAcceptance + : ENTITY_TEMPLATE_ID_MAP.stockAcceptance; + const client = createMockClient(testCase, testCase.validData(), { templateId: wrongTemplateId }); await expect(testCase.invoke(client)).rejects.toMatchObject({ name: 'OcpContractError', @@ -221,4 +438,26 @@ describe('decoder-backed acceptance readers', () => { }); await expect(testCase.invoke(client)).rejects.toBeInstanceOf(OcpContractError); }); + + it.each(acceptanceReaderCases)('$entityType rejects the right module on the wrong package line', async (testCase) => { + const expectedTemplateId = ENTITY_TEMPLATE_ID_MAP[testCase.entityType]; + const wrongTemplateId = expectedTemplateId.replace(/^#[^:]+/, '#OpenCapTable-wrong'); + const client = createMockClient(testCase, testCase.validData(), { + templateId: wrongTemplateId, + packageName: 'OpenCapTable-wrong', + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: 'OcpContractError', + code: OcpErrorCodes.SCHEMA_MISMATCH, + classification: 'package_name_mismatch', + contractId: testCase.contractId, + templateId: wrongTemplateId, + context: { + expectedTemplateId, + actualTemplateId: wrongTemplateId, + actualPackageName: 'OpenCapTable-wrong', + }, + }); + }); }); From ab98622958c9b6c98491ac79e5b1d762e5f2ac20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Jul 2026 19:46:33 +0000 Subject: [PATCH 3/6] Auto-fix: build changes --- test/mocks/fairmint-canton-node-sdk.ts | 3 ++- test/utils/getFeaturedAppRightContractDetails.test.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/mocks/fairmint-canton-node-sdk.ts b/test/mocks/fairmint-canton-node-sdk.ts index 0c4dcc89..7f7be419 100644 --- a/test/mocks/fairmint-canton-node-sdk.ts +++ b/test/mocks/fairmint-canton-node-sdk.ts @@ -219,7 +219,8 @@ export async function getFeaturedAppRightContractDetails(validatorApi: Validator if (!featuredAppRight) { throw new Error(`No featured app right found for party ${partyId}`); } - const synchronizerIdFromFeatured = typeof featuredAppRight.domain_id === 'string' ? featuredAppRight.domain_id : undefined; + const synchronizerIdFromFeatured = + typeof featuredAppRight.domain_id === 'string' ? featuredAppRight.domain_id : undefined; let synchronizerId = synchronizerIdFromFeatured; if (!synchronizerId) { // The featured-apps endpoint may not include the synchronizer/domain id. diff --git a/test/utils/getFeaturedAppRightContractDetails.test.ts b/test/utils/getFeaturedAppRightContractDetails.test.ts index 2d021441..f563591a 100644 --- a/test/utils/getFeaturedAppRightContractDetails.test.ts +++ b/test/utils/getFeaturedAppRightContractDetails.test.ts @@ -37,9 +37,7 @@ describe('getFeaturedAppRightContractDetails', () => { }, }; (responseWithDomain.featured_app_right as Record).domain_id = 'featured-domain'; - jest - .spyOn(validatorApi, 'lookupFeaturedAppRight') - .mockResolvedValue(responseWithDomain); + jest.spyOn(validatorApi, 'lookupFeaturedAppRight').mockResolvedValue(responseWithDomain); const featured = await getFeaturedAppRightContractDetails(validatorApi); From 63f009f532e90cbc7c873dd21b0da48675888660 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 18:24:08 -0400 Subject: [PATCH 4/6] test: assert acceptance decode failures once --- test/functions/acceptanceReaders.test.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/functions/acceptanceReaders.test.ts b/test/functions/acceptanceReaders.test.ts index 6985772a..7b1862cd 100644 --- a/test/functions/acceptanceReaders.test.ts +++ b/test/functions/acceptanceReaders.test.ts @@ -394,7 +394,17 @@ describe('decoder-backed acceptance readers', () => { for (const [level, createArgument, expectedSource] of cases) { const client = createMockClient(testCase, validData, { createArgument }); - await expect(testCase.invoke(client)).rejects.toMatchObject({ + let error: unknown; + + try { + await testCase.invoke(client); + throw new Error(`Expected ${testCase.entityType} to reject an extra ${level} field`); + } catch (caughtError: unknown) { + error = caughtError; + } + + expect(error).toBeInstanceOf(OcpParseError); + expect(error).toMatchObject({ name: 'OcpParseError', code: OcpErrorCodes.SCHEMA_MISMATCH, classification: 'lossy_daml_decode', @@ -405,13 +415,6 @@ describe('decoder-backed acceptance readers', () => { fieldPath: expectedSource, }, }); - - try { - await testCase.invoke(client); - throw new Error(`Expected ${testCase.entityType} to reject an extra ${level} field`); - } catch (error: unknown) { - expect(error).toBeInstanceOf(OcpParseError); - } } } ); From b271db139c91b3305bfb35620f34d5dd1a57f866 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 22:22:07 -0400 Subject: [PATCH 5/6] fix: enforce acceptance identifiers once --- .../capTable/acceptanceContractData.ts | 7 +- .../convertibleAcceptanceDataToDaml.ts | 6 + .../equityCompensationAcceptanceDataToDaml.ts | 6 + .../stockAcceptanceDataToDaml.ts | 6 + .../warrantAcceptanceDataToDaml.ts | 6 + test/converters/acceptanceConverters.test.ts | 57 ++++++++++ test/functions/acceptanceReaders.test.ts | 106 +++++++++++++++++- 7 files changed, 189 insertions(+), 5 deletions(-) diff --git a/src/functions/OpenCapTable/capTable/acceptanceContractData.ts b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts index c10e3c51..3304b5eb 100644 --- a/src/functions/OpenCapTable/capTable/acceptanceContractData.ts +++ b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts @@ -2,6 +2,7 @@ import { Fairmint } from '@fairmint/open-captable-protocol-daml-js'; import { OcpErrorCodes, OcpParseError } from '../../../errors'; import { toSafeDiagnosticText } from '../../../errors/OcpError'; import { assertSafeGeneratedDamlJson } from '../../../utils/generatedDamlValidation'; +import { validateRequiredString } from '../../../utils/validation'; import { ENTITY_TEMPLATE_ID_MAP, type OcfEntityType } from './batchTypes'; import { assertLosslessGeneratedDamlRoundTrip } from './damlCodecLosslessness'; @@ -112,7 +113,11 @@ function createAcceptanceCreateArgumentDecoder { expect(() => convertToDaml('equityCompensationAcceptance', ocfData)).toThrow('equityCompensationAcceptance.id'); }); }); + + test.each([ + [ + 'stockAcceptance', + 'stockAcceptance.security_id', + () => + convertToDaml('stockAcceptance', { + object_type: 'TX_STOCK_ACCEPTANCE', + id: 'stock-accept-empty-security', + date: '2024-01-15', + security_id: '', + }), + ], + [ + 'warrantAcceptance', + 'warrantAcceptance.security_id', + () => + convertToDaml('warrantAcceptance', { + object_type: 'TX_WARRANT_ACCEPTANCE', + id: 'warrant-accept-empty-security', + date: '2024-03-10', + security_id: '', + }), + ], + [ + 'convertibleAcceptance', + 'convertibleAcceptance.security_id', + () => + convertToDaml('convertibleAcceptance', { + object_type: 'TX_CONVERTIBLE_ACCEPTANCE', + id: 'convertible-accept-empty-security', + date: '2024-04-05', + security_id: '', + }), + ], + [ + 'equityCompensationAcceptance', + 'equityCompensationAcceptance.security_id', + () => + convertToDaml('equityCompensationAcceptance', { + object_type: 'TX_EQUITY_COMPENSATION_ACCEPTANCE', + id: 'equity-accept-empty-security', + date: '2024-05-01', + security_id: '', + }), + ], + ] as const)('%s rejects a zero-length security_id', (_entityType, fieldPath, invoke) => { + let error: unknown; + try { + invoke(); + } catch (caughtError: unknown) { + error = caughtError; + } + + expect(error).toBeInstanceOf(OcpValidationError); + expect(error).toMatchObject({ fieldPath, receivedValue: '' }); + }); }); describe('DAML → OCF conversion', () => { diff --git a/test/functions/acceptanceReaders.test.ts b/test/functions/acceptanceReaders.test.ts index 7b1862cd..f6ab150e 100644 --- a/test/functions/acceptanceReaders.test.ts +++ b/test/functions/acceptanceReaders.test.ts @@ -168,6 +168,19 @@ function ledgerCreateArgumentRoot(testCase: AcceptanceReaderCase): string { return `contract ${testCase.contractId}.eventsResponse.created.createdEvent.createArgument`; } +function expectSingleLedgerRead(client: LedgerJsonApiClient): void { + expect(client.getEventsByContractId).toHaveBeenCalledTimes(1); +} + +async function captureRejection(promise: Promise, message: string): Promise { + try { + await promise; + } catch (error: unknown) { + return error; + } + throw new Error(message); +} + describe('decoder-backed acceptance readers', () => { it.each(acceptanceReaderCases)('$entityType returns the exact canonical event shape', async (testCase) => { const client = createMockClient(testCase, testCase.validData()); @@ -176,6 +189,7 @@ describe('decoder-backed acceptance readers', () => { event: testCase.expectedEvent, contractId: testCase.contractId, }); + expectSingleLedgerRead(client); }); it.each(acceptanceReaderCases)( @@ -187,6 +201,7 @@ describe('decoder-backed acceptance readers', () => { data: testCase.expectedEvent, contractId: testCase.contractId, }); + expectSingleLedgerRead(client); } ); @@ -220,9 +235,79 @@ describe('decoder-backed acceptance readers', () => { field ); } + expectSingleLedgerRead(client); } }); + it.each(acceptanceReaderCases)( + '$entityType rejects zero-length identifiers once in dedicated and generic readers', + async (testCase) => { + for (const field of ['id', 'security_id'] as const) { + const invalidData = { ...testCase.validData(), [field]: '' }; + const expectedFieldPath = `${createArgumentRoot(testCase)}.acceptance_data.${field}`; + const dedicatedClient = createMockClient(testCase, invalidData); + const dedicatedError = await captureRejection( + testCase.invoke(dedicatedClient), + `Expected ${testCase.entityType} dedicated reader to reject empty ${field}` + ); + + expect(dedicatedError).toMatchObject({ + name: 'OcpValidationError', + code: OcpErrorCodes.REQUIRED_FIELD_MISSING, + fieldPath: expectedFieldPath, + expectedType: 'non-empty string', + receivedValue: '', + }); + expectSingleLedgerRead(dedicatedClient); + + const genericClient = createMockClient(testCase, invalidData); + const genericError = await captureRejection( + getEntityAsOcf(genericClient, testCase.entityType, testCase.contractId), + `Expected ${testCase.entityType} generic reader to reject empty ${field}` + ); + + expect(genericError).toMatchObject({ + name: 'OcpValidationError', + code: OcpErrorCodes.REQUIRED_FIELD_MISSING, + fieldPath: expectedFieldPath, + expectedType: 'non-empty string', + receivedValue: '', + }); + expectSingleLedgerRead(genericClient); + } + } + ); + + it.each(acceptanceReaderCases)( + '$entityType preserves non-empty whitespace and padded identifiers', + async (testCase) => { + const paddedData = { + ...testCase.validData(), + id: ' acceptance-id ', + security_id: ' ', + }; + const expectedEvent = { + ...testCase.expectedEvent, + id: ' acceptance-id ', + security_id: ' ', + }; + const dedicatedClient = createMockClient(testCase, paddedData); + + await expect(testCase.invoke(dedicatedClient)).resolves.toEqual({ + event: expectedEvent, + contractId: testCase.contractId, + }); + expectSingleLedgerRead(dedicatedClient); + + const genericClient = createMockClient(testCase, paddedData); + await expect(getEntityAsOcf(genericClient, testCase.entityType, testCase.contractId)).resolves.toEqual({ + data: expectedEvent, + contractId: testCase.contractId, + }); + expectSingleLedgerRead(genericClient); + } + ); + it.each(acceptanceReaderCases)('$entityType rejects malformed comment elements', async (testCase) => { const client = createMockClient(testCase, { ...testCase.validData(), @@ -415,6 +500,7 @@ describe('decoder-backed acceptance readers', () => { fieldPath: expectedSource, }, }); + expectSingleLedgerRead(client); } } ); @@ -427,13 +513,19 @@ describe('decoder-backed acceptance readers', () => { date: 'not-a-date', }); - await expect(testCase.invoke(client)).rejects.toMatchObject({ + const error = await captureRejection( + testCase.invoke(client), + `Expected ${testCase.entityType} reader to reject a malformed date` + ); + + expect(error).toMatchObject({ name: 'OcpValidationError', code: OcpErrorCodes.INVALID_FORMAT, fieldPath: `${testCase.entityType}.date`, receivedValue: 'not-a-date', }); - await expect(testCase.invoke(client)).rejects.toBeInstanceOf(OcpValidationError); + expect(error).toBeInstanceOf(OcpValidationError); + expectSingleLedgerRead(client); } ); @@ -627,7 +719,12 @@ describe('decoder-backed acceptance readers', () => { : ENTITY_TEMPLATE_ID_MAP.stockAcceptance; const client = createMockClient(testCase, testCase.validData(), { templateId: wrongTemplateId }); - await expect(testCase.invoke(client)).rejects.toMatchObject({ + const error = await captureRejection( + testCase.invoke(client), + `Expected ${testCase.entityType} reader to reject a wrong-template contract` + ); + + expect(error).toMatchObject({ name: 'OcpContractError', code: OcpErrorCodes.SCHEMA_MISMATCH, classification: 'module_entity_mismatch', @@ -638,7 +735,8 @@ describe('decoder-backed acceptance readers', () => { actualTemplateId: wrongTemplateId, }, }); - await expect(testCase.invoke(client)).rejects.toBeInstanceOf(OcpContractError); + expect(error).toBeInstanceOf(OcpContractError); + expectSingleLedgerRead(client); }); it.each(acceptanceReaderCases)('$entityType rejects the right module on the wrong package line', async (testCase) => { From 9e0947c2266d0aab66adbdc2f4f9d9d204b19526 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sun, 12 Jul 2026 09:42:47 -0400 Subject: [PATCH 6/6] fix: validate acceptance reader context --- .../capTable/acceptanceContractData.ts | 5 +- test/declarations/acceptanceReaders.types.ts | 21 ++-- test/functions/acceptanceReaders.test.ts | 29 +++++ test/typeContracts/typeAssertions.ts | 106 ++++++++++++++++++ test/types/acceptanceReaders.types.ts | 21 ++-- 5 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 test/typeContracts/typeAssertions.ts diff --git a/src/functions/OpenCapTable/capTable/acceptanceContractData.ts b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts index 3304b5eb..5a026e42 100644 --- a/src/functions/OpenCapTable/capTable/acceptanceContractData.ts +++ b/src/functions/OpenCapTable/capTable/acceptanceContractData.ts @@ -2,7 +2,7 @@ import { Fairmint } from '@fairmint/open-captable-protocol-daml-js'; import { OcpErrorCodes, OcpParseError } from '../../../errors'; import { toSafeDiagnosticText } from '../../../errors/OcpError'; import { assertSafeGeneratedDamlJson } from '../../../utils/generatedDamlValidation'; -import { validateRequiredString } from '../../../utils/validation'; +import { validatePartyId, validateRequiredString } from '../../../utils/validation'; import { ENTITY_TEMPLATE_ID_MAP, type OcfEntityType } from './batchTypes'; import { assertLosslessGeneratedDamlRoundTrip } from './damlCodecLosslessness'; @@ -113,6 +113,9 @@ function createAcceptanceCreateArgumentDecoder = T; -type IsAny = 0 extends 1 & T ? true : false; -type IsExactly = [A] extends [B] ? ([B] extends [A] ? true : false) : false; +import type { Assert, ContainsAny, IsExactly } from '../typeContracts/typeAssertions'; type StockEvent = GetStockAcceptanceAsOcfResult['event']; type ConvertibleEvent = GetConvertibleAcceptanceAsOcfResult['event']; @@ -36,10 +33,14 @@ const genericEquityCompensationIsExact: Assert< > = true; const genericWarrantIsExact: Assert> = true; -const stockIsNotAny: Assert, false>> = true; -const convertibleIsNotAny: Assert, false>> = true; -const equityCompensationIsNotAny: Assert, false>> = true; -const warrantIsNotAny: Assert, false>> = true; +const stockIsNotAny: Assert, false>> = true; +const convertibleIsNotAny: Assert, false>> = true; +const equityCompensationIsNotAny: Assert, false>> = true; +const warrantIsNotAny: Assert, false>> = true; +const genericStockIsNotAny: Assert, false>> = true; +const genericConvertibleIsNotAny: Assert, false>> = true; +const genericEquityCompensationIsNotAny: Assert, false>> = true; +const genericWarrantIsNotAny: Assert, false>> = true; declare const stockResult: GetStockAcceptanceAsOcfResult; declare const convertibleResult: GetConvertibleAcceptanceAsOcfResult; @@ -67,6 +68,10 @@ void stockIsNotAny; void convertibleIsNotAny; void equityCompensationIsNotAny; void warrantIsNotAny; +void genericStockIsNotAny; +void genericConvertibleIsNotAny; +void genericEquityCompensationIsNotAny; +void genericWarrantIsNotAny; void wrongWarrant; void wrongStock; void wrongConvertible; diff --git a/test/functions/acceptanceReaders.test.ts b/test/functions/acceptanceReaders.test.ts index f6ab150e..3abd0319 100644 --- a/test/functions/acceptanceReaders.test.ts +++ b/test/functions/acceptanceReaders.test.ts @@ -387,6 +387,35 @@ describe('decoder-backed acceptance readers', () => { }); }); + it.each(acceptanceReaderCases)( + '$entityType validates semantic Party IDs in the generated context', + async (testCase) => { + const cases = [ + ['issuer', '', OcpErrorCodes.REQUIRED_FIELD_MISSING], + ['issuer', 'issuer party', OcpErrorCodes.INVALID_FORMAT], + ['system_operator', '', OcpErrorCodes.REQUIRED_FIELD_MISSING], + ['system_operator', 'system operator', OcpErrorCodes.INVALID_FORMAT], + ] as const; + + for (const [field, value, code] of cases) { + const context = { ...VALID_CONTEXT, [field]: value }; + const client = createMockClient(testCase, testCase.validData(), { + createArgument: { + context, + [ENTITY_DATA_FIELD_MAP[testCase.entityType]]: testCase.validData(), + }, + }); + + await expect(testCase.invoke(client)).rejects.toMatchObject({ + name: OcpValidationError.name, + code, + fieldPath: `${createArgumentRoot(testCase)}.context.${field}`, + }); + expectSingleLedgerRead(client); + } + } + ); + it.each(acceptanceReaderCases)('$entityType rejects inherited generated context fields', async (testCase) => { const client = createMockClient(testCase, testCase.validData(), { createArgument: { diff --git a/test/typeContracts/typeAssertions.ts b/test/typeContracts/typeAssertions.ts new file mode 100644 index 00000000..53575d4f --- /dev/null +++ b/test/typeContracts/typeAssertions.ts @@ -0,0 +1,106 @@ +/** Shared compile-time assertions for source, built, and package-facing type contracts. */ + +export type IsAny = 0 extends 1 & Value ? true : false; + +export type Assert = IsAny extends true ? never : Condition; + +export type IsOptional = + IsAny extends true + ? never + : IsAny extends true + ? never + : [Value] extends [never] + ? never + : [Key] extends [never] + ? never + : {} extends Pick + ? true + : false; + +type TypeEqual = + (() => Value extends Left ? 1 : 2) extends () => Value extends Right ? 1 : 2 + ? (() => Value extends Right ? 1 : 2) extends () => Value extends Left ? 1 : 2 + ? true + : false + : false; + +type HasSeen = Seen extends readonly [infer Head, ...infer Tail] + ? TypeEqual extends true + ? true + : HasSeen + : false; + +type ArrayExtraValues = Value[Exclude]; + +type ConstructSignatureContainsAny = Value extends abstract new ( + ...args: infer Arguments +) => infer Instance + ? true extends + | ContainsAnyInternal + | ContainsAnyInternal + | ContainsAnyInternal + ? true + : false + : false; + +type ContainsAnyMember = + HasSeen extends true + ? false + : Value extends (...args: infer Arguments) => infer Result + ? true extends + | ContainsAnyInternal + | ContainsAnyInternal + | ContainsAnyInternal, readonly [...Seen, Value]> + | ContainsAnyInternal + | ConstructSignatureContainsAny + ? true + : false + : Value extends abstract new (...args: infer Arguments) => infer Instance + ? true extends + | ContainsAnyInternal + | ContainsAnyInternal + | ContainsAnyInternal + ? true + : false + : Value extends ReadonlyArray + ? true extends + | ContainsAnyInternal + | ContainsAnyInternal, readonly [...Seen, Value]> + ? true + : false + : Value extends object + ? true extends { + [Key in keyof Value]-?: ContainsAnyInternal; + }[keyof Value] + ? true + : false + : false; + +type ContainsAnyInternal = + IsAny extends true + ? true + : true extends (Value extends unknown ? ContainsAnyMember : never) + ? true + : false; + +/** + * Detect `any` in structurally reachable members and conditionally visible call/construct signatures. + * TypeScript does not expose every overload or generic default for complete reflection. + */ +export type ContainsAny = ContainsAnyInternal; + +type NormalizeExact = Value extends (...args: never[]) => unknown + ? Value + : Value extends abstract new (...args: never[]) => unknown + ? Value + : Value extends object + ? { [Key in keyof Value]: Value[Key] } + : Value; + +/** Exact structural equality guarded against both top-level and nested `any`. */ +export type IsExactly = + ContainsAny extends true + ? false + : ContainsAny extends true + ? false + : TypeEqual, NormalizeExact>; diff --git a/test/types/acceptanceReaders.types.ts b/test/types/acceptanceReaders.types.ts index 76003058..f529fd9d 100644 --- a/test/types/acceptanceReaders.types.ts +++ b/test/types/acceptanceReaders.types.ts @@ -11,10 +11,7 @@ import type { OcfStockAcceptance, OcfWarrantAcceptance, } from '../../src/types/native'; - -type Assert = T; -type IsAny = 0 extends 1 & T ? true : false; -type IsExactly = [A] extends [B] ? ([B] extends [A] ? true : false) : false; +import type { Assert, ContainsAny, IsExactly } from '../typeContracts/typeAssertions'; type StockEvent = GetStockAcceptanceAsOcfResult['event']; type ConvertibleEvent = GetConvertibleAcceptanceAsOcfResult['event']; @@ -36,10 +33,14 @@ const genericEquityCompensationIsExact: Assert< > = true; const genericWarrantIsExact: Assert> = true; -const stockIsNotAny: Assert, false>> = true; -const convertibleIsNotAny: Assert, false>> = true; -const equityCompensationIsNotAny: Assert, false>> = true; -const warrantIsNotAny: Assert, false>> = true; +const stockIsNotAny: Assert, false>> = true; +const convertibleIsNotAny: Assert, false>> = true; +const equityCompensationIsNotAny: Assert, false>> = true; +const warrantIsNotAny: Assert, false>> = true; +const genericStockIsNotAny: Assert, false>> = true; +const genericConvertibleIsNotAny: Assert, false>> = true; +const genericEquityCompensationIsNotAny: Assert, false>> = true; +const genericWarrantIsNotAny: Assert, false>> = true; declare const stockResult: GetStockAcceptanceAsOcfResult; declare const convertibleResult: GetConvertibleAcceptanceAsOcfResult; @@ -67,6 +68,10 @@ void stockIsNotAny; void convertibleIsNotAny; void equityCompensationIsNotAny; void warrantIsNotAny; +void genericStockIsNotAny; +void genericConvertibleIsNotAny; +void genericEquityCompensationIsNotAny; +void genericWarrantIsNotAny; void wrongWarrant; void wrongStock; void wrongConvertible;