Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ergonomic IGP configuration in CLI #4635

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

paulbalaji
Copy link
Contributor

@paulbalaji paulbalaji commented Oct 7, 2024

Description

Re-adding the ability to generate IGP hook configs using the CLI, but repurposing logic found in infra to make the configuration experience more ergonomic. Logic still behind the --advanced flag.

Enabling this allows IGP configuration in any place that supports hook config e.g. core/warp/hook init with --advanced.

We will use metadata in registry to:

  1. fetch price from Coingecko (prompt user if unable to find)
  2. fetch current gas prices via the default RPCs
  3. request user to enter an IGP margin in %
  4. Calculate the gasPrice + tokenExchangeRate for you

Note that it still sets overhead to some preexisting default.

? Select hook type interchainGasPaymaster
Creating interchainGasPaymaster...
? Detected owner address as 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 from signer, is this
correct? yes
? Use this same address for the beneficiary? yes
? Select network type Mainnet
? Select local chain for IGP hook bitlayer
? Select remote destination chains for IGP hook alephzero, ancient8
? Enter overhead for alephzero (e.g., 75000) for IGP hook 75000
? Enter overhead for ancient8 (e.g., 75000) for IGP hook 75000
Getting gas token prices for all chains from Coingecko...
Gas price for alephzero is 40.0
Gas token price for alephzero is $0.393347
Gas price for ancient8 is 0.001000252
Gas token price for ancient8 is $2356.71
Gas price for bitlayer is 0.050000007
Gas token price for bitlayer is $60576
? Enter IGP margin percentage (e.g. 10 for 10%) 100
Created interchainGasPaymaster!
Core config is valid, writing to file ./configs/core-config.yaml:

    owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
    defaultIsm:
      type: trustedRelayerIsm
      relayer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
    defaultHook:
      type: aggregationHook
      hooks:
        - type: merkleTreeHook
        - owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
          type: interchainGasPaymaster
          beneficiary: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
          oracleKey: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
          overhead:
            alephzero: 75000
            ancient8: 75000
          oracleConfig:
            alephzero:
              gasPrice: "40000000000"
              tokenExchangeRate: "129868"
            ancient8:
              gasPrice: "1000253"
              tokenExchangeRate: "778100236"
    requiredHook:
      owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
      type: protocolFee
      beneficiary: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
      maxProtocolFee: "1000000000000000000"
      protocolFee: "0"

✅ Successfully created new core deployment config.

Drive-by changes

Moving reusable infra logic into the SDK, and refactoring CLI+Infra to reuse the underlying logic. For example:

  • fetching token prices from coingecko
  • fetching gas prices using a chain's RPC

Related issues

Most recently, hyperlane-xyz/hyperlane-registry#236 (comment). But there have been numerous occasions where it would be nice for users to be self-sufficient in configuring and deploying an IGP hook for their PI deployments/relayer.

Backward compatibility

yes

Testing

  • creating igp config with hyperlane core init --advanced
  • making sure infra print-token-prices.ts still works
  • making sure infra print-gas-prices.ts still works

Copy link

changeset-bot bot commented Oct 7, 2024

🦋 Changeset detected

Latest commit: 670b211

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@hyperlane-xyz/cli Minor
@hyperlane-xyz/sdk Minor
@hyperlane-xyz/helloworld Minor
@hyperlane-xyz/infra Minor
@hyperlane-xyz/widgets Minor
@hyperlane-xyz/ccip-server Minor
@hyperlane-xyz/github-proxy Minor
@hyperlane-xyz/utils Minor
@hyperlane-xyz/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@paulbalaji paulbalaji marked this pull request as ready for review October 7, 2024 17:32
Copy link

codecov bot commented Oct 7, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 73.89%. Comparing base (7dccf80) to head (670b211).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4635   +/-   ##
=======================================
  Coverage   73.89%   73.89%           
=======================================
  Files         100      100           
  Lines        1421     1421           
  Branches      180      180           
=======================================
  Hits         1050     1050           
  Misses        350      350           
  Partials       21       21           
Components Coverage Δ
core 84.61% <ø> (ø)
hooks 75.71% <ø> (ø)
isms 79.20% <ø> (ø)
token 88.23% <ø> (ø)
middlewares 77.39% <ø> (ø)

Comment on lines +105 to +109
local: ChainName,
remote: ChainName,
tokenPrices: ChainMap<string>,
exchangeRateMarginPct: number,
decimals: { local: number; remote: number },
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a personal preference but I think it would be better to convert this list of params to a single object so that when this function is used we can directly see what each value is just by looking at the field names

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is also my preference wherever possible, I just didn't want to break the current tradition of individual args vs one object of args that we've got in utils. definitely down to make it an object if people are ok with it

