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