diff --git a/packages/cre-sdk-examples/src/workflows/on-chain/index.ts b/packages/cre-sdk-examples/src/workflows/on-chain/index.ts index 79b687ea..a55cb2cb 100644 --- a/packages/cre-sdk-examples/src/workflows/on-chain/index.ts +++ b/packages/cre-sdk-examples/src/workflows/on-chain/index.ts @@ -5,6 +5,7 @@ import { encodeCallMsg, getNetwork, type HTTPSendRequester, + isChainSelectorSupported, LAST_FINALIZED_BLOCK_NUMBER, ok, Runner, @@ -56,6 +57,12 @@ const onCronTrigger = (runtime: Runtime) => { // Get the first EVM configuration from the list const evmConfig = runtime.config.evms[0] + + // Make sure we try to run on supported chain + if (!isChainSelectorSupported(evmConfig.chainSelectorName)) { + throw new Error(`Chain selector name: ${evmConfig.chainSelectorName} is not supported.`) + } + const network = getNetwork({ chainFamily: 'evm', chainSelectorName: evmConfig.chainSelectorName, diff --git a/packages/cre-sdk/src/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.ts b/packages/cre-sdk/src/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.ts index af4b506e..584ec770 100644 --- a/packages/cre-sdk/src/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.ts +++ b/packages/cre-sdk/src/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.ts @@ -126,8 +126,8 @@ export class ClientCapability { static readonly CAPABILITY_NAME = 'evm' static readonly CAPABILITY_VERSION = '1.0.0' - /** Available chain selectors */ - static readonly SUPPORTED_CHAINS = { + /** Available ChainSelector values */ + static readonly SUPPORTED_CHAIN_SELECTORS = { 'avalanche-mainnet': 6433500567565415381n, 'avalanche-testnet-fuji': 14767482510784806043n, 'binance_smart_chain-mainnet-opbnb-1': 465944652040885897n, @@ -143,7 +143,7 @@ export class ClientCapability { 'polygon-testnet-amoy': 16281711391670634445n, } as const - constructor(private readonly chainSelector?: bigint) {} + constructor(private readonly ChainSelector: bigint) {} callContract( runtime: Runtime, @@ -160,10 +160,8 @@ export class ClientCapability { payload = fromJson(CallContractRequestSchema, input as CallContractRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -197,10 +195,8 @@ export class ClientCapability { payload = fromJson(FilterLogsRequestSchema, input as FilterLogsRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -234,10 +230,8 @@ export class ClientCapability { payload = fromJson(BalanceAtRequestSchema, input as BalanceAtRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -271,10 +265,8 @@ export class ClientCapability { payload = fromJson(EstimateGasRequestSchema, input as EstimateGasRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -311,10 +303,8 @@ export class ClientCapability { ) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability< GetTransactionByHashRequest, @@ -354,10 +344,8 @@ export class ClientCapability { ) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability< GetTransactionReceiptRequest, @@ -394,10 +382,8 @@ export class ClientCapability { payload = fromJson(HeaderByNumberRequestSchema, input as HeaderByNumberRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -431,10 +417,8 @@ export class ClientCapability { payload = fromJson(RegisterLogTrackingRequestSchema, input as RegisterLogTrackingRequestJson) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -471,10 +455,8 @@ export class ClientCapability { ) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -494,11 +476,9 @@ export class ClientCapability { } logTrigger(config: FilterLogTriggerRequestJson): ClientLogTrigger { - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID - return new ClientLogTrigger(config, capabilityId, 'LogTrigger') + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` + return new ClientLogTrigger(config, capabilityId, 'LogTrigger', this.ChainSelector) } writeReport( @@ -519,10 +499,8 @@ export class ClientCapability { ) } - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.chainSelector}@${ClientCapability.CAPABILITY_VERSION}` - : ClientCapability.CAPABILITY_ID + // Include all labels in capability ID for routing when specified + const capabilityId = `${ClientCapability.CAPABILITY_NAME}:ChainSelector:${this.ChainSelector}@${ClientCapability.CAPABILITY_VERSION}` const capabilityResponse = runtime.callCapability({ capabilityId, @@ -551,6 +529,7 @@ class ClientLogTrigger implements Trigger { config: FilterLogTriggerRequest | FilterLogTriggerRequestJson, private readonly _capabilityId: string, private readonly _method: string, + private readonly ChainSelector: bigint, ) { // biome-ignore lint/suspicious/noExplicitAny: Needed for runtime type checking of protocol buffer messages this.config = (config as any).$typeName diff --git a/packages/cre-sdk/src/generator/__tests__/multi-label-generator.test.ts b/packages/cre-sdk/src/generator/__tests__/multi-label-generator.test.ts new file mode 100644 index 00000000..6dca1652 --- /dev/null +++ b/packages/cre-sdk/src/generator/__tests__/multi-label-generator.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from 'bun:test' +import { Mode } from '@cre/generated/sdk/v1alpha/sdk_pb' +import { processLabels } from '../label-utils' + +describe('Multi-Label Generator Test', () => { + it('should process multiple label types correctly', () => { + // Create a mock capability metadata with all 5 label types + const mockCapOption: any = { + mode: Mode.DON, + capabilityId: 'multi-label-test@1.0.0', + labels: { + ChainSelector: { + kind: { + case: 'uint64Label', + value: { + defaults: { + 'ethereum-mainnet': 5009297550715157269n, + 'polygon-mainnet': 4051577828743386545n, + }, + }, + }, + }, + Environment: { + kind: { + case: 'stringLabel', + value: { + defaults: { + production: 'prod', + staging: 'stage', + development: 'dev', + }, + }, + }, + }, + RegionId: { + kind: { + case: 'uint32Label', + value: { + defaults: { + 'us-east-1': 1, + 'eu-west-1': 2, + 'ap-southeast-1': 3, + }, + }, + }, + }, + Offset: { + kind: { + case: 'int32Label', + value: { + defaults: { + 'negative-offset': -100, + 'zero-offset': 0, + 'positive-offset': 100, + }, + }, + }, + }, + Timestamp: { + kind: { + case: 'int64Label', + value: { + defaults: { + past: -1234567890n, + epoch: 0n, + future: 1234567890n, + }, + }, + }, + }, + }, + } + + // Process labels + const labels = processLabels(mockCapOption) + + // Verify we got all 5 labels + expect(labels).toHaveLength(5) + + // Verify ChainSelector label + const chainSelector = labels.find((l) => l.name === 'ChainSelector') + expect(chainSelector).toBeDefined() + expect(chainSelector?.type).toBe('bigint') + expect(chainSelector?.tsType).toBe('bigint') + expect(chainSelector?.defaults).toEqual({ + 'ethereum-mainnet': 5009297550715157269n, + 'polygon-mainnet': 4051577828743386545n, + }) + + // Verify Environment label + const environment = labels.find((l) => l.name === 'Environment') + expect(environment).toBeDefined() + expect(environment?.type).toBe('string') + expect(environment?.tsType).toBe('string') + expect(environment?.defaults).toEqual({ + production: 'prod', + staging: 'stage', + development: 'dev', + }) + + // Verify RegionId label (uint32) + const regionId = labels.find((l) => l.name === 'RegionId') + expect(regionId).toBeDefined() + expect(regionId?.type).toBe('number') + expect(regionId?.tsType).toBe('number') + expect(regionId?.defaults).toEqual({ + 'us-east-1': 1, + 'eu-west-1': 2, + 'ap-southeast-1': 3, + }) + + // Verify Offset label (int32) + const offset = labels.find((l) => l.name === 'Offset') + expect(offset).toBeDefined() + expect(offset?.type).toBe('number') + expect(offset?.tsType).toBe('number') + expect(offset?.defaults).toEqual({ + 'negative-offset': -100, + 'zero-offset': 0, + 'positive-offset': 100, + }) + + // Verify Timestamp label (int64) + const timestamp = labels.find((l) => l.name === 'Timestamp') + expect(timestamp).toBeDefined() + expect(timestamp?.type).toBe('bigint') + expect(timestamp?.tsType).toBe('bigint') + expect(timestamp?.defaults).toEqual({ + past: -1234567890n, + epoch: 0n, + future: 1234567890n, + }) + }) +}) diff --git a/packages/cre-sdk/src/generator/generate-action.ts b/packages/cre-sdk/src/generator/generate-action.ts index 98071801..2416ceb7 100644 --- a/packages/cre-sdk/src/generator/generate-action.ts +++ b/packages/cre-sdk/src/generator/generate-action.ts @@ -1,4 +1,5 @@ import type { DescMethod } from '@bufbuild/protobuf' +import { generateCapabilityIdLogic, type ProcessedLabel } from './label-utils' import { wrapType } from './utils' /** @@ -7,24 +8,17 @@ import { wrapType } from './utils' * @param method - The method descriptor * @param methodName - The camelCase method name * @param capabilityClassName - The class name of the capability object - * @param hasChainSelector - Whether this capability supports chainSelector routing + * @param labels - Array of processed labels for this capability * @returns The generated action method code */ export function generateActionMethod( method: DescMethod, methodName: string, capabilityClassName: string, - hasChainSelector: boolean = false, + labels: ProcessedLabel[], modePrefix: string, ): string { - const capabilityIdLogic = hasChainSelector - ? ` - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? \`\${${capabilityClassName}.CAPABILITY_NAME}:ChainSelector:\${this.chainSelector}@\${${capabilityClassName}.CAPABILITY_VERSION}\` - : ${capabilityClassName}.CAPABILITY_ID;` - : ` - const capabilityId = ${capabilityClassName}.CAPABILITY_ID;` + const capabilityIdLogic = generateCapabilityIdLogic(labels, capabilityClassName) // Check if we have wrapped types const wrappedInputType = wrapType(method.input) diff --git a/packages/cre-sdk/src/generator/generate-sdk.ts b/packages/cre-sdk/src/generator/generate-sdk.ts index 65033511..788bd12a 100644 --- a/packages/cre-sdk/src/generator/generate-sdk.ts +++ b/packages/cre-sdk/src/generator/generate-sdk.ts @@ -12,6 +12,12 @@ import { generateActionMethod } from './generate-action' import { generateReportWrapper } from './generate-report-wrapper' import { generateActionSugarClass } from './generate-sugar' import { generateTriggerClass, generateTriggerMethod } from './generate-trigger' +import { + generateCapabilityIdLogic, + generateConstructorParams, + generateLabelSupport, + processLabels, +} from './label-utils' import { getImportPathForFile, lowerCaseFirstLetter } from './utils' const getCapabilityServiceOptions = (service: DescService): CapabilityMetadata | false => { @@ -119,9 +125,8 @@ export function generateSdk(file: GenFile, outputDir: string) { const capabilityClassName = `${service.name}Capability` - // Check if this capability supports chainSelector via labels - const chainSelectorLabel = capOption.labels?.ChainSelector as any - const hasChainSelector = chainSelectorLabel?.kind?.case === 'uint64Label' + // Process all labels from capability metadata + const labels = processLabels(capOption) // Skip legacy methods const serviceMethods = service.methods.filter((method) => { @@ -197,51 +202,26 @@ export function generateSdk(file: GenFile, outputDir: string) { methodName, capabilityClassName, service.name, - hasChainSelector, + labels, ) } // Generate action method - return generateActionMethod( - method, - methodName, - capabilityClassName, - hasChainSelector, - modePrefix, - ) + return generateActionMethod(method, methodName, capabilityClassName, labels, modePrefix) }) .join('\n') // Generate trigger classes const triggerClasses = serviceMethods .filter((method) => method.methodKind === 'server_streaming') - .map((method) => generateTriggerClass(method, service.name)) + .map((method) => generateTriggerClass(method, service.name, labels)) .join('\n') const [capabilityName, capabilityVersion] = capOption.capabilityId.split('@') - // Extract chainSelector support - let chainSelectorSupport = '' - const constructorParams: string[] = [] - - if (hasChainSelector && capOption.labels) { - const chainSelectorLabel = capOption.labels.ChainSelector as any - if ( - chainSelectorLabel?.kind?.case === 'uint64Label' && - chainSelectorLabel?.kind?.value?.defaults - ) { - const defaults = chainSelectorLabel.kind.value.defaults - chainSelectorSupport = ` - /** Available chain selectors */ - static readonly SUPPORTED_CHAINS = { -${Object.entries(defaults) - .map(([key, value]) => ` "${key}": ${value}n`) - .join(',\n')} - } as const` - - constructorParams.push('private readonly chainSelector?: bigint') - } - } + // Generate label support (constants and constructor params) + const labelSupport = generateLabelSupport(labels) + const constructorParams = generateConstructorParams(labels) const constructorCode = constructorParams.length > 0 @@ -272,7 +252,7 @@ export class ${capabilityClassName} { static readonly CAPABILITY_NAME = "${capabilityName}"; static readonly CAPABILITY_VERSION = "${capabilityVersion}"; -${chainSelectorSupport} +${labelSupport} ${constructorCode} ${methods} } diff --git a/packages/cre-sdk/src/generator/generate-trigger.ts b/packages/cre-sdk/src/generator/generate-trigger.ts index 05d93b56..72dc566b 100644 --- a/packages/cre-sdk/src/generator/generate-trigger.ts +++ b/packages/cre-sdk/src/generator/generate-trigger.ts @@ -1,4 +1,5 @@ import type { DescMethod } from '@bufbuild/protobuf' +import { generateCapabilityIdLogic, type ProcessedLabel } from './label-utils' /** * Generates the trigger method implementation for a capability @@ -7,8 +8,7 @@ import type { DescMethod } from '@bufbuild/protobuf' * @param methodName - The camelCase method name * @param capabilityId - The capability ID * @param className - The capability class name - * @param hasChainSelector - Whether this capability supports chainSelector routing - * @param hasChainSelector - Whether this capability supports chainSelector routing + * @param labels - Array of processed labels for this capability * @returns The generated trigger method code */ export function generateTriggerMethod( @@ -16,22 +16,19 @@ export function generateTriggerMethod( methodName: string, capabilityClassName: string, className: string, - hasChainSelector: boolean, + labels: ProcessedLabel[], ): string { const triggerClassName = `${className}${method.name}` - const capabilityIdLogic = hasChainSelector - ? ` - // Include chainSelector in capability ID for routing when specified - const capabilityId = this.chainSelector - ? \`\${${capabilityClassName}.CAPABILITY_NAME}:ChainSelector:\${this.chainSelector}@\${${capabilityClassName}.CAPABILITY_VERSION}\` - : ${capabilityClassName}.CAPABILITY_ID;` - : ` - const capabilityId = ${capabilityClassName}.CAPABILITY_ID;` + const capabilityIdLogic = generateCapabilityIdLogic(labels, capabilityClassName) + + // Generate label arguments to pass to trigger constructor + const labelArgs = + labels.length > 0 ? ', ' + labels.map((label) => `this.${label.name}`).join(', ') : '' return ` ${methodName}(config: ${method.input.name}Json): ${triggerClassName} { ${capabilityIdLogic} - return new ${triggerClassName}(config, capabilityId, "${method.name}"); + return new ${triggerClassName}(config, capabilityId, "${method.name}"${labelArgs}); }` } @@ -39,13 +36,23 @@ export function generateTriggerMethod( * Generates the trigger class implementation * * @param method - The method descriptor - * @param capabilityId - The capability ID * @param className - The capability class name + * @param labels - Array of processed labels for this capability * @returns The generated trigger class code */ -export function generateTriggerClass(method: DescMethod, className: string): string { +export function generateTriggerClass( + method: DescMethod, + className: string, + labels: ProcessedLabel[], +): string { const triggerClassName = `${className}${method.name}` + // Generate label parameters for constructor only (no duplicate fields needed) + const labelParams = + labels.length > 0 + ? labels.map((label) => `private readonly ${label.name}: ${label.tsType}`).join(',\n ') + : '' + return ` /** * Trigger implementation for ${method.name} @@ -55,7 +62,8 @@ class ${triggerClassName} implements Trigger<${method.output.name}, ${method.out constructor( config: ${method.input.name} | ${method.input.name}Json, private readonly _capabilityId: string, - private readonly _method: string + private readonly _method: string, +${labelParams ? ' ' + labelParams + ',' : ''} ) { // biome-ignore lint/suspicious/noExplicitAny: Needed for runtime type checking of protocol buffer messages this.config = (config as any).$typeName ? config as ${method.input.name} : fromJson(${method.input.name}Schema, config as ${method.input.name}Json) diff --git a/packages/cre-sdk/src/generator/label-utils.ts b/packages/cre-sdk/src/generator/label-utils.ts new file mode 100644 index 00000000..80467460 --- /dev/null +++ b/packages/cre-sdk/src/generator/label-utils.ts @@ -0,0 +1,169 @@ +import type { CapabilityMetadata } from '@cre/generated/tools/generator/v1alpha/cre_metadata_pb' + +/** + * Represents a processed label with TypeScript-specific information + */ +export interface ProcessedLabel { + name: string // e.g., "ChainSelector" + type: 'string' | 'bigint' | 'number' + tsType: string // TypeScript type string + defaults?: Record + formatExpression: string // How to convert to string for capability ID +} + +/** + * Extracts and processes all labels from capability metadata + */ +export function processLabels(capOption: CapabilityMetadata): ProcessedLabel[] { + if (!capOption.labels || Object.keys(capOption.labels).length === 0) { + return [] + } + + const labels: ProcessedLabel[] = [] + + // Sort labels alphabetically for consistency + const sortedLabelNames = Object.keys(capOption.labels).sort() + + for (const labelName of sortedLabelNames) { + const label = capOption.labels[labelName] as any + + if (!label?.kind) continue + + const processed = processLabel(labelName, label) + if (processed) { + labels.push(processed) + } + } + + return labels +} + +/** + * Process a single label based on its type + */ +function processLabel(name: string, label: any): ProcessedLabel | null { + const kindCase = label.kind.case + const kindValue = label.kind.value + + switch (kindCase) { + case 'stringLabel': + return { + name, + type: 'string', + tsType: 'string', + defaults: kindValue?.defaults || undefined, + formatExpression: `\${this.${name}}`, + } + + case 'uint64Label': + return { + name, + type: 'bigint', + tsType: 'bigint', + defaults: kindValue?.defaults || undefined, + formatExpression: `\${this.${name}}`, + } + + case 'uint32Label': + return { + name, + type: 'number', + tsType: 'number', + defaults: kindValue?.defaults || undefined, + formatExpression: `\${this.${name}}`, + } + + case 'int64Label': + return { + name, + type: 'bigint', + tsType: 'bigint', + defaults: kindValue?.defaults || undefined, + formatExpression: `\${this.${name}}`, + } + + case 'int32Label': + return { + name, + type: 'number', + tsType: 'number', + defaults: kindValue?.defaults || undefined, + formatExpression: `\${this.${name}}`, + } + + default: + console.warn(`Unsupported label type: ${kindCase} for label: ${name}`) + return null + } +} + +/** + * Generate capability ID logic that includes all labels + */ +export function generateCapabilityIdLogic( + labels: ProcessedLabel[], + capabilityClassName: string, +): string { + if (labels.length === 0) { + return ` + const capabilityId = ${capabilityClassName}.CAPABILITY_ID;` + } + + // Build the capability ID with all labels + // Format: "name:Label1Name:label1Value:Label2Name:label2Value@version" + const labelParts = labels.map((label) => `:${label.name}:${label.formatExpression}`).join('') + + return ` + // Include all labels in capability ID for routing when specified + const capabilityId = \`\${${capabilityClassName}.CAPABILITY_NAME}${labelParts}@\${${capabilityClassName}.CAPABILITY_VERSION}\`;` +} + +/** + * Generate constructor parameters for all labels + */ +export function generateConstructorParams(labels: ProcessedLabel[]): string[] { + return labels.map((label) => `private readonly ${label.name}: ${label.tsType}`) +} + +/** + * Generate constants and helper functions for label defaults + */ +export function generateLabelSupport(labels: ProcessedLabel[]): string { + if (labels.length === 0) return '' + + const sections: string[] = [] + + for (const label of labels) { + if (!label.defaults || Object.keys(label.defaults).length === 0) { + continue + } + + // Generate SUPPORTED_* constants object + const constantsName = `SUPPORTED_${toScreamingSnakeCase(label.name)}S` + const entries = Object.entries(label.defaults) + .map(([key, value]) => { + const formattedValue = + label.type === 'bigint' ? `${value}n` : label.type === 'string' ? `"${value}"` : value + return ` "${key}": ${formattedValue}` + }) + .join(',\n') + + sections.push(` + /** Available ${label.name} values */ + static readonly ${constantsName} = { +${entries} + } as const`) + } + + return sections.join('\n') +} + +/** + * Convert camelCase to SCREAMING_SNAKE_CASE + */ +function toScreamingSnakeCase(str: string): string { + return str + .replace(/([A-Z])/g, '_$1') + .replace(/^_/, '') + .toUpperCase() +} diff --git a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts new file mode 100644 index 00000000..96e9c9c4 --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, test } from 'bun:test' +import { EVMClient } from '@cre/sdk/cre' +import { + type EncodeCallMsgPayload, + EVM_DEFAULT_REPORT_ENCODER, + encodeCallMsg, + isChainSelectorSupported, + LAST_FINALIZED_BLOCK_NUMBER, + LATEST_BLOCK_NUMBER, + prepareReportRequest, +} from './blockchain-helpers' + +describe('blockchain-helpers', () => { + describe('LAST_FINALIZED_BLOCK_NUMBER', () => { + test('should have correct structure for finalized block', () => { + expect(LAST_FINALIZED_BLOCK_NUMBER).toEqual({ + absVal: Buffer.from([3]).toString('base64'), + sign: '-1', + }) + }) + + test('should encode value 3 as base64', () => { + expect(LAST_FINALIZED_BLOCK_NUMBER.absVal).toBe('Aw==') + }) + }) + + describe('LATEST_BLOCK_NUMBER', () => { + test('should have correct structure for latest block', () => { + expect(LATEST_BLOCK_NUMBER).toEqual({ + absVal: Buffer.from([2]).toString('base64'), + sign: '-1', + }) + }) + + test('should encode value 2 as base64', () => { + expect(LATEST_BLOCK_NUMBER.absVal).toBe('Ag==') + }) + }) + + describe('encodeCallMsg', () => { + test('should encode call message with valid addresses and data', () => { + const payload: EncodeCallMsgPayload = { + from: '0x1234567890123456789012345678901234567890', + to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + data: '0x095ea7b3', + } + + const result = encodeCallMsg(payload) + + expect(result).toHaveProperty('from') + expect(result).toHaveProperty('to') + expect(result).toHaveProperty('data') + expect(typeof result.from).toBe('string') + expect(typeof result.to).toBe('string') + expect(typeof result.data).toBe('string') + }) + + test('should encode zero address', () => { + const payload: EncodeCallMsgPayload = { + from: '0x0000000000000000000000000000000000000000', + to: '0x0000000000000000000000000000000000000000', + data: '0x', + } + + const result = encodeCallMsg(payload) + + expect(result.from).toBeTruthy() + expect(result.to).toBeTruthy() + expect(result.data).toBeDefined() // Empty hex should encode to empty string + }) + + test('should encode function selector data', () => { + const payload: EncodeCallMsgPayload = { + from: '0x1234567890123456789012345678901234567890', + to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + data: '0xa9059cbb000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000000000000000064', + } + + const result = encodeCallMsg(payload) + + expect(result.data).toBeTruthy() + expect(result.data).toBeDefined() + if (result.data) { + expect(result.data.length).toBeGreaterThan(0) + } + }) + }) + + describe('EVM_DEFAULT_REPORT_ENCODER', () => { + test('should have correct default values', () => { + expect(EVM_DEFAULT_REPORT_ENCODER).toEqual({ + encoderName: 'evm', + signingAlgo: 'ecdsa', + hashingAlgo: 'keccak256', + }) + }) + + test('should have evm encoder name', () => { + expect(EVM_DEFAULT_REPORT_ENCODER.encoderName).toBe('evm') + }) + + test('should use ecdsa signing algorithm', () => { + expect(EVM_DEFAULT_REPORT_ENCODER.signingAlgo).toBe('ecdsa') + }) + + test('should use keccak256 hashing algorithm', () => { + expect(EVM_DEFAULT_REPORT_ENCODER.hashingAlgo).toBe('keccak256') + }) + }) + + describe('prepareReportRequest', () => { + test('should prepare report request with default encoder', () => { + const hexPayload = '0x1234567890abcdef' as const + + const result = prepareReportRequest(hexPayload) + + expect(result).toHaveProperty('encodedPayload') + expect(result).toHaveProperty('encoderName') + expect(result).toHaveProperty('signingAlgo') + expect(result).toHaveProperty('hashingAlgo') + expect(result.encoderName).toBe('evm') + expect(result.signingAlgo).toBe('ecdsa') + expect(result.hashingAlgo).toBe('keccak256') + }) + + test('should prepare report request with custom encoder', () => { + const hexPayload = '0xabcdef' as const + const customEncoder = { + encoderName: 'custom', + signingAlgo: 'ed25519', + hashingAlgo: 'sha256', + } + + const result = prepareReportRequest(hexPayload, customEncoder) + + expect(result.encoderName).toBe('custom') + expect(result.signingAlgo).toBe('ed25519') + expect(result.hashingAlgo).toBe('sha256') + expect(result.encodedPayload).toBeTruthy() + }) + + test('should encode payload as base64', () => { + const hexPayload = '0x00' as const + + const result = prepareReportRequest(hexPayload) + + expect(typeof result.encodedPayload).toBe('string') + expect(result.encodedPayload).toBeDefined() + if (result.encodedPayload) { + expect(result.encodedPayload.length).toBeGreaterThan(0) + } + }) + + test('should handle empty hex payload', () => { + const hexPayload = '0x' as const + + const result = prepareReportRequest(hexPayload) + + expect(result.encodedPayload).toBeDefined() + }) + }) + + describe('isChainSelectorSupported', () => { + test('should return true for supported chain selectors', () => { + // Get all supported chain selectors from EVMClient + const supportedChains = Object.keys(EVMClient.SUPPORTED_CHAIN_SELECTORS) + + if (supportedChains.length > 0) { + const firstChain = supportedChains[0] + expect(isChainSelectorSupported(firstChain)).toBe(true) + } + }) + + test('should return false for unsupported chain selectors', () => { + const unsupportedChain = 'DEFINITELY_NOT_A_REAL_CHAIN_12345' + expect(isChainSelectorSupported(unsupportedChain)).toBe(false) + }) + + test('should return false for empty string', () => { + expect(isChainSelectorSupported('')).toBe(false) + }) + + test('should be case sensitive', () => { + const supportedChains = Object.keys(EVMClient.SUPPORTED_CHAIN_SELECTORS) + + if (supportedChains.length > 0) { + const firstChain = supportedChains[0] + const lowerCase = firstChain.toLowerCase() + const upperCase = firstChain.toUpperCase() + + // Only fail if the case-modified version is actually different + if (lowerCase !== firstChain) { + expect(isChainSelectorSupported(lowerCase)).toBe(false) + } + if (upperCase !== firstChain) { + expect(isChainSelectorSupported(upperCase)).toBe(false) + } + } + }) + + test('should handle multiple supported chains', () => { + const supportedChains = Object.keys(EVMClient.SUPPORTED_CHAIN_SELECTORS) + + supportedChains.forEach((chain) => { + expect(isChainSelectorSupported(chain)).toBe(true) + }) + }) + }) +}) diff --git a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts index bc0e1900..322e7d30 100644 --- a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts +++ b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts @@ -1,5 +1,6 @@ import type { CallMsgJson } from '@cre/generated/capabilities/blockchain/evm/v1alpha/client_pb' import type { ReportRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb' +import { EVMClient } from '@cre/sdk/cre' import { hexToBase64 } from '@cre/sdk/utils/hex-utils' import type { Address, Hex } from 'viem' @@ -82,3 +83,6 @@ export const prepareReportRequest = ( encodedPayload: hexToBase64(hexEncodedPayload), ...reportEncoder, }) + +export const isChainSelectorSupported = (chainSelectorName: string) => + Object.keys(EVMClient.SUPPORTED_CHAIN_SELECTORS).includes(chainSelectorName) diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/getAllNetworks.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/get-all-networks.ts similarity index 100% rename from packages/cre-sdk/src/sdk/utils/chain-selectors/getAllNetworks.ts rename to packages/cre-sdk/src/sdk/utils/chain-selectors/get-all-networks.ts diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.test.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.test.ts new file mode 100644 index 00000000..f2b24631 --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.test.ts @@ -0,0 +1,332 @@ +import { describe, expect, it } from "bun:test"; +import { NetworkLookup } from "./network-lookup"; + +// Mock the generated networks module with deterministic fixtures +const evmMain = { + chainId: "1", + chainSelector: { name: "EVM_MAIN", selector: 100n }, + chainFamily: "evm", + networkType: "mainnet", +} as const; + +const evmTest = { + chainId: "5", + chainSelector: { name: "EVM_TEST", selector: 200n }, + chainFamily: "evm", + networkType: "testnet", +} as const; + +const solMain = { + chainId: "sol-main", + chainSelector: { name: "SOL_MAIN", selector: 300n }, + chainFamily: "solana", + networkType: "mainnet", +} as const; + +const solTest = { + chainId: "sol-test", + chainSelector: { name: "SOL_TEST", selector: 400n }, + chainFamily: "solana", + networkType: "testnet", +} as const; + +// Create all maps required by getNetwork +const mainnetBySelector = new Map([ + [evmMain.chainSelector.selector, evmMain], + [solMain.chainSelector.selector, solMain], +]); +const testnetBySelector = new Map([ + [evmTest.chainSelector.selector, evmTest], + [solTest.chainSelector.selector, solTest], +]); +const mainnetByName = new Map([ + [evmMain.chainSelector.name, evmMain], + [solMain.chainSelector.name, solMain], +]); +const testnetByName = new Map([ + [evmTest.chainSelector.name, evmTest], + [solTest.chainSelector.name, solTest], +]); + +const mainnetBySelectorByFamily = { + evm: new Map([[evmMain.chainSelector.selector, evmMain]]), + solana: new Map([[solMain.chainSelector.selector, solMain]]), + aptos: new Map(), + sui: new Map(), + ton: new Map(), + tron: new Map(), +} as const; + +const testnetBySelectorByFamily = { + evm: new Map([[evmTest.chainSelector.selector, evmTest]]), + solana: new Map([[solTest.chainSelector.selector, solTest]]), + aptos: new Map(), + sui: new Map(), + ton: new Map(), + tron: new Map(), +} as const; + +const mainnetByNameByFamily = { + evm: new Map([[evmMain.chainSelector.name, evmMain]]), + solana: new Map([[solMain.chainSelector.name, solMain]]), + aptos: new Map(), + sui: new Map(), + ton: new Map(), + tron: new Map(), +} as const; + +const testnetByNameByFamily = { + evm: new Map([[evmTest.chainSelector.name, evmTest]]), + solana: new Map([[solTest.chainSelector.name, solTest]]), + aptos: new Map(), + sui: new Map(), + ton: new Map(), + tron: new Map(), +} as const; + +const lookup = new NetworkLookup({ + mainnetByName, + mainnetByNameByFamily, + mainnetBySelector, + mainnetBySelectorByFamily, + testnetByName, + testnetByNameByFamily, + testnetBySelector, + testnetBySelectorByFamily, +}); + +const getNetwork = (opts: any) => lookup.find(opts); + +describe("getNetwork", () => { + it("returns undefined when neither chainSelector nor chainSelectorName provided", () => { + expect(getNetwork({})).toBeUndefined(); + }); + + // chainFamily + chainSelector + it("uses family+selector with isTestnet=true (testnet family map)", () => { + const result = getNetwork({ + chainFamily: "evm", + chainSelector: 200n, + isTestnet: true, + }); + expect(result).toEqual(evmTest); + }); + + it("uses family+selector with isTestnet=false (mainnet family map)", () => { + const result = getNetwork({ + chainFamily: "evm", + chainSelector: 100n, + isTestnet: false, + }); + expect(result).toEqual(evmMain); + }); + + it("uses family+selector with isTestnet undefined defaults to mainnet map", () => { + const result = getNetwork({ chainFamily: "solana", chainSelector: 300n }); + expect(result).toEqual(solMain); + }); + + it("uses family+selector with isTestnet undefined prefers testnet when present", () => { + const result = getNetwork({ chainFamily: "evm", chainSelector: 200n }); + expect(result).toEqual(evmTest); + }); + + it("family+selector returns undefined when selector not in that family", () => { + const result = getNetwork({ + chainFamily: "evm", + chainSelector: 300n, + isTestnet: false, + }); + expect(result).toBeUndefined(); + }); + + // chainFamily + chainSelectorName + it("uses family+name with isTestnet=true (testnet family map)", () => { + const result = getNetwork({ + chainFamily: "solana", + chainSelectorName: "SOL_TEST", + isTestnet: true, + }); + expect(result).toEqual(solTest); + }); + + it("uses family+name with isTestnet=false (mainnet family map)", () => { + const result = getNetwork({ + chainFamily: "solana", + chainSelectorName: "SOL_MAIN", + isTestnet: false, + }); + expect(result).toEqual(solMain); + }); + + it("uses family+name with isTestnet undefined defaults to mainnet map", () => { + const result = getNetwork({ + chainFamily: "evm", + chainSelectorName: "EVM_MAIN", + }); + expect(result).toEqual(evmMain); + }); + + it("uses family+name with isTestnet undefined prefers testnet when present", () => { + const result = getNetwork({ + chainFamily: "evm", + chainSelectorName: "EVM_TEST", + }); + expect(result).toEqual(evmTest); + }); + + it("family+name returns undefined when name not in that family", () => { + const result = getNetwork({ + chainFamily: "solana", + chainSelectorName: "EVM_MAIN", + isTestnet: false, + }); + expect(result).toBeUndefined(); + }); + + // selector only + it("selector only with isTestnet=false returns mainnet", () => { + const result = getNetwork({ chainSelector: 100n, isTestnet: false }); + expect(result).toEqual(evmMain); + }); + + it("selector only with isTestnet=true returns testnet", () => { + const result = getNetwork({ chainSelector: 200n, isTestnet: true }); + expect(result).toEqual(evmTest); + }); + + it("selector only with isTestnet undefined prefers testnet if exists", () => { + // create duplicate selector present in both maps to assert preference + const dupMain = { + ...evmMain, + chainSelector: { + ...evmMain.chainSelector, + selector: 900n, + name: "DUP_MAIN", + }, + }; + const dupTest = { + ...evmTest, + chainSelector: { + ...evmTest.chainSelector, + selector: 900n, + name: "DUP_TEST", + }, + }; + mainnetBySelector.set(900n, dupMain); + testnetBySelector.set(900n, dupTest); + + const result = getNetwork({ chainSelector: 900n }); + expect(result).toEqual(dupTest); + }); + + it("selector only with isTestnet undefined falls back to mainnet if not in testnet", () => { + const result = getNetwork({ chainSelector: 300n }); + expect(result).toEqual(solMain); + }); + + it("selector only returns undefined when not found anywhere", () => { + const result = getNetwork({ chainSelector: 9999n }); + expect(result).toBeUndefined(); + }); + + // both selector and name provided - selector takes precedence + it("both selector and name provided without family uses selector path", () => { + const result = getNetwork({ + chainSelector: 100n, + chainSelectorName: "SOL_MAIN", + isTestnet: false, + }); + expect(result).toEqual(evmMain); + }); + + it("both selector and name provided with family uses selector path", () => { + const result = getNetwork({ + chainFamily: "solana", + chainSelector: 300n, + chainSelectorName: "EVM_MAIN", + }); + expect(result).toEqual(solMain); + }); + + it("both selector and name provided prefers testnet by selector when isTestnet undefined", () => { + // ensure duplicate in both maps like earlier + const dupMain = { + ...evmMain, + chainSelector: { + ...evmMain.chainSelector, + selector: 901n, + name: "DUP2_MAIN", + }, + }; + const dupTest = { + ...evmTest, + chainSelector: { + ...evmTest.chainSelector, + selector: 901n, + name: "DUP2_TEST", + }, + }; + mainnetBySelector.set(901n, dupMain); + testnetBySelector.set(901n, dupTest); + + const result = getNetwork({ + chainSelector: 901n, + chainSelectorName: "DUP2_MAIN", + }); + expect(result).toEqual(dupTest); + }); + + // name only + it("name only with isTestnet=false returns mainnet", () => { + const result = getNetwork({ + chainSelectorName: "EVM_MAIN", + isTestnet: false, + }); + expect(result).toEqual(evmMain); + }); + + it("name only with isTestnet=true returns testnet", () => { + const result = getNetwork({ + chainSelectorName: "EVM_TEST", + isTestnet: true, + }); + expect(result).toEqual(evmTest); + }); + + it("name only with isTestnet undefined prefers testnet if exists", () => { + // For names, ensure both entries exist for a shared name key + mainnetByName.set("DUP", evmMain); + testnetByName.set("DUP", evmTest); + const result = getNetwork({ chainSelectorName: "DUP" }); + expect(result).toEqual(evmTest); + }); + + it("name only with isTestnet undefined falls back to mainnet if not in testnet", () => { + const result = getNetwork({ chainSelectorName: "SOL_MAIN" }); + expect(result).toEqual(solMain); + }); + + it("returns undefined for non-existent chainSelectorName", () => { + const result = getNetwork({ chainSelectorName: "UNKNOWN_NAME" }); + expect(result).toBeUndefined(); + }); + + it("returns undefined for unsupported family when maps are empty (selector)", () => { + const result = getNetwork({ + chainFamily: "aptos", + chainSelector: 100n, + isTestnet: false, + }); + expect(result).toBeUndefined(); + }); + + it("returns undefined for unsupported family when maps are empty (name)", () => { + const result = getNetwork({ + chainFamily: "aptos", + chainSelectorName: "EVM_MAIN", + isTestnet: false, + }); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.ts new file mode 100644 index 00000000..5d1ed616 --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/chain-selectors/get-network.ts @@ -0,0 +1,31 @@ +import { + mainnetByName, + mainnetByNameByFamily, + mainnetBySelector, + mainnetBySelectorByFamily, + testnetByName, + testnetByNameByFamily, + testnetBySelector, + testnetBySelectorByFamily, +} from '@cre/generated/networks' +import { type GetNetworkOptions, NetworkLookup } from './network-lookup' +import type { NetworkInfo } from './types' + +const defaultLookup = new NetworkLookup({ + mainnetByName, + mainnetByNameByFamily, + mainnetBySelector, + mainnetBySelectorByFamily, + testnetByName, + testnetByNameByFamily, + testnetBySelector, + testnetBySelectorByFamily, +}) + +/** + * High-performance network lookup using Maps for O(1) performance + * @param options - Search criteria + * @returns NetworkInfo if found, undefined otherwise + */ +export const getNetwork = (options: GetNetworkOptions): NetworkInfo | undefined => + defaultLookup.find(options) diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.test.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.test.ts deleted file mode 100644 index a3f9edc9..00000000 --- a/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { describe, expect, it, mock } from 'bun:test' - -// Mock the generated networks module with deterministic fixtures -const mockModulePath = '@cre/generated/networks' - -const evmMain = { - chainId: '1', - chainSelector: { name: 'EVM_MAIN', selector: 100n }, - chainFamily: 'evm', - networkType: 'mainnet', -} as const - -const evmTest = { - chainId: '5', - chainSelector: { name: 'EVM_TEST', selector: 200n }, - chainFamily: 'evm', - networkType: 'testnet', -} as const - -const solMain = { - chainId: 'sol-main', - chainSelector: { name: 'SOL_MAIN', selector: 300n }, - chainFamily: 'solana', - networkType: 'mainnet', -} as const - -const solTest = { - chainId: 'sol-test', - chainSelector: { name: 'SOL_TEST', selector: 400n }, - chainFamily: 'solana', - networkType: 'testnet', -} as const - -// Create all maps required by getNetwork -const mainnetBySelector = new Map([ - [evmMain.chainSelector.selector, evmMain], - [solMain.chainSelector.selector, solMain], -]) -const testnetBySelector = new Map([ - [evmTest.chainSelector.selector, evmTest], - [solTest.chainSelector.selector, solTest], -]) -const mainnetByName = new Map([ - [evmMain.chainSelector.name, evmMain], - [solMain.chainSelector.name, solMain], -]) -const testnetByName = new Map([ - [evmTest.chainSelector.name, evmTest], - [solTest.chainSelector.name, solTest], -]) - -const mainnetBySelectorByFamily = { - evm: new Map([[evmMain.chainSelector.selector, evmMain]]), - solana: new Map([[solMain.chainSelector.selector, solMain]]), - aptos: new Map(), - sui: new Map(), - ton: new Map(), - tron: new Map(), -} as const - -const testnetBySelectorByFamily = { - evm: new Map([[evmTest.chainSelector.selector, evmTest]]), - solana: new Map([[solTest.chainSelector.selector, solTest]]), - aptos: new Map(), - sui: new Map(), - ton: new Map(), - tron: new Map(), -} as const - -const mainnetByNameByFamily = { - evm: new Map([[evmMain.chainSelector.name, evmMain]]), - solana: new Map([[solMain.chainSelector.name, solMain]]), - aptos: new Map(), - sui: new Map(), - ton: new Map(), - tron: new Map(), -} as const - -const testnetByNameByFamily = { - evm: new Map([[evmTest.chainSelector.name, evmTest]]), - solana: new Map([[solTest.chainSelector.name, solTest]]), - aptos: new Map(), - sui: new Map(), - ton: new Map(), - tron: new Map(), -} as const - -// Install module mock before importing the SUT -mock.module(mockModulePath, () => ({ - mainnetByName, - mainnetByNameByFamily, - mainnetBySelector, - mainnetBySelectorByFamily, - testnetByName, - testnetByNameByFamily, - testnetBySelector, - testnetBySelectorByFamily, -})) - -const { getNetwork } = await import('./getNetwork') - -describe('getNetwork', () => { - it('returns undefined when neither chainSelector nor chainSelectorName provided', () => { - expect(getNetwork({})).toBeUndefined() - }) - - // chainFamily + chainSelector - it('uses family+selector with isTestnet=true (testnet family map)', () => { - const result = getNetwork({ - chainFamily: 'evm', - chainSelector: 200n, - isTestnet: true, - }) - expect(result).toEqual(evmTest) - }) - - it('uses family+selector with isTestnet=false (mainnet family map)', () => { - const result = getNetwork({ - chainFamily: 'evm', - chainSelector: 100n, - isTestnet: false, - }) - expect(result).toEqual(evmMain) - }) - - it('uses family+selector with isTestnet undefined defaults to mainnet map', () => { - const result = getNetwork({ chainFamily: 'solana', chainSelector: 300n }) - expect(result).toEqual(solMain) - }) - - it('uses family+selector with isTestnet undefined prefers testnet when present', () => { - const result = getNetwork({ chainFamily: 'evm', chainSelector: 200n }) - expect(result).toEqual(evmTest) - }) - - it('family+selector returns undefined when selector not in that family', () => { - const result = getNetwork({ - chainFamily: 'evm', - chainSelector: 300n, - isTestnet: false, - }) - expect(result).toBeUndefined() - }) - - // chainFamily + chainSelectorName - it('uses family+name with isTestnet=true (testnet family map)', () => { - const result = getNetwork({ - chainFamily: 'solana', - chainSelectorName: 'SOL_TEST', - isTestnet: true, - }) - expect(result).toEqual(solTest) - }) - - it('uses family+name with isTestnet=false (mainnet family map)', () => { - const result = getNetwork({ - chainFamily: 'solana', - chainSelectorName: 'SOL_MAIN', - isTestnet: false, - }) - expect(result).toEqual(solMain) - }) - - it('uses family+name with isTestnet undefined defaults to mainnet map', () => { - const result = getNetwork({ - chainFamily: 'evm', - chainSelectorName: 'EVM_MAIN', - }) - expect(result).toEqual(evmMain) - }) - - it('uses family+name with isTestnet undefined prefers testnet when present', () => { - const result = getNetwork({ - chainFamily: 'evm', - chainSelectorName: 'EVM_TEST', - }) - expect(result).toEqual(evmTest) - }) - - it('family+name returns undefined when name not in that family', () => { - const result = getNetwork({ - chainFamily: 'solana', - chainSelectorName: 'EVM_MAIN', - isTestnet: false, - }) - expect(result).toBeUndefined() - }) - - // selector only - it('selector only with isTestnet=false returns mainnet', () => { - const result = getNetwork({ chainSelector: 100n, isTestnet: false }) - expect(result).toEqual(evmMain) - }) - - it('selector only with isTestnet=true returns testnet', () => { - const result = getNetwork({ chainSelector: 200n, isTestnet: true }) - expect(result).toEqual(evmTest) - }) - - it('selector only with isTestnet undefined prefers testnet if exists', () => { - // create duplicate selector present in both maps to assert preference - const dupMain = { - ...evmMain, - chainSelector: { - ...evmMain.chainSelector, - selector: 900n, - name: 'DUP_MAIN', - }, - } - const dupTest = { - ...evmTest, - chainSelector: { - ...evmTest.chainSelector, - selector: 900n, - name: 'DUP_TEST', - }, - } - mainnetBySelector.set(900n, dupMain) - testnetBySelector.set(900n, dupTest) - - const result = getNetwork({ chainSelector: 900n }) - expect(result).toEqual(dupTest) - }) - - it('selector only with isTestnet undefined falls back to mainnet if not in testnet', () => { - const result = getNetwork({ chainSelector: 300n }) - expect(result).toEqual(solMain) - }) - - it('selector only returns undefined when not found anywhere', () => { - const result = getNetwork({ chainSelector: 9999n }) - expect(result).toBeUndefined() - }) - - // both selector and name provided - selector takes precedence - it('both selector and name provided without family uses selector path', () => { - const result = getNetwork({ - chainSelector: 100n, - chainSelectorName: 'SOL_MAIN', - isTestnet: false, - }) - expect(result).toEqual(evmMain) - }) - - it('both selector and name provided with family uses selector path', () => { - const result = getNetwork({ - chainFamily: 'solana', - chainSelector: 300n, - chainSelectorName: 'EVM_MAIN', - }) - expect(result).toEqual(solMain) - }) - - it('both selector and name provided prefers testnet by selector when isTestnet undefined', () => { - // ensure duplicate in both maps like earlier - const dupMain = { - ...evmMain, - chainSelector: { - ...evmMain.chainSelector, - selector: 901n, - name: 'DUP2_MAIN', - }, - } - const dupTest = { - ...evmTest, - chainSelector: { - ...evmTest.chainSelector, - selector: 901n, - name: 'DUP2_TEST', - }, - } - mainnetBySelector.set(901n, dupMain) - testnetBySelector.set(901n, dupTest) - - const result = getNetwork({ - chainSelector: 901n, - chainSelectorName: 'DUP2_MAIN', - }) - expect(result).toEqual(dupTest) - }) - - // name only - it('name only with isTestnet=false returns mainnet', () => { - const result = getNetwork({ - chainSelectorName: 'EVM_MAIN', - isTestnet: false, - }) - expect(result).toEqual(evmMain) - }) - - it('name only with isTestnet=true returns testnet', () => { - const result = getNetwork({ - chainSelectorName: 'EVM_TEST', - isTestnet: true, - }) - expect(result).toEqual(evmTest) - }) - - it('name only with isTestnet undefined prefers testnet if exists', () => { - // For names, ensure both entries exist for a shared name key - mainnetByName.set('DUP', evmMain) - testnetByName.set('DUP', evmTest) - const result = getNetwork({ chainSelectorName: 'DUP' }) - expect(result).toEqual(evmTest) - }) - - it('name only with isTestnet undefined falls back to mainnet if not in testnet', () => { - const result = getNetwork({ chainSelectorName: 'SOL_MAIN' }) - expect(result).toEqual(solMain) - }) - - it('returns undefined for non-existent chainSelectorName', () => { - const result = getNetwork({ chainSelectorName: 'UNKNOWN_NAME' }) - expect(result).toBeUndefined() - }) - - it('returns undefined for unsupported family when maps are empty (selector)', () => { - const result = getNetwork({ - chainFamily: 'aptos', - chainSelector: 100n, - isTestnet: false, - }) - expect(result).toBeUndefined() - }) - - it('returns undefined for unsupported family when maps are empty (name)', () => { - const result = getNetwork({ - chainFamily: 'aptos', - chainSelectorName: 'EVM_MAIN', - isTestnet: false, - }) - expect(result).toBeUndefined() - }) -}) diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.ts deleted file mode 100644 index f3996be2..00000000 --- a/packages/cre-sdk/src/sdk/utils/chain-selectors/getNetwork.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - mainnetByName, - mainnetByNameByFamily, - mainnetBySelector, - mainnetBySelectorByFamily, - testnetByName, - testnetByNameByFamily, - testnetBySelector, - testnetBySelectorByFamily, -} from '@cre/generated/networks' -import type { ChainFamily, NetworkInfo } from './types' - -interface GetNetworkOptions { - chainSelector?: bigint - chainSelectorName?: string - isTestnet?: boolean | undefined - chainFamily?: ChainFamily -} - -/** - * High-performance network lookup using Maps for O(1) performance - * @param options - Search criteria - * @returns NetworkInfo if found, undefined otherwise - */ -export const getNetwork = (options: GetNetworkOptions): NetworkInfo | undefined => { - const { chainSelector, chainSelectorName, isTestnet, chainFamily } = options - - const getBySelector = (map: Map) => { - if (chainSelector === undefined) return undefined - return map.get(chainSelector) - } - - // Validate input - need either chainSelector or chainSelectorName - if (!chainSelector && !chainSelectorName) { - return undefined - } - - // If both chainFamily and network type are specified, use the most specific maps - if (chainFamily && chainSelector !== undefined) { - if (isTestnet === false) { - return getBySelector(mainnetBySelectorByFamily[chainFamily]) - } - - if (isTestnet === true) { - return getBySelector(testnetBySelectorByFamily[chainFamily]) - } - - // If user haven't defined if it's testnet or not we can try all networks, starting from testnet - let network = getBySelector(testnetBySelectorByFamily[chainFamily]) - if (!network) { - network = getBySelector(mainnetBySelectorByFamily[chainFamily]) - } - return network - } - - if (chainFamily && chainSelectorName) { - if (isTestnet === false) { - return mainnetByNameByFamily[chainFamily].get(chainSelectorName) - } - - if (isTestnet === true) { - return testnetByNameByFamily[chainFamily].get(chainSelectorName) - } - - // If user haven't defined if it's testnet or not we can try all networks, starting from testnet - let network = testnetByNameByFamily[chainFamily].get(chainSelectorName) - if (!network) { - network = mainnetByNameByFamily[chainFamily].get(chainSelectorName) - } - return network - } - - // If only network type is specified, use the general maps - if (chainSelector !== undefined) { - if (isTestnet === false) { - return getBySelector(mainnetBySelector) - } - - if (isTestnet === true) { - return getBySelector(testnetBySelector) - } - - // If user haven't defined if it's testnet or not we can try all networks, starting from testnet - let network = getBySelector(testnetBySelector) - if (!network) { - network = getBySelector(mainnetBySelector) - } - return network - } - - if (chainSelectorName) { - if (isTestnet === false) { - return mainnetByName.get(chainSelectorName) - } - - if (isTestnet === true) { - return testnetByName.get(chainSelectorName) - } - - // If user haven't defined if it's testnet or not we can try all networks, starting from testnet - let network = testnetByName.get(chainSelectorName) - if (!network) { - network = mainnetByName.get(chainSelectorName) - } - return network - } - - return undefined -} diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/index.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/index.ts index 73feaebb..2bb80ec4 100644 --- a/packages/cre-sdk/src/sdk/utils/chain-selectors/index.ts +++ b/packages/cre-sdk/src/sdk/utils/chain-selectors/index.ts @@ -1,3 +1,3 @@ -export * from './getAllNetworks' -export * from './getNetwork' +export * from './get-all-networks' +export * from './get-network' export * from './types' diff --git a/packages/cre-sdk/src/sdk/utils/chain-selectors/network-lookup.ts b/packages/cre-sdk/src/sdk/utils/chain-selectors/network-lookup.ts new file mode 100644 index 00000000..d4e6c84f --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/chain-selectors/network-lookup.ts @@ -0,0 +1,119 @@ +import type { ChainFamily, NetworkInfo } from './types' + +export interface GetNetworkOptions { + chainSelector?: bigint + chainSelectorName?: string + isTestnet?: boolean | undefined + chainFamily?: ChainFamily +} + +export type NetworkMapBySelector = Map +export type NetworkMapByName = Map +export type NetworkFamilyMapBySelector = Record +export type NetworkFamilyMapByName = Record + +export interface NetworkMaps { + mainnetByName: NetworkMapByName + mainnetByNameByFamily: NetworkFamilyMapByName + mainnetBySelector: NetworkMapBySelector + mainnetBySelectorByFamily: NetworkFamilyMapBySelector + testnetByName: NetworkMapByName + testnetByNameByFamily: NetworkFamilyMapByName + testnetBySelector: NetworkMapBySelector + testnetBySelectorByFamily: NetworkFamilyMapBySelector +} + +export class NetworkLookup { + constructor(private maps: NetworkMaps) {} + + /** + * High-performance network lookup using Maps for O(1) performance + * @param options - Search criteria + * @returns NetworkInfo if found, undefined otherwise + */ + find(options: GetNetworkOptions): NetworkInfo | undefined { + const { chainSelector, chainSelectorName, isTestnet, chainFamily } = options + + const getBySelector = (map: Map) => { + if (chainSelector === undefined) return undefined + return map.get(chainSelector) + } + + // Validate input - need either chainSelector or chainSelectorName + if (!chainSelector && !chainSelectorName) { + return undefined + } + + // If both chainFamily and network type are specified, use the most specific maps + if (chainFamily && chainSelector !== undefined) { + if (isTestnet === false) { + return getBySelector(this.maps.mainnetBySelectorByFamily[chainFamily]) + } + + if (isTestnet === true) { + return getBySelector(this.maps.testnetBySelectorByFamily[chainFamily]) + } + + // If user haven't defined if it's testnet or not we can try all networks, starting from testnet + let network = getBySelector(this.maps.testnetBySelectorByFamily[chainFamily]) + if (!network) { + network = getBySelector(this.maps.mainnetBySelectorByFamily[chainFamily]) + } + return network + } + + if (chainFamily && chainSelectorName) { + if (isTestnet === false) { + return this.maps.mainnetByNameByFamily[chainFamily].get(chainSelectorName) + } + + if (isTestnet === true) { + return this.maps.testnetByNameByFamily[chainFamily].get(chainSelectorName) + } + + // If user haven't defined if it's testnet or not we can try all networks, starting from testnet + let network = this.maps.testnetByNameByFamily[chainFamily].get(chainSelectorName) + if (!network) { + network = this.maps.mainnetByNameByFamily[chainFamily].get(chainSelectorName) + } + return network + } + + // If only network type is specified, use the general maps + if (chainSelector !== undefined) { + if (isTestnet === false) { + return getBySelector(this.maps.mainnetBySelector) + } + + if (isTestnet === true) { + return getBySelector(this.maps.testnetBySelector) + } + + // If user haven't defined if it's testnet or not we can try all networks, starting from testnet + let network = getBySelector(this.maps.testnetBySelector) + if (!network) { + network = getBySelector(this.maps.mainnetBySelector) + } + return network + } + + if (chainSelectorName) { + if (isTestnet === false) { + return this.maps.mainnetByName.get(chainSelectorName) + } + + if (isTestnet === true) { + return this.maps.testnetByName.get(chainSelectorName) + } + + // If user haven't defined if it's testnet or not we can try all networks, starting from testnet + let network = this.maps.testnetByName.get(chainSelectorName) + if (!network) { + network = this.maps.mainnetByName.get(chainSelectorName) + } + return network + } + + return undefined + } +} diff --git a/packages/cre-sdk/src/sdk/utils/index.ts b/packages/cre-sdk/src/sdk/utils/index.ts index 6b4221da..9272593f 100644 --- a/packages/cre-sdk/src/sdk/utils/index.ts +++ b/packages/cre-sdk/src/sdk/utils/index.ts @@ -3,6 +3,7 @@ export * from './capabilities/http/http-helpers' export * from './chain-selectors' export * from './decode-json' export * from './hex-utils' +export * from './safe-json-stringify' export * from './values/consensus_aggregators' export * from './values/serializer_types' export * from './values/value'