UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

210 lines 9.27 kB
import { constants } from 'ethers'; import { BaseFee__factory, CrossCollateralRoutingFee__factory, LinearFee__factory, OffchainQuotedLinearFee__factory, RoutingFee__factory, } from '@hyperlane-xyz/core'; import { assert, concurrentMap, rootLogger, } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { HyperlaneReader } from '../utils/HyperlaneReader.js'; import { OnchainTokenFeeType, TokenFeeType, } from './types.js'; import { getCrossCollateralRouterKeys, getEffectiveCrossCollateralDestinations, } from './crossCollateralUtils.js'; import { ASSUMED_MAX_AMOUNT_FOR_ZERO_SUPPLY, BPS_PRECISION, MAX_BPS, assertBpsPrecision, convertToBps, } from './utils.js'; export class EvmTokenFeeReader extends HyperlaneReader { multiProvider; chain; concurrency; logger = rootLogger.child({ module: 'EvmTokenFeeReader' }); constructor(multiProvider, chain, concurrency = multiProvider.tryGetRpcConcurrency(chain) ?? DEFAULT_CONTRACT_READ_CONCURRENCY) { super(multiProvider, chain); this.multiProvider = multiProvider; this.chain = chain; this.concurrency = concurrency; } async deriveTokenFeeConfig(params) { const { address, routingDestinations, crossCollateralRouters } = params; const tokenFee = BaseFee__factory.connect(address, this.provider); let derivedConfig; const onchainFeeType = await tokenFee.feeType(); switch (onchainFeeType) { case OnchainTokenFeeType.LinearFee: derivedConfig = await this.deriveLinearFeeConfig(address); break; case OnchainTokenFeeType.ProgressiveFee: derivedConfig = await this.deriveProgressiveFeeConfig(address); break; case OnchainTokenFeeType.RegressiveFee: derivedConfig = await this.deriveRegressiveFeeConfig(address); break; case OnchainTokenFeeType.RoutingFee: derivedConfig = await this.deriveRoutingFeeConfig({ address, routingDestinations, }); break; case OnchainTokenFeeType.OffchainQuotedLinearFee: derivedConfig = await this.deriveOffchainQuotedLinearFeeConfig(address); break; case OnchainTokenFeeType.CrossCollateralRoutingFee: derivedConfig = await this.deriveCrossCollateralRoutingFeeConfig({ address, routingDestinations, crossCollateralRouters, }); break; default: throw new Error(`Unsupported token fee type: ${onchainFeeType}`); } return derivedConfig; } async deriveLinearFeeConfig(address) { const tokenFee = LinearFee__factory.connect(address, this.provider); const [token, owner, maxFee, halfAmount] = await Promise.all([ tokenFee.token(), tokenFee.owner(), tokenFee.maxFee(), tokenFee.halfAmount(), ]); const maxFeeBn = BigInt(maxFee.toString()); const halfAmountBn = BigInt(halfAmount.toString()); const bps = convertToBps(maxFeeBn, halfAmountBn); return { type: TokenFeeType.LinearFee, maxFee: maxFeeBn, halfAmount: halfAmountBn, address, bps, token, owner, }; } async deriveOffchainQuotedLinearFeeConfig(address) { const tokenFee = OffchainQuotedLinearFee__factory.connect(address, this.provider); const [token, owner, maxFee, halfAmount, quoteSigners] = await Promise.all([ tokenFee.token(), tokenFee.owner(), tokenFee.maxFee(), tokenFee.halfAmount(), tokenFee.quoteSigners(), ]); const maxFeeBn = BigInt(maxFee.toString()); const halfAmountBn = BigInt(halfAmount.toString()); const bps = convertToBps(maxFeeBn, halfAmountBn); return { type: TokenFeeType.OffchainQuotedLinearFee, maxFee: maxFeeBn, halfAmount: halfAmountBn, address, bps, token, owner, quoteSigners: [...quoteSigners], }; } async deriveProgressiveFeeConfig(_address) { throw new Error('Not implemented'); } async deriveRegressiveFeeConfig(_address) { throw new Error('Not implemented'); } async deriveRoutingFeeConfig(params) { const { address, routingDestinations } = params; const routingFee = RoutingFee__factory.connect(address, this.provider); const [token, owner] = await Promise.all([ routingFee.token(), routingFee.owner(), ]); const feeContracts = {}; if (routingDestinations) await Promise.all(routingDestinations.map(async (destination) => { const subFeeAddress = await routingFee.feeContracts(destination); if (subFeeAddress === constants.AddressZero) return; const chainName = this.multiProvider.tryGetChainName(destination); if (!chainName) return; feeContracts[chainName] = await this.deriveTokenFeeConfig({ address: subFeeAddress, }); })); return { type: TokenFeeType.RoutingFee, address, token, owner, feeContracts, }; } async deriveCrossCollateralRoutingFeeConfig(params) { const { address, routingDestinations, crossCollateralRouters } = params; const effectiveRoutingDestinations = getEffectiveCrossCollateralDestinations(routingDestinations, crossCollateralRouters); assert(effectiveRoutingDestinations.length > 0, 'CrossCollateralRoutingFee requires routingDestinations or crossCollateralRouters to derive fee config'); const routingFee = CrossCollateralRoutingFee__factory.connect(address, this.provider); const owner = await routingFee.owner(); const feeConfigCache = new Map(); const parseFeeConfig = this.createCachedFeeConfigReader(feeConfigCache); const destinationEntries = await concurrentMap(this.concurrency, effectiveRoutingDestinations, async (destination) => this.deriveCrossCollateralDestinationFees({ routingFee, destination, crossCollateralRouters, parseFeeConfig, })); const feeContracts = {}; for (const entry of destinationEntries) { if (!entry) continue; feeContracts[entry.chainName] = entry.routerFeeConfigs; } return { type: TokenFeeType.CrossCollateralRoutingFee, address, owner, feeContracts, }; } createCachedFeeConfigReader(feeConfigCache) { // Multiple router keys can point at the same child fee address. return async (subFeeAddress) => { const cacheKey = subFeeAddress.toLowerCase(); if (!feeConfigCache.has(cacheKey)) { feeConfigCache.set(cacheKey, this.deriveTokenFeeConfig({ address: subFeeAddress, })); } const cachedFeeConfig = feeConfigCache.get(cacheKey); assert(cachedFeeConfig, `Missing cached fee config promise for ${subFeeAddress}`); return cachedFeeConfig; }; } async deriveCrossCollateralDestinationFees({ routingFee, destination, crossCollateralRouters, parseFeeConfig, }) { const chainName = this.multiProvider.tryGetChainName(destination); if (!chainName) return undefined; const routerKeys = getCrossCollateralRouterKeys(destination, crossCollateralRouters); const routerSubFees = await concurrentMap(this.concurrency, routerKeys, async (router) => ({ router, subFeeAddress: await routingFee.feeContracts(destination, router), })); const routerFeeConfigs = {}; await concurrentMap(this.concurrency, routerSubFees, async ({ router, subFeeAddress }) => { if (subFeeAddress === constants.AddressZero) return; routerFeeConfigs[router] = await parseFeeConfig(subFeeAddress); }); if (Object.keys(routerFeeConfigs).length === 0) { return undefined; } return { chainName, routerFeeConfigs }; } convertFromBps(bps) { if (!Number.isFinite(bps) || bps <= 0) { throw new Error('bps must be > 0 to prevent division by zero'); } assertBpsPrecision(bps); const maxFee = BigInt(constants.MaxUint256.toString()) / ASSUMED_MAX_AMOUNT_FOR_ZERO_SUPPLY; const scaledBps = BigInt(Math.round(bps * Number(BPS_PRECISION))); const halfAmount = ((maxFee / 2n) * MAX_BPS * BPS_PRECISION) / scaledBps; return { maxFee, halfAmount, }; } } //# sourceMappingURL=EvmTokenFeeReader.js.map