@ -1,5 +1,5 @@
import { confirm , input , select } from '@inquirer/prompts' ;
import { BigNumber } from 'bignumber.js' ;
import { BigNumber as BigNumberJs } from 'bignumber.js' ;
import { ethers } from 'ethers' ;
import { z } from 'zod' ;
@ -7,12 +7,10 @@ import {
ChainMap ,
ChainName ,
GasOracleContractType ,
HookConfig ,
HookType ,
IgpHookConfig ,
MerkleTreeHookConfig ,
MultisigIsmConfig ,
ProtocolFeeHookConfig ,
HooksConfig ,
MultisigConfig ,
chainMetadata ,
defaultMultisigConfigs ,
multisigIsmVerificationCost ,
} from '@hyperlane-xyz/sdk' ;
@ -41,25 +39,56 @@ const MerkleTreeSchema = z.object({
type : z . literal ( HookType . MERKLE_TREE ) ,
} ) ;
const HookSchema = z . union ( [ ProtocolFeeSchema , MerkleTreeSchema ] ) ;
const IGPSchema = z . object ( {
type : z . literal ( HookType . INTERCHAIN_GAS_PAYMASTER ) ,
owner : z.string ( ) ,
beneficiary : z.string ( ) ,
overhead : z.record ( z . number ( ) ) ,
gasOracleType : z.record ( z . literal ( GasOracleContractType . StorageGasOracle ) ) ,
oracleKey : z.string ( ) ,
} ) ;
const RoutingConfigSchema : z.ZodSchema < any > = z . lazy ( ( ) = >
z . object ( {
type : z . literal ( HookType . ROUTING ) ,
owner : z.string ( ) ,
domains : z.record ( HookConfigSchema ) ,
} ) ,
) ;
const AggregationConfigSchema : z.ZodSchema < any > = z . lazy ( ( ) = >
z . object ( {
type : z . literal ( HookType . AGGREGATION ) ,
hooks : z.array ( HookConfigSchema ) ,
} ) ,
) ;
const HookConfigSchema = z . union ( [
ProtocolFeeSchema ,
MerkleTreeSchema ,
IGPSchema ,
RoutingConfigSchema ,
AggregationConfigSchema ,
] ) ;
export type HookConfig = z . infer < typeof HookConfigSchema > ;
const ConfigSchema = z . object ( {
required : HookSchema ,
default : HookSchema ,
const Hooks ConfigSchema = z . object ( {
required : HookConfig Schema ,
default : HookConfig Schema ,
} ) ;
const HookConfigMapSchema = z . object ( { } ) . catchall ( ConfigSchema ) ;
export type HookConfigMap = z . infer < typeof HookConfigMapSchema > ;
const Hooks ConfigMapSchema = z . record ( Hooks ConfigSchema) ;
export type Hooks ConfigMap = z . infer < typeof Hooks ConfigMapSchema > ;
export function isValidHookConfigMap ( config : any ) {
return HookConfigMapSchema . safeParse ( config ) . success ;
return Hooks ConfigMapSchema . safeParse ( config ) . success ;
}
export function presetHookConfigs (
owner : Address ,
local : ChainName ,
destinationChains : ChainName [ ] ,
ismConfig? : MultisigIsmConfig ,
) {
multisigConfig? : Multisig Config,
) : HooksConfig {
const gasOracleType = destinationChains . reduce <
ChainMap < GasOracleContractType >
> ( ( acc , chain ) = > {
@ -69,14 +98,17 @@ export function presetHookConfigs(
const overhead = destinationChains . reduce < ChainMap < number > > ( ( acc , chain ) = > {
let validatorThreshold : number ;
let validatorCount : number ;
if ( ism Config) {
validatorThreshold = ism Config. threshold ;
validatorCount = ism Config. validators . length ;
if ( multisig Config) {
validatorThreshold = multisig Config. threshold ;
validatorCount = multisig Config. validators . length ;
} else if ( local in defaultMultisigConfigs ) {
validatorThreshold = defaultMultisigConfigs [ local ] . threshold ;
validatorCount = defaultMultisigConfigs [ local ] . validators . length ;
} else {
throw new Error ( 'Cannot estimate gas overhead for IGP hook' ) ;
// default values
// fix here: https://github.com/hyperlane-xyz/issues/issues/773
validatorThreshold = 2 ;
validatorCount = 3 ;
}
acc [ chain ] = multisigIsmVerificationCost (
validatorThreshold ,
@ -89,17 +121,17 @@ export function presetHookConfigs(
return {
required : {
type : HookType . PROTOCOL_FEE ,
maxProtocolFee : ethers.utils.parseUnits ( '1' , 'gwei' ) ,
protocolFee : ethers.utils.parseUnits ( '0' , 'wei' ) ,
maxProtocolFee : ethers.utils.parseUnits ( '1' , 'gwei' ) . toString ( ) ,
protocolFee : ethers.utils.parseUnits ( '0' , 'wei' ) . toString ( ) ,
beneficiary : owner ,
owner : owner ,
} as ProtocolFeeHookConfig ,
} ,
default : {
type : HookType . AGGREGATION ,
hooks : [
{
type : HookType . MERKLE_TREE ,
} as MerkleTreeHookConfig ,
} ,
{
type : HookType . INTERCHAIN_GAS_PAYMASTER ,
owner : owner ,
@ -107,19 +139,19 @@ export function presetHookConfigs(
gasOracleType ,
overhead ,
oracleKey : owner ,
} as IgpHookConfig ,
} ,
] ,
} ,
} ;
}
export function readHookConfig ( filePath : string ) {
export function readHooks ConfigMap ( filePath : string ) {
const config = readYamlOrJson ( filePath ) ;
if ( ! config ) {
logRed ( ` No hook config found at ${ filePath } ` ) ;
return ;
}
const result = HookConfigMapSchema . safeParse ( config ) ;
const result = Hooks ConfigMapSchema . safeParse ( config ) ;
if ( ! result . success ) {
const firstIssue = result . error . issues [ 0 ] ;
throw new Error (
@ -127,21 +159,15 @@ export function readHookConfig(filePath: string) {
) ;
}
const parsedConfig = result . data ;
const defaultHook : ChainMap < HookConfig > = objMap (
const hooks : ChainMap < Hooks Config > = objMap (
parsedConfig ,
( _ , config ) = >
( {
type : config . default . type ,
} as HookConfig ) ,
( _ , config ) = > config as HooksConfig ,
) ;
logGreen ( ` All hook configs in ${ filePath } are valid ` ) ;
return defaultHook ;
logGreen ( ` All hook configs in ${ filePath } are valid for ${ hooks } ` ) ;
return hooks ;
}
// TODO: read different hook configs
// export async function readProtocolFeeHookConfig(config: {type: HookType.PROTOCOL_FEE, ...}) {
export async function createHookConfig ( {
export async function createHooksConfigMap ( {
format ,
outPath ,
chainConfigPath ,
@ -154,70 +180,15 @@ export async function createHookConfig({
const customChains = readChainConfigsIfExists ( chainConfigPath ) ;
const chains = await runMultiChainSelectionStep ( customChains ) ;
const result : HookConfigMap = { } ;
const result : Hooks ConfigMap = { } ;
for ( const chain of chains ) {
for ( const hookRequirements of [ 'required' , 'default' ] ) {
log ( ` Setting ${ hookRequirements } hook for chain ${ chain } ` ) ;
const hookType = await select ( {
message : 'Select hook type' ,
choices : [
{ value : 'merkle_tree' , name : 'MerkleTreeHook' } ,
{ value : 'protocol_fee' , name : 'StaticProtocolFee' } ,
] ,
pageSize : 5 ,
} ) ;
if ( hookType === 'merkle_tree' ) {
result [ chain ] = {
. . . result [ chain ] ,
[ hookRequirements ] : { type : HookType . MERKLE_TREE } ,
} ;
} else if ( hookType === 'protocol_fee' ) {
const owner = await input ( {
message : 'Enter owner address' ,
} ) ;
const ownerAddress = normalizeAddressEvm ( owner ) ;
let beneficiary ;
let sameAsOwner = false ;
sameAsOwner = await confirm ( {
message : 'Use this same address for the beneficiary?' ,
} ) ;
if ( sameAsOwner ) {
beneficiary = ownerAddress ;
} else {
beneficiary = await input ( {
message : 'Enter beneficiary address' ,
} ) ;
}
const beneficiaryAddress = normalizeAddressEvm ( beneficiary ) ;
// TODO: input in gwei, wei, etc
const maxProtocolFee = toWei (
await input ( {
message : 'Enter max protocol fee in (e.g. 1.0)' ,
} ) ,
) ;
const protocolFee = toWei (
await input ( {
message : 'Enter protocol fee (e.g. 1.0)' ,
} ) ,
) ;
if ( BigNumber ( protocolFee ) . gt ( maxProtocolFee ) ) {
errorRed ( 'Protocol fee cannot be greater than max protocol fee' ) ;
throw new Error ( 'Invalid protocol fee' ) ;
}
result [ chain ] = {
. . . result [ chain ] ,
[ hookRequirements ] : {
type : HookType . PROTOCOL_FEE ,
maxProtocolFee : maxProtocolFee.toString ( ) ,
protocolFee : protocolFee.toString ( ) ,
beneficiary : beneficiaryAddress ,
owner : ownerAddress ,
} ,
} ;
} else {
throw new Error ( ` Invalid hook type: ${ hookType } } ` ) ;
}
const remotes = chains . filter ( ( c ) = > c !== chain ) ;
result [ chain ] = {
. . . result [ chain ] ,
[ hookRequirements ] : await createHookConfig ( chain , remotes ) ,
} ;
}
if ( isValidHookConfigMap ( result ) ) {
logGreen ( ` Hook config is valid, writing to file ${ outPath } ` ) ;
@ -230,3 +201,208 @@ export async function createHookConfig({
}
}
}
export async function createHookConfig (
chain : ChainName ,
remotes : ChainName [ ] ,
) : Promise < HookConfig > {
let lastConfig : HookConfig ;
const hookType = await select ( {
message : 'Select hook type' ,
choices : [
{
value : HookType.MERKLE_TREE ,
name : HookType.MERKLE_TREE ,
description :
'Add messages to the incremental merkle tree on origin chain (needed for the merkleRootMultisigIsm on the remote chain)' ,
} ,
{
value : HookType.PROTOCOL_FEE ,
name : HookType.PROTOCOL_FEE ,
description : 'Charge fees for each message dispatch from this chain' ,
} ,
{
value : HookType.INTERCHAIN_GAS_PAYMASTER ,
name : HookType.INTERCHAIN_GAS_PAYMASTER ,
description :
'Allow for payments for expected gas to be paid by the relayer while delivering on remote chain' ,
} ,
{
value : HookType.AGGREGATION ,
name : HookType.AGGREGATION ,
description :
'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence' ,
} ,
{
value : HookType.ROUTING ,
name : HookType.ROUTING ,
description :
'Each destination domain can have its own hook configured via DomainRoutingHook' ,
} ,
] ,
pageSize : 10 ,
} ) ;
if ( hookType === HookType . MERKLE_TREE ) {
lastConfig = { type : HookType . MERKLE_TREE } ;
} else if ( hookType === HookType . PROTOCOL_FEE ) {
lastConfig = await createProtocolFeeConfig ( chain ) ;
} else if ( hookType === HookType . INTERCHAIN_GAS_PAYMASTER ) {
lastConfig = await createIGPConfig ( remotes ) ;
} else if ( hookType === HookType . AGGREGATION ) {
lastConfig = await createAggregationConfig ( chain , remotes ) ;
} else if ( hookType === HookType . ROUTING ) {
lastConfig = await createRoutingConfig ( chain , remotes ) ;
} else {
throw new Error ( ` Invalid hook type: ${ hookType } ` ) ;
}
return lastConfig ;
}
export async function createProtocolFeeConfig (
chain : ChainName ,
) : Promise < HookConfig > {
const owner = await input ( {
message : 'Enter owner address' ,
} ) ;
const ownerAddress = normalizeAddressEvm ( owner ) ;
let beneficiary ;
let sameAsOwner = false ;
sameAsOwner = await confirm ( {
message : 'Use this same address for the beneficiary?' ,
} ) ;
if ( sameAsOwner ) {
beneficiary = ownerAddress ;
} else {
beneficiary = await input ( {
message : 'Enter beneficiary address' ,
} ) ;
}
const beneficiaryAddress = normalizeAddressEvm ( beneficiary ) ;
// TODO: input in gwei, wei, etc
const maxProtocolFee = toWei (
await input ( {
message : ` Enter max protocol fee ${ nativeTokenAndDecimals (
chain ,
) } e . g . 1.0 ) ` ,
} ) ,
) ;
const protocolFee = toWei (
await input ( {
message : ` Enter protocol fee in ${ nativeTokenAndDecimals (
chain ,
) } e . g . 0.01 ) ` ,
} ) ,
) ;
if ( BigNumberJs ( protocolFee ) . gt ( maxProtocolFee ) ) {
errorRed ( 'Protocol fee cannot be greater than max protocol fee' ) ;
throw new Error ( 'Invalid protocol fee' ) ;
}
return {
type : HookType . PROTOCOL_FEE ,
maxProtocolFee : maxProtocolFee.toString ( ) ,
protocolFee : protocolFee.toString ( ) ,
beneficiary : beneficiaryAddress ,
owner : ownerAddress ,
} ;
}
export async function createIGPConfig (
remotes : ChainName [ ] ,
) : Promise < HookConfig > {
const owner = await input ( {
message : 'Enter owner address' ,
} ) ;
const ownerAddress = normalizeAddressEvm ( owner ) ;
let beneficiary , oracleKey ;
let sameAsOwner = false ;
sameAsOwner = await confirm ( {
message : 'Use this same address for the beneficiary and gasOracleKey?' ,
} ) ;
if ( sameAsOwner ) {
beneficiary = ownerAddress ;
oracleKey = ownerAddress ;
} else {
beneficiary = await input ( {
message : 'Enter beneficiary address' ,
} ) ;
oracleKey = await input ( {
message : 'Enter gasOracleKey address' ,
} ) ;
}
const beneficiaryAddress = normalizeAddressEvm ( beneficiary ) ;
const oracleKeyAddress = normalizeAddressEvm ( oracleKey ) ;
const overheads : ChainMap < number > = { } ;
for ( const chain of remotes ) {
const overhead = parseInt (
await input ( {
message : ` Enter overhead for ${ chain } (eg 75000) ` ,
} ) ,
) ;
overheads [ chain ] = overhead ;
}
return {
type : HookType . INTERCHAIN_GAS_PAYMASTER ,
beneficiary : beneficiaryAddress ,
owner : ownerAddress ,
oracleKey : oracleKeyAddress ,
overhead : overheads ,
gasOracleType : objMap (
overheads ,
( ) = > GasOracleContractType . StorageGasOracle ,
) ,
} ;
}
export async function createAggregationConfig (
chain : ChainName ,
remotes : ChainName [ ] ,
) : Promise < HookConfig > {
const hooksNum = parseInt (
await input ( {
message : 'Enter the number of hooks to aggregate (number)' ,
} ) ,
10 ,
) ;
const hooks : Array < HookConfig > = [ ] ;
for ( let i = 0 ; i < hooksNum ; i ++ ) {
logBlue ( ` Creating hook ${ i + 1 } of ${ hooksNum } ... ` ) ;
hooks . push ( await createHookConfig ( chain , remotes ) ) ;
}
return {
type : HookType . AGGREGATION ,
hooks ,
} ;
}
export async function createRoutingConfig (
origin : ChainName ,
remotes : ChainName [ ] ,
) : Promise < HookConfig > {
const owner = await input ( {
message : 'Enter owner address' ,
} ) ;
const ownerAddress = owner ;
const domainsMap : ChainMap < HookConfig > = { } ;
for ( const chain of remotes ) {
await confirm ( {
message : ` You are about to configure hook for remote chain ${ chain } . Continue? ` ,
} ) ;
const config = await createHookConfig ( origin , remotes ) ;
domainsMap [ chain ] = config ;
}
return {
type : HookType . ROUTING ,
owner : ownerAddress ,
domains : domainsMap ,
} ;
}
function nativeTokenAndDecimals ( chain : ChainName ) {
return ` 10^ ${
chainMetadata [ chain ] . nativeToken ? . decimals ? ? '18'
} which you cannot exceed ( in $ {
chainMetadata [ chain ] . nativeToken ? . symbol ? ? 'eth'
} ` ;
}