typescript/cli/src/config/hooks.ts Show resolved Hide resolved
Comment on lines -190 to 328
);
const isTestnet = context.chainMetadata[localChain].isTestnet;
const remoteChains = await runMultiChainSelectionStep(
objFilter(
context.chainMetadata,
(_, metadata): metadata is ChainMetadata =>
metadata.name !== localChain,
),
'Select remote destination chains for IGP hook',
1,
isTestnet ? 'testnet' : 'mainnet',
);

// Only need to set overhead for remote chains
const overhead: ChainMap<number> = {};
for (const chain of remoteChains) {
overhead[chain] = parseInt(
await input({
message: `Enter overhead for ${chain} (eg 75000) for IGP hook`,
message: `Enter overhead for ${chain} (e.g., 75000) for IGP hook`,
default: '75000',
}),
);
overheads[chain] = overhead;
}

// Restrict metadata to only the chains that are part of the IGP hook
const filteredMetadata = objFilter(
context.chainMetadata,
(_, metadata): metadata is ChainMetadata =>
remoteChains.includes(metadata.name) || metadata.name === localChain,
);

// Fetch prices for all chains in the IGP hook
// For testnet, hardcode prices to 10 USD
// For mainnet, fetch prices from Coingecko
// If mainnet prices are not found, user will be prompted to enter them
let fetchedPrices: ChainMap<string | undefined> = {};
if (isTestnet) {
// Get the prices of the remote chains' tokens from Coingecko
logBlue(`Hardcoding all gas token prices to 10 USD for testnet...`);
fetchedPrices = objMap(filteredMetadata, () => '10');
} else {
// Get the prices of the remote chains' tokens from Coingecko
logBlue(`Getting gas token prices for all chains from Coingecko...`);
fetchedPrices = await getCoingeckoTokenPrices(filteredMetadata);
}

const mpp = new MultiProtocolProvider(context.chainMetadata);
const prices: ChainMap<ChainGasOracleParams> = {};

for (const chain of Object.keys(filteredMetadata)) {
const gasPrice = await getGasPrice(mpp, chain);
logBlue(`Gas price for ${chain} is ${gasPrice.amount}`);

// if the price is not found on Coingecko, prompt user to enter it
let tokenPrice = fetchedPrices[chain];
if (!tokenPrice) {
tokenPrice = await input({
message: `Enter the price of ${chain}'s token in USD`,
});
} else {
logBlue(`Gas token price for ${chain} is $${tokenPrice}`);
}

const decimals = context.chainMetadata[chain].nativeToken?.decimals;
if (!decimals) {
throw new Error(`No decimals found in metadata for ${chain}`);
}
prices[chain] = {
gasPrice,
nativeToken: { price: tokenPrice, decimals },
};
}

// Allow user to enter a IGP exchange rate margin in %
const exchangeRateMarginPct = parseInt(
await input({
message: `Enter IGP margin percentage (e.g. 10 for 10%)`,
default: '10',
}),
);

const oracleConfig = getLocalStorageGasOracleConfig(
localChain,
prices,
exchangeRateMarginPct,
);

return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary,
owner,
oracleKey,
overhead: overheads,
oracleConfig: {},
overhead,
oracleConfig,
};
},
Copy link
Contributor

Choose a reason for hiding this comment

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

My only complaint here is that the function body is bit long maybe some code block might be exported into their own functions. Examples:

This could go into its own function named something like getIgpOwner

const unnormalizedOwner =
      !advanced && context.signer
        ? await context.signer.getAddress()
        : await detectAndConfirmOrPrompt(
            async () => context.signer?.getAddress(),
            'For Interchain Gas Paymaster, enter',
            'owner address',
            'signer',
          );

This could be put inside a getGasPriceByChain function or something similar

    const prices: ChainMap<ChainGasOracleParams> = {};

    for (const chain of Object.keys(filteredMetadata)) {
      const gasPrice = await getGasPrice(mpp, chain);
      logBlue(`Gas price for ${chain} is ${gasPrice.amount}`);

      // if the price is not found on Coingecko, prompt user to enter it
      let tokenPrice = fetchedPrices[chain];
      if (!tokenPrice) {
        tokenPrice = await input({
          message: `Enter the price of ${chain}'s token in USD`,
        });
      } else {
        logBlue(`Gas token price for ${chain} is $${tokenPrice}`);
      }

      const decimals = context.chainMetadata[chain].nativeToken?.decimals;
      if (!decimals) {
        throw new Error(`No decimals found in metadata for ${chain}`);
      }
      prices[chain] = {
        gasPrice,
        nativeToken: { price: tokenPrice, decimals },
      };
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm i'll have a play around

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants