Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 119 additions & 1 deletion src/fs/FileSystemRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
ChainMap,
ChainMetadata,
ChainName,
SignerConfig,
WarpCoreConfig,
WarpRouteDeployConfig,
} from '@hyperlane-xyz/sdk';
import { isSignerRef } from '@hyperlane-xyz/sdk';

import {
CHAIN_FILE_REGEX,
SCHEMA_REF,
WARP_ROUTE_CONFIG_FILE_REGEX,
WARP_ROUTE_DEPLOY_FILE_REGEX,
} from '../consts.js';
import { ChainAddresses, ChainAddressesSchema, UpdateChainParams, WarpRouteId } from '../types.js';
import {
ChainAddresses,
ChainAddressesSchema,
SignerConfigMap,
SignerConfiguration,
UpdateChainParams,
WarpRouteId,
} from '../types.js';
import { toYamlString } from '../utils.js';

import {
Expand All @@ -42,10 +51,17 @@
export class FileSystemRegistry extends SynchronousRegistry implements IRegistry {
public readonly type = RegistryType.FileSystem;

// Signer configuration cache
protected signerConfigCache?: SignerConfiguration;

constructor(options: FileSystemRegistryOptions) {
super(options);
}

protected getSignersPath(): string {
return 'signers';
}

getUri(itemPath?: string): string {
if (!itemPath) return super.getUri();
return path.join(this.uri, itemPath);
Expand Down Expand Up @@ -135,6 +151,108 @@
});
}

/**
* Get the full signer configuration from the signers/ directory
* Parses all .yaml files in the signers/ directory and merges them
*/
getSignerConfiguration(): SignerConfiguration | null {
if (this.signerConfigCache) return this.signerConfigCache;

const signersPath = path.join(this.uri, this.getSignersPath());
if (!fs.existsSync(signersPath)) return null;

const signerFiles = this.listFiles(signersPath).filter(
(f) => f.endsWith('.yaml') || f.endsWith('.yml'),
);

if (signerFiles.length === 0) return null;

// Merge all signer config files
let mergedConfig: SignerConfiguration = {};

Check failure on line 171 in src/fs/FileSystemRegistry.ts

View workflow job for this annotation

GitHub Actions / lint

'mergedConfig' is never reassigned. Use 'const' instead

for (const filePath of signerFiles) {
try {
const data = fs.readFileSync(filePath, 'utf8');
const parsed = yamlParse(data) as SignerConfiguration;

// Merge signers
if (parsed.signers) {
mergedConfig.signers = { ...mergedConfig.signers, ...parsed.signers };
}

// Merge defaults (later files override earlier ones)
if (parsed.defaults) {
mergedConfig.defaults = {
...mergedConfig.defaults,
...parsed.defaults,
// Deep merge protocols and chains
protocols: {
...mergedConfig.defaults?.protocols,
...parsed.defaults.protocols,
},
chains: {
...mergedConfig.defaults?.chains,
...parsed.defaults.chains,
},
};
}
} catch (error) {
this.logger.warn(`Failed to parse signer config file ${filePath}: ${error}`);
}
}

this.signerConfigCache = mergedConfig;
return mergedConfig;
}
Comment on lines +154 to +206
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix lint + make override order deterministic.
ESLint flags mergedConfig as never reassigned, and file order currently depends on filesystem traversal. Sorting makes overrides predictable.

🛠️ Suggested tweak
-    const signerFiles = this.listFiles(signersPath).filter(
-      (f) => f.endsWith('.yaml') || f.endsWith('.yml'),
-    );
+    const signerFiles = this.listFiles(signersPath)
+      .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
+      .sort();
...
-    let mergedConfig: SignerConfiguration = {};
+    const mergedConfig: SignerConfiguration = {};
🧰 Tools
🪛 ESLint

[error] 171-171: 'mergedConfig' is never reassigned. Use 'const' instead.

(prefer-const)

🪛 GitHub Check: lint

[failure] 171-171:
'mergedConfig' is never reassigned. Use 'const' instead

🤖 Prompt for AI Agents
In `@src/fs/FileSystemRegistry.ts` around lines 154 - 206, getSignerConfiguration
has two issues: mergedConfig is declared with let but never reassigned (ESLint),
and signer file processing order is non-deterministic; fix by declaring
mergedConfig as const in getSignerConfiguration() and sorting signerFiles (e.g.,
signerFiles.sort()) before the for loop so overrides are deterministic; keep
existing merge logic that spreads parsed.signers and parsed.defaults (including
protocols/chains) and preserve the try/catch/logger.warn behavior in the same
function.


/**
* Get all named signer configurations
*/
getSigners(): SignerConfigMap | null {
const config = this.getSignerConfiguration();
return config?.signers ?? null;
}

/**
* Get a specific named signer configuration
*/
getSigner(id: string): SignerConfig | null {
const signers = this.getSigners();
return signers?.[id] ?? null;
}

/**
* Get the default signer for a chain using hierarchy: chain > protocol > default
* Resolves refs to actual signer configs
*/
getDefaultSigner(chainName?: ChainName): SignerConfig | null {
const config = this.getSignerConfiguration();
if (!config?.defaults) return null;

let signerOrRef = config.defaults.default;

// Check chain-specific override
if (chainName && config.defaults.chains?.[chainName]) {
signerOrRef = config.defaults.chains[chainName];
}
// Check protocol-specific override
else if (chainName) {
const metadata = this.getChainMetadata(chainName);
if (metadata?.protocol && config.defaults.protocols?.[metadata.protocol]) {
signerOrRef = config.defaults.protocols[metadata.protocol];
}
}

if (!signerOrRef) return null;

// Resolve ref if needed
if (isSignerRef(signerOrRef)) {
return this.getSigner(signerOrRef.ref);
}

return signerOrRef;
}

protected listFiles(dirPath: string): string[] {
if (!fs.existsSync(dirPath)) return [];

Expand Down
22 changes: 22 additions & 0 deletions src/fs/registry-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Logger } from 'pino';
import { GithubRegistry } from '../registry/GithubRegistry.js';
import { GCPSignerRegistry, parseGCPUri } from '../registry/GCPSignerRegistry.js';
import {
FoundryKeystoreRegistry,
parseFoundryKeystoreUri,
} from '../registry/FoundryKeystoreRegistry.js';
Comment on lines +3 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle invalid gcp:// or foundry:// URIs with scheme-specific errors.
Right now Line 83–97 only instantiates when parsing succeeds; malformed URIs fall through to file-path validation and throw “Invalid file system path,” which is pretty confusing for users. I'd route any gcp:// or foundry:// scheme straight to their registries so the constructor emits the proper error.

🛠️ Suggested fix
-import { GCPSignerRegistry, parseGCPUri } from '../registry/GCPSignerRegistry.js';
+import { GCPSignerRegistry } from '../registry/GCPSignerRegistry.js';
 import {
   FoundryKeystoreRegistry,
-  parseFoundryKeystoreUri,
 } from '../registry/FoundryKeystoreRegistry.js';
@@
-      // Check for GCP signer registry (gcp://project/secret-name)
-      if (parseGCPUri(uri)) {
+      // Check for GCP signer registry (gcp://project/secret-name)
+      if (uri.startsWith('gcp://')) {
         return new GCPSignerRegistry({
           uri,
           logger: childLogger,
         });
       }
 
       // Check for Foundry keystore registry (foundry://accountName or foundry:///path/accountName)
-      if (parseFoundryKeystoreUri(uri)) {
+      if (uri.startsWith('foundry://')) {
         return new FoundryKeystoreRegistry({
           uri,
           logger: childLogger,
         });
       }

Also applies to: 83-97

🤖 Prompt for AI Agents
In `@src/fs/registry-utils.ts` around lines 3 - 7, Currently malformed gcp:// or
foundry:// URIs fall through to file-path validation and produce misleading
"Invalid file system path" errors; change the logic that handles input URIs so
that any input starting with "gcp://" calls parseGCPUri and instantiates
GCPSignerRegistry (using GCPSignerRegistry constructor) and any input starting
with "foundry://" calls parseFoundryKeystoreUri and instantiates
FoundryKeystoreRegistry (using FoundryKeystoreRegistry constructor) so
parse/constructor errors are surfaced immediately; specifically, detect the
scheme prefix before file-path validation, invoke
parseGCPUri/parseFoundryKeystoreUri and let or rethrow their errors (or wrap
them with a clearer message) instead of falling through to the file-path branch.

import { FileSystemRegistry } from './FileSystemRegistry.js';
import { IRegistry } from '../registry/IRegistry.js';
import { PROXY_DEPLOYED_URL } from '../consts.js';
Expand Down Expand Up @@ -74,6 +79,23 @@ export function getRegistry({
.filter((uri) => !!uri)
.map((uri, index) => {
const childLogger = registryLogger?.child({ uri, index });

// Check for GCP signer registry (gcp://project/secret-name)
if (parseGCPUri(uri)) {
return new GCPSignerRegistry({
uri,
logger: childLogger,
});
}

// Check for Foundry keystore registry (foundry://accountName or foundry:///path/accountName)
if (parseFoundryKeystoreUri(uri)) {
return new FoundryKeystoreRegistry({
uri,
logger: childLogger,
});
}

if (isProtocolUrl(uri, ['https:']) && uri.includes('github')) {
return new GithubRegistry({
uri,
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export {
} from './consts.js';
export { BaseRegistry } from './registry/BaseRegistry.js';
export { GithubRegistry, GithubRegistryOptions } from './registry/GithubRegistry.js';
export {
GCPSignerRegistry,
GCPSignerRegistryOptions,
parseGCPUri,
} from './registry/GCPSignerRegistry.js';
export {
FoundryKeystoreRegistry,
FoundryKeystoreRegistryOptions,
parseFoundryKeystoreUri,
} from './registry/FoundryKeystoreRegistry.js';
export {
AddWarpRouteConfigOptions,
ChainFiles,
Expand Down
38 changes: 38 additions & 0 deletions src/registry/BaseRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChainMetadata,
ChainName,
HypTokenRouterConfig,
SignerConfig,
WarpCoreConfig,
WarpRouteDeployConfig,
} from '@hyperlane-xyz/sdk';
Expand All @@ -13,6 +14,8 @@ import { WARP_ROUTE_ID_REGEX } from '../consts.js';
import type {
ChainAddresses,
MaybePromise,
SignerConfigMap,
SignerConfiguration,
UpdateChainParams,
WarpDeployConfigMap,
WarpRouteFilterParams,
Expand Down Expand Up @@ -192,6 +195,41 @@ export abstract class BaseRegistry implements IRegistry {
abstract getWarpDeployConfig(routeId: string): MaybePromise<WarpRouteDeployConfig | null>;
abstract getWarpDeployConfigs(filter?: WarpRouteFilterParams): MaybePromise<WarpDeployConfigMap>;

// Default signer implementations - return null by default
// Subclasses can override to provide signer support

/**
* Get all named signer configurations
* Default implementation returns null (not supported)
*/
getSigners(): MaybePromise<SignerConfigMap | null> {
return null;
}

/**
* Get a specific named signer configuration
* Default implementation returns null (not found)
*/
getSigner(_id: string): MaybePromise<SignerConfig | null> {
return null;
}

/**
* Get the default signer for a chain using hierarchy: chain > protocol > default
* Default implementation returns null (not configured)
*/
getDefaultSigner(_chainName?: ChainName): MaybePromise<SignerConfig | null> {
return null;
}

/**
* Get the full signer configuration
* Default implementation returns null (not supported)
*/
getSignerConfiguration(): MaybePromise<SignerConfiguration | null> {
return null;
}

merge(otherRegistry: IRegistry): IRegistry {
return new MergedRegistry({ registries: [this, otherRegistry], logger: this.logger });
}
Expand Down
Loading
Loading