@@ -397,7 +397,10 @@ export const buildAdapterSettings = <
397397 customSettings = { } as SettingsDefinitionMap ,
398398 envVarsPrefix = '' as string ,
399399} ) : AdapterSettings < CustomSettings > => {
400- const vars = { } as Record < string , SettingValueType | undefined >
400+ const vars = { } as Record <
401+ string ,
402+ SettingValueType | undefined | Getter < SettingValueType | undefined >
403+ >
401404
402405 // Iterate base adapter env vars
403406 for ( const [ key , config ] of Object . entries ( BaseSettingsDefinition ) as Array <
@@ -416,7 +419,7 @@ export const buildAdapterSettings = <
416419 `Custom env var "${ key } " declared, but a base framework env var with that name already exists.` ,
417420 )
418421 }
419- const value = getEnv ( key as string , config , envVarsPrefix ) ?? config . default
422+ const value = getEnvOrEnvGetter ( key as string , config , envVarsPrefix )
420423 vars [ key ] = value
421424 }
422425
@@ -457,13 +460,31 @@ const getEnvName = (name: string, prefix = ''): string => {
457460
458461const isEnvNameValid = ( name : string ) => / ^ [ _ a - z 0 - 9 ] + $ / i. test ( name )
459462
463+ export const getEnvOrEnvGetter = (
464+ name : string ,
465+ settingsDefinition : SettingDefinition ,
466+ prefix = '' ,
467+ ) : SettingValueType | undefined | Getter < SettingValueType | undefined > => {
468+ if ( settingsDefinition . variablePlaceholder === undefined ) {
469+ return getEnv ( name , settingsDefinition , prefix ) ?? settingsDefinition . default
470+ }
471+ return new EnvGetter ( name , settingsDefinition , prefix )
472+ }
473+
460474export const getEnv = (
461475 name : string ,
462476 settingsDefinition : SettingDefinition ,
463477 prefix = '' ,
464478) : SettingValueType | null => {
465479 const value = process . env [ getEnvName ( name , prefix ) ]
480+ return parseEnv ( value , name , settingsDefinition )
481+ }
466482
483+ export const parseEnv = (
484+ value : string | undefined ,
485+ name : string ,
486+ settingsDefinition : SettingDefinition ,
487+ ) : SettingValueType | null => {
467488 if ( ! value || value === '' || value === '""' ) {
468489 return null
469490 }
@@ -485,6 +506,92 @@ export const getEnv = (
485506 }
486507}
487508
509+ type VariableEnvVarEntry < T extends ValidSettingValue > = {
510+ // The name used in the settings definition. E.g., 'NETWORK_RPC_URL'
511+ settingKey : string
512+ // The variable part for a specific instance. E.g., 'ETHEREUM'
513+ variable : string
514+ // The setting key with the variable part replaced. E.g., 'ETHEREUM_RPC_URL'
515+ settingName : string
516+ // The actual name in the environment. E.g., 'PREFIX_ETHEREUM_RPC_URL'
517+ envVarName : string
518+ // The parsed value. E.g., some URL.
519+ value : T
520+ }
521+
522+ export interface Getter < T extends SettingValueType | undefined > {
523+ get ( variable : string ) : T
524+ entries ( ) : VariableEnvVarEntry < Exclude < T , undefined > > [ ]
525+ }
526+
527+ export class EnvGetter <
528+ T extends SettingDefinition & IsVariable = Extract < SettingDefinition , IsVariable > ,
529+ > {
530+ private name : string
531+ private settingsDefinition : T
532+ private prefix : string
533+ private variableMap : Record < string , VariableEnvVarEntry < SettingTypeWhenPresent < T > > > = { }
534+
535+ constructor ( name : string , settingsDefinition : T , prefix : string ) {
536+ this . name = name
537+ this . settingsDefinition = settingsDefinition
538+ this . prefix = prefix
539+
540+ // If the setting name is 'NETWORK_RPC_URL' and `variablePlaceholder` is
541+ // 'NETWORK', then `namePattern` will be /([A-Z0-9_]+)_RPC_URL/ to match
542+ // all relevant environment variables and extract the variable part.
543+ const namePattern = new RegExp (
544+ `^${ getEnvName ( name , prefix ) . replace ( settingsDefinition . variablePlaceholder , '([A-Z0-9_]+)' ) } $` ,
545+ )
546+ for ( const [ envVarName , value ] of Object . entries ( process . env ) ) {
547+ const match = envVarName . match ( namePattern )
548+ if ( ! match ) {
549+ continue
550+ }
551+ const variablePart = match [ 1 ]
552+ const settingName = name . replace ( settingsDefinition . variablePlaceholder , variablePart )
553+ const parsed = parseEnv ( value , settingName , settingsDefinition )
554+ if ( parsed !== null ) {
555+ this . variableMap [ variablePart ] = {
556+ settingKey : name ,
557+ variable : variablePart ,
558+ settingName,
559+ envVarName,
560+ value : parsed as SettingTypeWhenPresent < T > ,
561+ }
562+ }
563+ }
564+ }
565+
566+ get ( variable : string ) : SettingType < T > {
567+ const canonicalVariable = variable . replace ( / \W / g, '_' ) . toUpperCase ( )
568+ if ( canonicalVariable in this . variableMap ) {
569+ return this . variableMap [ canonicalVariable ] . value as SettingType < T >
570+ }
571+ if ( this . settingsDefinition . default !== undefined ) {
572+ return this . settingsDefinition . default as SettingType < T >
573+ }
574+ if ( ! this . settingsDefinition . required ) {
575+ return undefined as SettingType < T >
576+ }
577+ const envName = getEnvName (
578+ this . name . replace ( this . settingsDefinition . variablePlaceholder , canonicalVariable ) ,
579+ this . prefix ,
580+ )
581+ throw new Error ( `Missing required environment variable: ${ envName } ` )
582+ }
583+
584+ entries ( ) : VariableEnvVarEntry < SettingTypeWhenPresent < T > > [ ] {
585+ return Object . values ( this . variableMap )
586+ }
587+
588+ validate ( validationErrors : string [ ] ) {
589+ for ( const { settingName, value } of Object . values ( this . variableMap ) ) {
590+ validateSetting ( settingName , value , this . settingsDefinition , validationErrors )
591+ }
592+ }
593+ }
594+
488595type SettingValueType = string | number | boolean
489596type SettingTypeWhenPresent < C extends SettingDefinition > = C [ 'type' ] extends 'string'
490597 ? string
@@ -507,7 +614,7 @@ export type SettingDefinitionBase = {
507614 description : string
508615 sensitive ?: boolean
509616 required ?: boolean
510- }
617+ } & ( { variablePlaceholder ?: never } | { variablePlaceholder : string } )
511618
512619export type NonEnumSettingDefinition < TypeString , Type > = SettingDefinitionBase & {
513620 type : TypeString
@@ -541,6 +648,21 @@ type IsOptional = {
541648 description : string
542649}
543650
651+ type IsVariable = { variablePlaceholder : string }
652+ type IsFixed = {
653+ variablePlaceholder ?: never
654+ // Add description for the same issue with weak types as in IsOptional.
655+ description : string
656+ }
657+
658+ type VariableSettingKeys < T extends SettingsDefinitionMap > = {
659+ [ K in keyof T ] : T [ K ] extends IsVariable ? K : never
660+ } [ keyof T ]
661+
662+ type FixedSettingKeys < T extends SettingsDefinitionMap > = {
663+ [ K in keyof T ] : T [ K ] extends IsFixed ? K : never
664+ } [ keyof T ]
665+
544666type NonOptionalSettingKeys < T extends SettingsDefinitionMap > = {
545667 [ K in keyof T ] : T [ K ] extends HasDefault | IsRequired ? K : never
546668} [ keyof T ]
@@ -550,9 +672,15 @@ type OptionalSettingKeys<T extends SettingsDefinitionMap> = {
550672} [ keyof T ]
551673
552674export type Settings < T extends SettingsDefinitionMap > = {
553- - readonly [ K in OptionalSettingKeys < T > ] ?: SettingType < T [ K ] >
675+ - readonly [ K in Extract < FixedSettingKeys < T > , OptionalSettingKeys < T > > ] ?:
676+ | SettingTypeWhenPresent < T [ K ] >
677+ | undefined
678+ } & {
679+ - readonly [ K in Extract < FixedSettingKeys < T > , NonOptionalSettingKeys < T > > ] : SettingTypeWhenPresent <
680+ T [ K ]
681+ >
554682} & {
555- - readonly [ K in NonOptionalSettingKeys < T > ] : SettingType < T [ K ] >
683+ - readonly [ K in VariableSettingKeys < T > ] : Getter < SettingType < T [ K ] > >
556684}
557685
558686export type BaseAdapterSettings = Settings < BaseSettingsDefinitionType >
@@ -617,12 +745,16 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
617745 Object . entries ( BaseSettingsDefinition as SettingsDefinitionMap )
618746 . concat ( Object . entries ( this . settingsDefinition || { } ) )
619747 . forEach ( ( [ name , setting ] ) => {
620- validateSetting (
621- name ,
622- ( this . settings as Record < string , ValidSettingValue > ) [ name ] ,
623- setting ,
624- validationErrors ,
625- )
748+ if ( setting . variablePlaceholder !== undefined ) {
749+ ; ( this . settings as unknown as Record < string , EnvGetter > ) [ name ] . validate ( validationErrors )
750+ } else {
751+ validateSetting (
752+ name ,
753+ ( this . settings as Record < string , ValidSettingValue > ) [ name ] ,
754+ setting ,
755+ validationErrors ,
756+ )
757+ }
626758 } )
627759
628760 if ( validationErrors . length > 0 ) {
@@ -651,10 +783,26 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
651783 alwaysCensored . some ( ( pattern ) => name . includes ( pattern ) ) ) &&
652784 ( this . settings as Record < string , ValidSettingValue > ) [ name ] ,
653785 )
654- . map ( ( [ name ] ) => ( {
786+ . flatMap ( ( [ name ] ) => {
787+ const settings = this . settings as Record <
788+ string ,
789+ ValidSettingValue | Getter < ValidSettingValue >
790+ >
791+ const settingValue = settings [ name ]
792+ if ( settingValue instanceof EnvGetter ) {
793+ return settingValue
794+ . entries ( )
795+ . map (
796+ ( { settingName, value } ) =>
797+ [ settingName , value ] satisfies [ string , ValidSettingValue ] ,
798+ )
799+ }
800+ return [ [ name as string , settingValue ] ] as [ string , ValidSettingValue ] [ ]
801+ } )
802+ . map ( ( [ name , value ] : [ string , ValidSettingValue ] ) => ( {
655803 key : name ,
656804 value : new RegExp (
657- ( ( this . settings as Record < string , ValidSettingValue > ) [ name ] ! as string )
805+ ( value ! as string )
658806 // Escaping potential special characters in values before creating regex
659807 . replace ( / [ - [ \] { } ( ) * + ? . , \\ ^ $ | # \s ] / g, '\\$&' )
660808 // Escaping special case for new line characters. This is needed to properly match and censor private keys,
@@ -689,5 +837,5 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
689837type SettingsObjectSpecifier = {
690838 __reserved_settings : never
691839}
692- type ValidSettingValue = string | number | boolean
840+ export type ValidSettingValue = string | number | boolean
693841export type GenericConfigStructure = BaseAdapterSettings & SettingsObjectSpecifier
0 commit comments