@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
179 lines • 9.02 kB
JavaScript
import { BigNumber as BigNumberJs } from 'bignumber.js';
import { BigNumber, ethers } from 'ethers';
import { ProtocolType, assert, convertDecimals, objMap, } from '@hyperlane-xyz/utils';
import { getProtocolExchangeRateDecimals } from '../consts/igp.js';
import { getCosmosRegistryChain } from '../utils/cosmos.js';
export async function getGasPrice(mpp, chain) {
const protocolType = mpp.getProtocol(chain);
switch (protocolType) {
case ProtocolType.Ethereum: {
const provider = mpp.getProvider(chain);
const gasPrice = await provider.provider.getGasPrice();
return {
amount: ethers.utils.formatUnits(gasPrice, 'gwei'),
decimals: 9,
};
}
case ProtocolType.Cosmos:
case ProtocolType.CosmosNative: {
const { amount } = await getCosmosChainGasPrice(chain, mpp);
return {
amount,
decimals: 1,
};
}
case ProtocolType.Sealevel:
// TODO get a reasonable value
return {
amount: '0.001',
decimals: 9,
};
default:
throw new Error(`Unsupported protocol type: ${protocolType}`);
}
}
// Gets the gas price for a Cosmos chain
export async function getCosmosChainGasPrice(chain, chainMetadataManager) {
const metadata = chainMetadataManager.getChainMetadata(chain);
if (!metadata) {
throw new Error(`No metadata found for Cosmos chain ${chain}`);
}
if (metadata.protocol !== ProtocolType.Cosmos &&
metadata.protocol !== ProtocolType.CosmosNative) {
throw new Error(`Chain ${chain} is not a Cosmos chain`);
}
// Use the cosmos registry gas price first.
let cosmosRegistryChain;
try {
cosmosRegistryChain = await getCosmosRegistryChain(chain);
}
catch {
// Fallback to our registry gas price from the metadata.
if (metadata.gasPrice) {
return metadata.gasPrice;
}
throw new Error(`No gas price found for Cosmos chain ${chain} in the registry or metadata`);
}
const nativeToken = metadata.nativeToken;
if (!nativeToken) {
throw new Error(`No native token found for Cosmos chain ${chain}`);
}
if (!nativeToken.denom) {
throw new Error(`No denom found for native token on Cosmos chain ${chain}`);
}
const fee = cosmosRegistryChain.fees?.fee_tokens.find((fee) => {
return (fee.denom === nativeToken.denom || fee.denom === `u${nativeToken.denom}`);
});
if (!fee || fee.average_gas_price === undefined) {
throw new Error(`No gas price found for Cosmos chain ${chain}`);
}
return {
denom: fee.denom,
amount: fee.average_gas_price.toString(),
};
}
// Gets the exchange rate of the remote quoted in local tokens, not accounting for decimals.
function getTokenExchangeRate({ local, remote, tokenPrices, exchangeRateMarginPct, }) {
// Workaround for chicken-egg dependency problem.
// We need to provide some default value here to satisfy the config on initial load,
// whilst knowing that it will get overwritten when a script actually gets run.
const defaultValue = '1';
const localValue = new BigNumberJs(tokenPrices[local] ?? defaultValue);
const remoteValue = new BigNumberJs(tokenPrices[remote] ?? defaultValue);
// Note this does not account for decimals!
let exchangeRate = remoteValue.div(localValue);
// Apply the premium
exchangeRate = exchangeRate.times(100 + exchangeRateMarginPct).div(100);
assert(exchangeRate.isGreaterThan(0), 'Exchange rate must be greater than 0, possible loss of precision');
return exchangeRate;
}
function getProtocolExchangeRate(localProtocolType, exchangeRate) {
const multiplierDecimals = getProtocolExchangeRateDecimals(localProtocolType);
const multiplier = new BigNumberJs(10).pow(multiplierDecimals);
const integer = exchangeRate
.times(multiplier)
.integerValue(BigNumberJs.ROUND_FLOOR)
.toString(10);
return BigNumber.from(integer);
}
// Gets the StorageGasOracleConfig for each remote chain for a particular local chain.
// Accommodates small non-integer gas prices by scaling up the gas price
// and scaling down the exchange rate by the same factor.
// A gasPriceModifier can be supplied to adjust the gas price based on a prospective
// gasOracleConfig.
// Values take into consideration the local chain's needs depending on the protocol type,
// e.g. the expected decimals of the token exchange rate, or whether to account for
// a native token decimal difference in the exchange rate.
// Therefore the values here can be applied directly to the chain's gas oracle.
export function getLocalStorageGasOracleConfig({ local, localProtocolType, gasOracleParams, exchangeRateMarginPct, gasPriceModifier, typicalCostGetter, }) {
const remotes = Object.keys(gasOracleParams).filter((remote) => remote !== local);
const tokenPrices = objMap(gasOracleParams, (chain) => gasOracleParams[chain].nativeToken.price);
const localDecimals = gasOracleParams[local].nativeToken.decimals;
return remotes.reduce((agg, remote) => {
const remoteDecimals = gasOracleParams[remote].nativeToken.decimals;
// The exchange rate, not yet accounting for decimals, and potentially
// floating point.
let exchangeRateFloat = getTokenExchangeRate({
local,
remote,
tokenPrices,
exchangeRateMarginPct,
});
if (localProtocolType !== ProtocolType.Sealevel) {
// On all chains other than Sealevel, we need to adjust the exchange rate for decimals.
exchangeRateFloat = convertDecimals(remoteDecimals, localDecimals, exchangeRateFloat);
}
// Make the exchange rate an integer by scaling it up by the appropriate factor for the protocol.
const exchangeRate = getProtocolExchangeRate(localProtocolType, exchangeRateFloat);
// First parse the gas price as a number, so we have floating point precision.
// Recall it's possible to have gas prices that are not integers, even
// after converting to the "wei" version of the token.
const gasPrice = new BigNumberJs(gasOracleParams[remote].gasPrice.amount).times(new BigNumberJs(10).pow(gasOracleParams[remote].gasPrice.decimals));
if (gasPrice.isNaN()) {
throw new Error(`Invalid gas price for chain ${remote}: ${gasOracleParams[remote].gasPrice.amount}`);
}
// Get a prospective gasOracleConfig, adjusting the gas price and exchange rate
// as needed to account for precision loss (e.g. if the gas price is super small).
let gasOracleConfig = adjustForPrecisionLoss(gasPrice, exchangeRate, remoteDecimals, remote);
// Apply the modifier if provided.
if (gasPriceModifier) {
// Once again adjust for precision loss after applying the modifier.
gasOracleConfig = adjustForPrecisionLoss(gasPriceModifier(local, remote, gasOracleConfig), BigNumber.from(gasOracleConfig.tokenExchangeRate), remoteDecimals, remote);
}
if (typicalCostGetter) {
gasOracleConfig.typicalCost = typicalCostGetter(local, remote, gasOracleConfig);
}
return {
...agg,
[remote]: gasOracleConfig,
};
}, {});
}
function adjustForPrecisionLoss(gasPrice, exchangeRate, remoteDecimals, remote) {
let newGasPrice = new BigNumberJs(gasPrice);
let newExchangeRate = exchangeRate;
// We may have very little precision, and ultimately need an integer value for
// the gas price that will be set on-chain. If this is the case, we scale up the
// gas price and scale down the exchange rate by the same factor.
if (newGasPrice.lt(10) && newGasPrice.mod(1) !== new BigNumberJs(0)) {
// Scale up the gas price by 1e4 (arbitrary choice)
const gasPriceScalingFactor = 1e4;
// Check that there's no significant underflow when applying
// this to the exchange rate:
const adjustedExchangeRate = newExchangeRate.div(gasPriceScalingFactor);
const recoveredExchangeRate = adjustedExchangeRate.mul(gasPriceScalingFactor);
if (recoveredExchangeRate.mul(100).div(newExchangeRate).lt(99)) {
throw new Error(`Too much underflow when downscaling exchange rate for remote chain ${remote}`);
}
newGasPrice = newGasPrice.times(gasPriceScalingFactor);
newExchangeRate = adjustedExchangeRate;
}
const newGasPriceInteger = newGasPrice.integerValue(BigNumberJs.ROUND_CEIL);
assert(newGasPriceInteger.gt(0), 'Gas price must be greater than 0, possible loss of precision');
return {
tokenExchangeRate: newExchangeRate.toString(),
gasPrice: newGasPriceInteger.toString(),
tokenDecimals: remoteDecimals,
};
}
//# sourceMappingURL=utils.js.map