Merge branch 'main' of github.com:abacus-network/abacus-monorepo into trevor/decimal-consistency-checker
commit
97571fc3e1
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/utils': minor |
||||
--- |
||||
|
||||
Added `isPrivateKeyEvm` function for validating EVM private keys |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Introduce GcpValidator for retrieving announcements, checkpoints and metadata for a Validator posting to a GCP bucket. Uses GcpStorageWrapper for bucket operations. |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/cli': minor |
||||
--- |
||||
|
||||
Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Added a getter to derive ATA payer accounts on Sealevel warp routes |
@ -1,4 +0,0 @@ |
||||
node_modules |
||||
dist |
||||
coverage |
||||
*.cts |
@ -1,65 +0,0 @@ |
||||
{ |
||||
"env": { |
||||
"node": true, |
||||
"browser": true, |
||||
"es2021": true |
||||
}, |
||||
"root": true, |
||||
"parser": "@typescript-eslint/parser", |
||||
"parserOptions": { |
||||
"ecmaVersion": 12, |
||||
"sourceType": "module", |
||||
"project": "./tsconfig.json" |
||||
}, |
||||
"plugins": ["@typescript-eslint","jest"], |
||||
"extends": [ |
||||
"eslint:recommended", |
||||
"plugin:@typescript-eslint/recommended", |
||||
"prettier" |
||||
], |
||||
"rules": { |
||||
"no-console": ["error"], |
||||
"no-eval": ["error"], |
||||
"no-extra-boolean-cast": ["error"], |
||||
"no-ex-assign": ["error"], |
||||
"no-constant-condition": ["off"], |
||||
"no-return-await": ["error"], |
||||
"no-restricted-imports": ["error", { |
||||
"name": "console", |
||||
"message": "Please use a logger and/or the utils' package assert" |
||||
}, { |
||||
"name": "fs", |
||||
"message": "Avoid use of node-specific libraries" |
||||
}], |
||||
"guard-for-in": ["error"], |
||||
"@typescript-eslint/ban-ts-comment": ["off"], |
||||
"@typescript-eslint/explicit-module-boundary-types": ["off"], |
||||
"@typescript-eslint/no-explicit-any": ["off"], |
||||
"@typescript-eslint/no-floating-promises": ["error"], |
||||
"@typescript-eslint/no-non-null-assertion": ["off"], |
||||
"@typescript-eslint/no-require-imports": ["warn"], |
||||
"@typescript-eslint/no-unused-vars": [ |
||||
"error", |
||||
{ |
||||
"argsIgnorePattern": "^_", |
||||
"varsIgnorePattern": "^_", |
||||
"caughtErrorsIgnorePattern": "^_" |
||||
} |
||||
], |
||||
"@typescript-eslint/ban-types": [ |
||||
"error", |
||||
{ |
||||
"types": { |
||||
// Unban the {} type which is a useful shorthand for non-nullish value |
||||
"{}": false |
||||
}, |
||||
"extendDefaults": true |
||||
} |
||||
], |
||||
"jest/no-disabled-tests": "warn", |
||||
"jest/no-focused-tests": "error", |
||||
"jest/no-identical-title": "error", |
||||
"jest/prefer-to-have-length": "warn", |
||||
"jest/valid-expect": "error" |
||||
} |
||||
} |
@ -1 +1 @@ |
||||
82013508db45dcd55b44d2721414d26817686c8f |
||||
385b83950adba6f033be836b627bab7d89aae38d |
||||
|
@ -0,0 +1,3 @@ |
||||
{ |
||||
"dependencyTypes": ["prod", "dev"] |
||||
} |
@ -0,0 +1,115 @@ |
||||
import { FlatCompat } from '@eslint/eslintrc'; |
||||
import js from '@eslint/js'; |
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'; |
||||
import tsParser from '@typescript-eslint/parser'; |
||||
import importPlugin from 'eslint-plugin-import'; |
||||
import jest from 'eslint-plugin-jest'; |
||||
import globals from 'globals'; |
||||
import path from 'node:path'; |
||||
import { fileURLToPath } from 'node:url'; |
||||
|
||||
const __filename = fileURLToPath(import.meta.url); |
||||
const __dirname = path.dirname(__filename); |
||||
export const compat = new FlatCompat({ |
||||
baseDirectory: __dirname, |
||||
recommendedConfig: js.configs.recommended, |
||||
allConfig: js.configs.all, |
||||
}); |
||||
|
||||
export default [ |
||||
{ |
||||
ignores: [ |
||||
'**/node_modules', |
||||
'**/dist', |
||||
'**/coverage', |
||||
'**/*.cjs', |
||||
'**/*.cts', |
||||
'**/*.mjs', |
||||
'jest.config.js', |
||||
], |
||||
}, |
||||
...compat.extends( |
||||
'eslint:recommended', |
||||
'plugin:import/recommended', |
||||
'plugin:import/typescript', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'prettier', |
||||
), |
||||
{ |
||||
plugins: { |
||||
import: importPlugin, |
||||
'@typescript-eslint': typescriptEslint, |
||||
jest, |
||||
}, |
||||
|
||||
languageOptions: { |
||||
globals: { |
||||
...globals.node, |
||||
...globals.browser, |
||||
}, |
||||
|
||||
parser: tsParser, |
||||
ecmaVersion: 12, |
||||
sourceType: 'module', |
||||
|
||||
parserOptions: { |
||||
project: './tsconfig.json', |
||||
}, |
||||
}, |
||||
|
||||
settings: { |
||||
'import/resolver': { |
||||
typescript: true, |
||||
node: true, |
||||
}, |
||||
}, |
||||
|
||||
rules: { |
||||
'guard-for-in': ['error'], |
||||
'import/no-cycle': ['error'], |
||||
'import/no-self-import': ['error'], |
||||
'import/no-named-as-default-member': ['off'], |
||||
'no-console': ['error'], |
||||
'no-eval': ['error'], |
||||
'no-extra-boolean-cast': ['error'], |
||||
'no-ex-assign': ['error'], |
||||
'no-constant-condition': ['off'], |
||||
'no-return-await': ['error'], |
||||
|
||||
'no-restricted-imports': [ |
||||
'error', |
||||
{ |
||||
name: 'console', |
||||
message: 'Please use a logger and/or the utils package assert', |
||||
}, |
||||
{ |
||||
name: 'fs', |
||||
message: 'Avoid use of node-specific libraries', |
||||
}, |
||||
], |
||||
|
||||
'@typescript-eslint/ban-ts-comment': ['off'], |
||||
'@typescript-eslint/explicit-module-boundary-types': ['off'], |
||||
'@typescript-eslint/no-explicit-any': ['off'], |
||||
'@typescript-eslint/no-floating-promises': ['error'], |
||||
'@typescript-eslint/no-non-null-assertion': ['off'], |
||||
'@typescript-eslint/no-require-imports': ['warn'], |
||||
'@typescript-eslint/no-unused-expressions': ['off'], |
||||
'@typescript-eslint/no-empty-object-type': ['off'], |
||||
'@typescript-eslint/no-unused-vars': [ |
||||
'error', |
||||
{ |
||||
argsIgnorePattern: '^_', |
||||
varsIgnorePattern: '^_', |
||||
caughtErrorsIgnorePattern: '^_', |
||||
}, |
||||
], |
||||
|
||||
'jest/no-disabled-tests': 'warn', |
||||
'jest/no-focused-tests': 'error', |
||||
'jest/no-identical-title': 'error', |
||||
'jest/prefer-to-have-length': 'warn', |
||||
'jest/valid-expect': 'error', |
||||
}, |
||||
}, |
||||
]; |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"eclipsemainnet": { |
||||
"hex": "0x82f7445ccda6396092998c8f841f0d4eb63cca29ba23cfd2609d283f3ee9d13f", |
||||
"base58": "9pEgj7m2VkwLtJHPtTw5d8vbB7kfjzcXXCRgdwruW7C2" |
||||
}, |
||||
"ethereum": { |
||||
"hex": "0x000000000000000000000000d34fe1685c28a68bb4b8faaadcb2769962ae737c", |
||||
"base58": "1111111111113wkPRLXCJuj9539UEURsN3qhoaYb" |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
{ |
||||
"eclipsemainnet": { |
||||
"type": "synthetic", |
||||
"decimals": 9, |
||||
"remoteDecimals": 18, |
||||
"name": "Autocompounding Pirex Ether", |
||||
"symbol": "apxETH", |
||||
"uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/ae7df1bc00af19f8ba692c14e4df3acdbf30497d/deployments/warp_routes/APXETH/metadata.json", |
||||
"interchainGasPaymaster": "3Wp4qKkgf4tjXz1soGyTSndCgBPLZFSrZkiDZ8Qp9EEj" |
||||
}, |
||||
"ethereum": { |
||||
"type": "collateral", |
||||
"decimals": 18, |
||||
"token": "0x9ba021b0a9b958b5e75ce9f6dff97c7ee52cb3e6", |
||||
"foreignDeployment": "0xd34FE1685c28A68Bb4B8fAaadCb2769962AE737c" |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"eclipsemainnet": { |
||||
"hex": "0xb06d58417c929a624e9b689e604e6d60ca652168ee76b9a290bd5b974b22b306", |
||||
"base58": "CshTfxXWMvnRAwBTCjQ4577bkP5po5ZuNG1QTuQxA5Au" |
||||
}, |
||||
"solanamainnet": { |
||||
"hex": "0x08bb318b88b38cc6f450b185e51a9c42402dc9d36fa6741c19c2aa62464a5eb3", |
||||
"base58": "b5pMgizA9vrGRt3hVqnU7vUVGBQUnLpwPzcJhG1ucyQ" |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
{ |
||||
"solanamainnet": { |
||||
"type": "collateral", |
||||
"decimals": 9, |
||||
"interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", |
||||
"token": "ezSoL6fY1PVdJcJsUpe5CM3xkfmy3zoVCABybm5WtiC", |
||||
"splTokenProgram": "token" |
||||
}, |
||||
"eclipsemainnet": { |
||||
"type": "synthetic", |
||||
"decimals": 9, |
||||
"name": "Renzo Restaked SOL", |
||||
"symbol": "ezSOL", |
||||
"uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/ae7df1bc00af19f8ba692c14e4df3acdbf30497d/deployments/warp_routes/EZSOL/metadata.json", |
||||
"interchainGasPaymaster": "3Wp4qKkgf4tjXz1soGyTSndCgBPLZFSrZkiDZ8Qp9EEj" |
||||
} |
||||
} |
@ -1,6 +0,0 @@ |
||||
{ |
||||
"rules": { |
||||
"no-console": ["off"] |
||||
} |
||||
} |
||||
|
@ -0,0 +1,17 @@ |
||||
import MonorepoDefaults from '../../eslint.config.mjs'; |
||||
|
||||
export default [ |
||||
...MonorepoDefaults, |
||||
{ |
||||
files: ['./src/**/*.ts'], |
||||
}, |
||||
{ |
||||
rules: { |
||||
'no-console': ['off'], |
||||
'no-restricted-imports': ['off'], |
||||
}, |
||||
}, |
||||
{ |
||||
ignores: ['**/__mocks__/*','**/tests/*',] |
||||
} |
||||
]; |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"extends": "../tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "./dist/", |
||||
"rootDir": "./src" |
||||
}, |
||||
"exclude": ["./node_modules/", "./dist/"], |
||||
"include": ["./src/*.ts"] |
||||
} |
@ -1,2 +0,0 @@ |
||||
node_modules |
||||
dist |
@ -1,6 +0,0 @@ |
||||
{ |
||||
"rules": { |
||||
"no-console": ["off"], |
||||
"no-restricted-imports": ["off"] |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
import MonorepoDefaults from '../../eslint.config.mjs'; |
||||
|
||||
export default [ |
||||
...MonorepoDefaults, |
||||
{ |
||||
files: ['./src/**/*.ts', './cli.ts', './env.ts'], |
||||
}, |
||||
{ |
||||
rules: { |
||||
'no-console': ['off'], |
||||
'no-restricted-imports': ['off'], |
||||
}, |
||||
}, |
||||
{ |
||||
ignores: ['./src/tests/**/*.ts'], |
||||
rules: { |
||||
'import/no-cycle': ['off'], |
||||
}, |
||||
}, |
||||
]; |
@ -0,0 +1,70 @@ |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
import { CommandModule } from 'yargs'; |
||||
|
||||
import { |
||||
createStrategyConfig, |
||||
readChainSubmissionStrategyConfig, |
||||
} from '../config/strategy.js'; |
||||
import { CommandModuleWithWriteContext } from '../context/types.js'; |
||||
import { log, logCommandHeader } from '../logger.js'; |
||||
import { indentYamlOrJson } from '../utils/files.js'; |
||||
import { maskSensitiveData } from '../utils/output.js'; |
||||
|
||||
import { |
||||
DEFAULT_STRATEGY_CONFIG_PATH, |
||||
outputFileCommandOption, |
||||
strategyCommandOption, |
||||
} from './options.js'; |
||||
|
||||
/** |
||||
* Parent command |
||||
*/ |
||||
export const strategyCommand: CommandModule = { |
||||
command: 'strategy', |
||||
describe: 'Manage Hyperlane deployment strategies', |
||||
builder: (yargs) => |
||||
yargs.command(init).command(read).version(false).demandCommand(), |
||||
handler: () => log('Command required'), |
||||
}; |
||||
|
||||
export const init: CommandModuleWithWriteContext<{ |
||||
out: string; |
||||
}> = { |
||||
command: 'init', |
||||
describe: 'Creates strategy configuration', |
||||
builder: { |
||||
out: outputFileCommandOption(DEFAULT_STRATEGY_CONFIG_PATH), |
||||
}, |
||||
handler: async ({ context, out }) => { |
||||
logCommandHeader(`Hyperlane Strategy Init`); |
||||
|
||||
await createStrategyConfig({ |
||||
context, |
||||
outPath: out, |
||||
}); |
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
export const read: CommandModuleWithWriteContext<{ |
||||
strategy: string; |
||||
}> = { |
||||
command: 'read', |
||||
describe: 'Reads strategy configuration', |
||||
builder: { |
||||
strategy: { |
||||
...strategyCommandOption, |
||||
demandOption: true, |
||||
default: DEFAULT_STRATEGY_CONFIG_PATH, |
||||
}, |
||||
}, |
||||
handler: async ({ strategy: strategyUrl }) => { |
||||
logCommandHeader(`Hyperlane Strategy Read`); |
||||
|
||||
const strategy = await readChainSubmissionStrategyConfig(strategyUrl); |
||||
const maskedConfig = maskSensitiveData(strategy); |
||||
log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); |
||||
|
||||
process.exit(0); |
||||
}, |
||||
}; |
@ -0,0 +1,34 @@ |
||||
import { CommandType } from '../../../commands/signCommands.js'; |
||||
|
||||
import { MultiChainResolver } from './MultiChainResolver.js'; |
||||
import { SingleChainResolver } from './SingleChainResolver.js'; |
||||
import { ChainResolver } from './types.js'; |
||||
|
||||
/** |
||||
* @class ChainResolverFactory |
||||
* @description Intercepts commands to determine the appropriate chain resolver strategy based on command type. |
||||
*/ |
||||
export class ChainResolverFactory { |
||||
private static strategyMap: Map<CommandType, () => ChainResolver> = new Map([ |
||||
[CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()], |
||||
[CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], |
||||
[CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], |
||||
[CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()], |
||||
[CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()], |
||||
[CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()], |
||||
[CommandType.STATUS, () => MultiChainResolver.forOriginDestination()], |
||||
[CommandType.SUBMIT, () => MultiChainResolver.forStrategyConfig()], |
||||
[CommandType.RELAYER, () => MultiChainResolver.forRelayer()], |
||||
]); |
||||
|
||||
/** |
||||
* @param argv - Command line arguments. |
||||
* @returns ChainResolver - The appropriate chain resolver strategy based on the command type. |
||||
*/ |
||||
static getStrategy(argv: Record<string, any>): ChainResolver { |
||||
const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; |
||||
const createStrategy = |
||||
this.strategyMap.get(commandKey) || (() => new SingleChainResolver()); |
||||
return createStrategy(); |
||||
} |
||||
} |
@ -0,0 +1,200 @@ |
||||
import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; |
||||
import { assert } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; |
||||
import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; |
||||
import { logRed } from '../../../logger.js'; |
||||
import { |
||||
extractChainsFromObj, |
||||
runMultiChainSelectionStep, |
||||
runSingleChainSelectionStep, |
||||
} from '../../../utils/chains.js'; |
||||
import { |
||||
isFile, |
||||
readYamlOrJson, |
||||
runFileSelectionStep, |
||||
} from '../../../utils/files.js'; |
||||
import { getWarpCoreConfigOrExit } from '../../../utils/warp.js'; |
||||
|
||||
import { ChainResolver } from './types.js'; |
||||
|
||||
enum ChainSelectionMode { |
||||
ORIGIN_DESTINATION, |
||||
AGENT_KURTOSIS, |
||||
WARP_CONFIG, |
||||
WARP_READ, |
||||
STRATEGY, |
||||
RELAYER, |
||||
} |
||||
|
||||
// This class could be broken down into multiple strategies
|
||||
|
||||
/** |
||||
* @title MultiChainResolver |
||||
* @notice Resolves chains based on the specified selection mode. |
||||
*/ |
||||
export class MultiChainResolver implements ChainResolver { |
||||
constructor(private mode: ChainSelectionMode) {} |
||||
|
||||
async resolveChains(argv: ChainMap<any>): Promise<ChainName[]> { |
||||
switch (this.mode) { |
||||
case ChainSelectionMode.WARP_CONFIG: |
||||
return this.resolveWarpRouteConfigChains(argv); |
||||
case ChainSelectionMode.WARP_READ: |
||||
return this.resolveWarpCoreConfigChains(argv); |
||||
case ChainSelectionMode.AGENT_KURTOSIS: |
||||
return this.resolveAgentChains(argv); |
||||
case ChainSelectionMode.STRATEGY: |
||||
return this.resolveStrategyChains(argv); |
||||
case ChainSelectionMode.RELAYER: |
||||
return this.resolveRelayerChains(argv); |
||||
case ChainSelectionMode.ORIGIN_DESTINATION: |
||||
default: |
||||
return this.resolveOriginDestinationChains(argv); |
||||
} |
||||
} |
||||
|
||||
private async resolveWarpRouteConfigChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; |
||||
argv.context.chains = await this.getWarpRouteConfigChains( |
||||
argv.config.trim(), |
||||
argv.skipConfirmation, |
||||
); |
||||
return argv.context.chains; |
||||
} |
||||
|
||||
private async resolveWarpCoreConfigChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
if (argv.symbol || argv.warp) { |
||||
const warpCoreConfig = await getWarpCoreConfigOrExit({ |
||||
context: argv.context, |
||||
warp: argv.warp, |
||||
symbol: argv.symbol, |
||||
}); |
||||
argv.context.warpCoreConfig = warpCoreConfig; |
||||
const chains = extractChainsFromObj(warpCoreConfig); |
||||
return chains; |
||||
} else if (argv.chain) { |
||||
return [argv.chain]; |
||||
} else { |
||||
throw new Error( |
||||
`Please specify either a symbol, chain and address or warp file`, |
||||
); |
||||
} |
||||
} |
||||
|
||||
private async resolveAgentChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
const { chainMetadata } = argv.context; |
||||
argv.origin = |
||||
argv.origin ?? |
||||
(await runSingleChainSelectionStep( |
||||
chainMetadata, |
||||
'Select the origin chain', |
||||
)); |
||||
|
||||
if (!argv.targets) { |
||||
const selectedRelayChains = await runMultiChainSelectionStep({ |
||||
chainMetadata: chainMetadata, |
||||
message: 'Select chains to relay between', |
||||
requireNumber: 2, |
||||
}); |
||||
argv.targets = selectedRelayChains.join(','); |
||||
} |
||||
|
||||
return [argv.origin, ...argv.targets]; |
||||
} |
||||
|
||||
private async resolveOriginDestinationChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
const { chainMetadata } = argv.context; |
||||
|
||||
argv.origin = |
||||
argv.origin ?? |
||||
(await runSingleChainSelectionStep( |
||||
chainMetadata, |
||||
'Select the origin chain', |
||||
)); |
||||
|
||||
argv.destination = |
||||
argv.destination ?? |
||||
(await runSingleChainSelectionStep( |
||||
chainMetadata, |
||||
'Select the destination chain', |
||||
)); |
||||
|
||||
return [argv.origin, argv.destination]; |
||||
} |
||||
|
||||
private async resolveStrategyChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
const strategy = await readChainSubmissionStrategyConfig(argv.strategy); |
||||
return extractChainsFromObj(strategy); |
||||
} |
||||
|
||||
private async resolveRelayerChains( |
||||
argv: Record<string, any>, |
||||
): Promise<ChainName[]> { |
||||
return argv.chains.split(',').map((item: string) => item.trim()); |
||||
} |
||||
|
||||
private async getWarpRouteConfigChains( |
||||
configPath: string, |
||||
skipConfirmation: boolean, |
||||
): Promise<ChainName[]> { |
||||
if (!configPath || !isFile(configPath)) { |
||||
assert(!skipConfirmation, 'Warp route deployment config is required'); |
||||
configPath = await runFileSelectionStep( |
||||
'./configs', |
||||
'Warp route deployment config', |
||||
'warp', |
||||
); |
||||
} else { |
||||
logRed(`Using warp route deployment config at ${configPath}`); |
||||
} |
||||
|
||||
// Alternative to readWarpRouteDeployConfig that doesn't use context for signer and zod validation
|
||||
const warpRouteConfig = (await readYamlOrJson(configPath)) as Record< |
||||
string, |
||||
any |
||||
>; |
||||
|
||||
const chains = Object.keys(warpRouteConfig) as ChainName[]; |
||||
assert( |
||||
chains.length !== 0, |
||||
'No chains found in warp route deployment config', |
||||
); |
||||
|
||||
return chains; |
||||
} |
||||
|
||||
static forAgentKurtosis(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.AGENT_KURTOSIS); |
||||
} |
||||
|
||||
static forOriginDestination(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.ORIGIN_DESTINATION); |
||||
} |
||||
|
||||
static forRelayer(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.RELAYER); |
||||
} |
||||
|
||||
static forStrategyConfig(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.STRATEGY); |
||||
} |
||||
|
||||
static forWarpRouteConfig(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.WARP_CONFIG); |
||||
} |
||||
|
||||
static forWarpCoreConfig(): MultiChainResolver { |
||||
return new MultiChainResolver(ChainSelectionMode.WARP_READ); |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { runSingleChainSelectionStep } from '../../../utils/chains.js'; |
||||
|
||||
import { ChainResolver } from './types.js'; |
||||
|
||||
/** |
||||
* @title SingleChainResolver |
||||
* @notice Strategy implementation for managing single-chain operations |
||||
* @dev Primarily used for operations like 'core:apply' and 'warp:read' |
||||
*/ |
||||
export class SingleChainResolver implements ChainResolver { |
||||
/** |
||||
* @notice Determines the chain to be used for signing operations |
||||
* @dev Either uses the chain specified in argv or prompts for interactive selection |
||||
*/ |
||||
async resolveChains(argv: ChainMap<any>): Promise<ChainName[]> { |
||||
argv.chain ||= await runSingleChainSelectionStep( |
||||
argv.context.chainMetadata, |
||||
'Select chain to connect:', |
||||
); |
||||
|
||||
return [argv.chain]; // Explicitly return as single-item array
|
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; |
||||
|
||||
export interface ChainResolver { |
||||
/** |
||||
* Determines the chains to be used for signing |
||||
* @param argv Command arguments |
||||
* @returns Array of chain names |
||||
*/ |
||||
resolveChains(argv: ChainMap<any>): Promise<ChainName[]>; |
||||
} |
@ -0,0 +1,22 @@ |
||||
import { Signer } from 'ethers'; |
||||
|
||||
import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; |
||||
import { Address } from '@hyperlane-xyz/utils'; |
||||
|
||||
export interface SignerConfig { |
||||
privateKey: string; |
||||
address?: Address; // For chains like StarkNet that require address
|
||||
extraParams?: Record<string, any>; // For any additional chain-specific params
|
||||
} |
||||
|
||||
export interface IMultiProtocolSigner { |
||||
getSignerConfig(chain: ChainName): Promise<SignerConfig> | SignerConfig; |
||||
getSigner(config: SignerConfig): Signer; |
||||
} |
||||
|
||||
export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { |
||||
constructor(protected config: ChainSubmissionStrategy) {} |
||||
|
||||
abstract getSignerConfig(chain: ChainName): Promise<SignerConfig>; |
||||
abstract getSigner(config: SignerConfig): Signer; |
||||
} |
@ -0,0 +1,79 @@ |
||||
import { password } from '@inquirer/prompts'; |
||||
import { Signer, Wallet } from 'ethers'; |
||||
|
||||
import { |
||||
ChainName, |
||||
ChainSubmissionStrategy, |
||||
ChainTechnicalStack, |
||||
MultiProvider, |
||||
TxSubmitterType, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { ProtocolType } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
BaseMultiProtocolSigner, |
||||
IMultiProtocolSigner, |
||||
SignerConfig, |
||||
} from './BaseMultiProtocolSigner.js'; |
||||
|
||||
export class MultiProtocolSignerFactory { |
||||
static getSignerStrategy( |
||||
chain: ChainName, |
||||
strategyConfig: ChainSubmissionStrategy, |
||||
multiProvider: MultiProvider, |
||||
): IMultiProtocolSigner { |
||||
const { protocol, technicalStack } = multiProvider.getChainMetadata(chain); |
||||
|
||||
switch (protocol) { |
||||
case ProtocolType.Ethereum: |
||||
if (technicalStack === ChainTechnicalStack.ZkSync) |
||||
return new ZKSyncSignerStrategy(strategyConfig); |
||||
return new EthereumSignerStrategy(strategyConfig); |
||||
default: |
||||
throw new Error(`Unsupported protocol: ${protocol}`); |
||||
} |
||||
} |
||||
} |
||||
|
||||
class EthereumSignerStrategy extends BaseMultiProtocolSigner { |
||||
async getSignerConfig(chain: ChainName): Promise<SignerConfig> { |
||||
const submitter = this.config[chain]?.submitter as { |
||||
type: TxSubmitterType.JSON_RPC; |
||||
privateKey?: string; |
||||
}; |
||||
|
||||
const privateKey = |
||||
submitter?.privateKey ?? |
||||
(await password({ |
||||
message: `Please enter the private key for chain ${chain}`, |
||||
})); |
||||
|
||||
return { privateKey }; |
||||
} |
||||
|
||||
getSigner(config: SignerConfig): Signer { |
||||
return new Wallet(config.privateKey); |
||||
} |
||||
} |
||||
|
||||
// 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean
|
||||
// TODO: import ZKSync signer
|
||||
class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { |
||||
async getSignerConfig(chain: ChainName): Promise<SignerConfig> { |
||||
const submitter = this.config[chain]?.submitter as { |
||||
privateKey?: string; |
||||
}; |
||||
|
||||
const privateKey = |
||||
submitter?.privateKey ?? |
||||
(await password({ |
||||
message: `Please enter the private key for chain ${chain}`, |
||||
})); |
||||
|
||||
return { privateKey }; |
||||
} |
||||
|
||||
getSigner(config: SignerConfig): Signer { |
||||
return new Wallet(config.privateKey); |
||||
} |
||||
} |
@ -0,0 +1,153 @@ |
||||
import { Signer } from 'ethers'; |
||||
import { Logger } from 'pino'; |
||||
|
||||
import { |
||||
ChainName, |
||||
ChainSubmissionStrategy, |
||||
MultiProvider, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { assert, rootLogger } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ENV } from '../../../utils/env.js'; |
||||
|
||||
import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; |
||||
import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; |
||||
|
||||
export interface MultiProtocolSignerOptions { |
||||
logger?: Logger; |
||||
key?: string; |
||||
} |
||||
|
||||
/** |
||||
* @title MultiProtocolSignerManager |
||||
* @dev Context manager for signers across multiple protocols |
||||
*/ |
||||
export class MultiProtocolSignerManager { |
||||
protected readonly signerStrategies: Map<ChainName, IMultiProtocolSigner>; |
||||
protected readonly signers: Map<ChainName, Signer>; |
||||
public readonly logger: Logger; |
||||
|
||||
constructor( |
||||
protected readonly submissionStrategy: ChainSubmissionStrategy, |
||||
protected readonly chains: ChainName[], |
||||
protected readonly multiProvider: MultiProvider, |
||||
protected readonly options: MultiProtocolSignerOptions = {}, |
||||
) { |
||||
this.logger = |
||||
options?.logger || |
||||
rootLogger.child({ |
||||
module: 'MultiProtocolSignerManager', |
||||
}); |
||||
this.signerStrategies = new Map(); |
||||
this.signers = new Map(); |
||||
this.initializeStrategies(); |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets up chain-specific signer strategies |
||||
*/ |
||||
protected initializeStrategies(): void { |
||||
for (const chain of this.chains) { |
||||
const strategy = MultiProtocolSignerFactory.getSignerStrategy( |
||||
chain, |
||||
this.submissionStrategy, |
||||
this.multiProvider, |
||||
); |
||||
this.signerStrategies.set(chain, strategy); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Configures signers for EVM chains in MultiProvider |
||||
*/ |
||||
async getMultiProvider(): Promise<MultiProvider> { |
||||
for (const chain of this.chains) { |
||||
const signer = await this.initSigner(chain); |
||||
this.multiProvider.setSigner(chain, signer); |
||||
} |
||||
|
||||
return this.multiProvider; |
||||
} |
||||
|
||||
/** |
||||
* @notice Creates signer for specific chain |
||||
*/ |
||||
async initSigner(chain: ChainName): Promise<Signer> { |
||||
const { privateKey } = await this.resolveConfig(chain); |
||||
|
||||
const signerStrategy = this.signerStrategies.get(chain); |
||||
assert(signerStrategy, `No signer strategy found for chain ${chain}`); |
||||
|
||||
return signerStrategy.getSigner({ privateKey }); |
||||
} |
||||
|
||||
/** |
||||
* @notice Creates signers for all chains |
||||
*/ |
||||
async initAllSigners(): Promise<typeof this.signers> { |
||||
const signerConfigs = await this.resolveAllConfigs(); |
||||
|
||||
for (const { chain, privateKey } of signerConfigs) { |
||||
const signerStrategy = this.signerStrategies.get(chain); |
||||
if (signerStrategy) { |
||||
this.signers.set(chain, signerStrategy.getSigner({ privateKey })); |
||||
} |
||||
} |
||||
|
||||
return this.signers; |
||||
} |
||||
|
||||
/** |
||||
* @notice Resolves all chain configurations |
||||
*/ |
||||
private async resolveAllConfigs(): Promise< |
||||
Array<{ chain: ChainName; privateKey: string }> |
||||
> { |
||||
return Promise.all(this.chains.map((chain) => this.resolveConfig(chain))); |
||||
} |
||||
|
||||
/** |
||||
* @notice Resolves single chain configuration |
||||
*/ |
||||
private async resolveConfig( |
||||
chain: ChainName, |
||||
): Promise<{ chain: ChainName; privateKey: string }> { |
||||
const signerStrategy = this.signerStrategies.get(chain); |
||||
assert(signerStrategy, `No signer strategy found for chain ${chain}`); |
||||
|
||||
let privateKey: string; |
||||
|
||||
if (this.options.key) { |
||||
this.logger.info( |
||||
`Using private key passed via CLI --key flag for chain ${chain}`, |
||||
); |
||||
privateKey = this.options.key; |
||||
} else if (ENV.HYP_KEY) { |
||||
this.logger.info(`Using private key from .env for chain ${chain}`); |
||||
privateKey = ENV.HYP_KEY; |
||||
} else { |
||||
privateKey = await this.extractPrivateKey(chain, signerStrategy); |
||||
} |
||||
|
||||
return { chain, privateKey }; |
||||
} |
||||
|
||||
/** |
||||
* @notice Gets private key from strategy |
||||
*/ |
||||
private async extractPrivateKey( |
||||
chain: ChainName, |
||||
signerStrategy: IMultiProtocolSigner, |
||||
): Promise<string> { |
||||
const strategyConfig = await signerStrategy.getSignerConfig(chain); |
||||
assert( |
||||
strategyConfig.privateKey, |
||||
`No private key found for chain ${chain}`, |
||||
); |
||||
|
||||
this.logger.info( |
||||
`Extracting private key from strategy config/user prompt for chain ${chain}`, |
||||
); |
||||
return strategyConfig.privateKey; |
||||
} |
||||
} |
@ -0,0 +1,35 @@ |
||||
import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { readWarpCoreConfig } from '../config/warp.js'; |
||||
import { CommandContext } from '../context/types.js'; |
||||
import { logRed } from '../logger.js'; |
||||
|
||||
import { selectRegistryWarpRoute } from './tokens.js'; |
||||
|
||||
/** |
||||
* Gets a {@link WarpCoreConfig} based on the provided path or prompts the user to choose one: |
||||
* - if `symbol` is provided the user will have to select one of the available warp routes. |
||||
* - if `warp` is provided the config will be read by the provided file path. |
||||
* - if none is provided the CLI will exit. |
||||
*/ |
||||
export async function getWarpCoreConfigOrExit({ |
||||
context, |
||||
symbol, |
||||
warp, |
||||
}: { |
||||
context: CommandContext; |
||||
symbol?: string; |
||||
warp?: string; |
||||
}): Promise<WarpCoreConfig> { |
||||
let warpCoreConfig: WarpCoreConfig; |
||||
if (symbol) { |
||||
warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); |
||||
} else if (warp) { |
||||
warpCoreConfig = readWarpCoreConfig(warp); |
||||
} else { |
||||
logRed(`Please specify either a symbol or warp config`); |
||||
process.exit(0); |
||||
} |
||||
|
||||
return warpCoreConfig; |
||||
} |
@ -1 +1 @@ |
||||
export const VERSION = '7.1.0'; |
||||
export const VERSION = '7.2.0'; |
||||
|
@ -1,5 +0,0 @@ |
||||
node_modules |
||||
dist |
||||
coverage |
||||
src/types |
||||
hardhat.config.ts |
@ -1,39 +0,0 @@ |
||||
{ |
||||
"env": { |
||||
"node": true, |
||||
"browser": true, |
||||
"es2021": true |
||||
}, |
||||
"root": true, |
||||
"parser": "@typescript-eslint/parser", |
||||
"parserOptions": { |
||||
"ecmaVersion": 12, |
||||
"sourceType": "module", |
||||
"project": "./tsconfig.json" |
||||
}, |
||||
"plugins": ["@typescript-eslint"], |
||||
"extends": [ |
||||
"eslint:recommended", |
||||
"plugin:@typescript-eslint/recommended", |
||||
"prettier" |
||||
], |
||||
"rules": { |
||||
"no-eval": ["error"], |
||||
"no-ex-assign": ["error"], |
||||
"no-constant-condition": ["off"], |
||||
"@typescript-eslint/ban-ts-comment": ["off"], |
||||
"@typescript-eslint/explicit-module-boundary-types": ["off"], |
||||
"@typescript-eslint/no-explicit-any": ["off"], |
||||
"@typescript-eslint/no-floating-promises": ["error"], |
||||
"@typescript-eslint/no-non-null-assertion": ["off"], |
||||
"@typescript-eslint/no-require-imports": ["warn"], |
||||
"@typescript-eslint/no-unused-vars": [ |
||||
"error", |
||||
{ |
||||
"argsIgnorePattern": "^_", |
||||
"varsIgnorePattern": "^_", |
||||
"caughtErrorsIgnorePattern": "^_" |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
import MonorepoDefaults from '../../eslint.config.mjs'; |
||||
|
||||
export default [ |
||||
...MonorepoDefaults, |
||||
{ |
||||
files: ['./src/**/*.ts'], |
||||
}, |
||||
{ |
||||
ignores: ["**/src/types/*"], |
||||
}, |
||||
{ |
||||
ignores: ['./src/scripts'], |
||||
rules: { |
||||
'no-console': ['off'], |
||||
}, |
||||
}, |
||||
]; |
@ -0,0 +1,20 @@ |
||||
{ |
||||
"everclear": { |
||||
"sender": "0xEFfAB7cCEBF63FbEFB4884964b12259d4374FaAa" |
||||
}, |
||||
"ethereum": { |
||||
"sender": "0x9ADA72CCbAfe94248aFaDE6B604D1bEAacc899A7" |
||||
}, |
||||
"optimism": { |
||||
"sender": "0x9ADA72CCbAfe94248aFaDE6B604D1bEAacc899A7" |
||||
}, |
||||
"bsc": { |
||||
"sender": "0x9ADA72CCbAfe94248aFaDE6B604D1bEAacc899A7" |
||||
}, |
||||
"base": { |
||||
"sender": "0x9ADA72CCbAfe94248aFaDE6B604D1bEAacc899A7" |
||||
}, |
||||
"arbitrum": { |
||||
"sender": "0x9ADA72CCbAfe94248aFaDE6B604D1bEAacc899A7" |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue