@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
182 lines • 8.49 kB
JavaScript
import { constants } from 'ethers';
import { RoutingFee__factory } from '@hyperlane-xyz/core';
import { assert, deepEquals, eqAddress, isZeroishAddress, objMap, objMerge, objOmit, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmTokenFeeDeployer } from './EvmTokenFeeDeployer.js';
import { EvmTokenFeeReader, } from './EvmTokenFeeReader.js';
import { ImmutableTokenFeeType, TokenFeeConfigSchema, TokenFeeType, } from './types.js';
export class EvmTokenFeeModule extends HyperlaneModule {
multiProvider;
contractVerifier;
logger = rootLogger.child({ module: 'EvmTokenFeeModule' });
deployer;
reader;
chainName;
chainId;
constructor(multiProvider, params, contractVerifier) {
super(params);
this.multiProvider = multiProvider;
this.contractVerifier = contractVerifier;
this.chainName = multiProvider.getChainName(params.chain);
this.chainId = multiProvider.getDomainId(this.chainName);
this.deployer = new EvmTokenFeeDeployer(multiProvider, this.chainName, {
logger: this.logger,
contractVerifier: contractVerifier,
});
this.reader = new EvmTokenFeeReader(multiProvider, this.chainName);
}
static async create({ multiProvider, chain, config, contractVerifier, }) {
const chainName = multiProvider.getChainName(chain);
const module = new EvmTokenFeeModule(multiProvider, {
addresses: {
deployedFee: constants.AddressZero,
},
chain,
config,
}, contractVerifier);
const contracts = await module.deploy({
multiProvider,
chainName,
contractVerifier,
config,
});
module.args.addresses.deployedFee =
contracts[chainName][config.type].address;
return module;
}
// Processes the Input config to the Final config
// For LinearFee, it converts the bps to maxFee and halfAmount
static async expandConfig(params) {
const { config, multiProvider, chainName } = params;
let intermediaryConfig;
if (config.type === TokenFeeType.LinearFee) {
const reader = new EvmTokenFeeReader(params.multiProvider, params.chainName);
let { maxFee, halfAmount } = config;
if (!isZeroishAddress(config.token)) {
const { maxFee: convertedMaxFee, halfAmount: convertedHalfAmount } = await reader.convertFromBps(config.bps, config.token);
maxFee = convertedMaxFee;
halfAmount = convertedHalfAmount;
}
assert(maxFee && halfAmount, 'Config properties "maxFee" and "halfAmount" must be supplied when "token" is not supplied');
intermediaryConfig = {
type: TokenFeeType.LinearFee,
token: config.token,
owner: config.owner,
bps: config.bps,
maxFee,
halfAmount,
};
}
else if (config.type === TokenFeeType.RoutingFee) {
const feeContracts = config.feeContracts
? await promiseObjAll(objMap(config.feeContracts, async (_, innerConfig) => {
return EvmTokenFeeModule.expandConfig({
config: innerConfig,
multiProvider,
chainName,
});
}))
: undefined;
intermediaryConfig = {
type: TokenFeeType.RoutingFee,
token: config.token,
owner: config.owner,
maxFee: constants.MaxUint256.toBigInt(),
halfAmount: constants.MaxUint256.toBigInt(),
feeContracts,
};
}
else {
intermediaryConfig = config;
}
return TokenFeeConfigSchema.parse(intermediaryConfig);
}
async deploy(params) {
const deployer = new EvmTokenFeeDeployer(params.multiProvider, params.chainName, {
contractVerifier: params.contractVerifier,
});
return deployer.deploy({ [params.chainName]: params.config });
}
async read(params) {
const address = params?.address ?? this.args.addresses.deployedFee;
const routingDestinations = params?.routingDestinations;
return this.reader.deriveTokenFeeConfig({
address: address,
routingDestinations,
});
}
async update(targetConfig, params) {
let updateTransactions = [];
const normalizedTargetConfig = normalizeConfig(await EvmTokenFeeModule.expandConfig({
config: targetConfig,
multiProvider: this.multiProvider,
chainName: this.chainName,
}));
const actualConfig = await this.read(params);
const normalizedActualConfig = normalizeConfig(actualConfig);
//If configs are the same, return empty array
if (deepEquals(normalizedActualConfig, normalizedTargetConfig)) {
this.logger.debug(`Same config for ${normalizedTargetConfig.type}, no update needed`);
return [];
}
// Redeploy immutable fee types, if owner is the same, but the rest of the config is different
const nonOwnerDiffers = !deepEquals(objOmit(normalizedActualConfig, { owner: true }), objOmit(normalizedTargetConfig, { owner: true }));
if (ImmutableTokenFeeType.includes(normalizedTargetConfig.type) &&
nonOwnerDiffers) {
this.logger.info(`Immutable fee type ${normalizedTargetConfig.type}, redeploying`);
const contracts = await this.deploy({
config: normalizedTargetConfig,
multiProvider: this.multiProvider,
chainName: this.chainName,
contractVerifier: this.contractVerifier,
});
this.args.addresses.deployedFee =
contracts[this.chainName][normalizedTargetConfig.type].address;
return [];
}
// if the type is a mutable (for now, only routing fee), then update
updateTransactions = [
...(await this.updateRoutingFee(objMerge(actualConfig, normalizedTargetConfig, 10, true))),
...this.createOwnershipUpdateTxs(normalizedActualConfig, normalizedTargetConfig),
];
return updateTransactions;
}
async updateRoutingFee(targetConfig) {
const updateTransactions = [];
if (!targetConfig.feeContracts)
return [];
const currentRoutingAddress = this.args.addresses.deployedFee;
await promiseObjAll(objMap(targetConfig.feeContracts, async (chainName, config) => {
const address = config.address;
const subFeeModule = new EvmTokenFeeModule(this.multiProvider, {
addresses: {
deployedFee: address,
},
chain: this.chainName,
config,
}, this.contractVerifier);
const subFeeUpdateTransactions = await subFeeModule.update(config, {
address,
});
const { deployedFee: deployedSubFee } = subFeeModule.serialize();
updateTransactions.push(...subFeeUpdateTransactions);
if (!eqAddress(deployedSubFee, address)) {
const annotation = `Sub fee contract redeployed. Updating contract for ${chainName} to ${deployedSubFee}`;
this.logger.debug(annotation);
updateTransactions.push({
annotation: annotation,
chainId: this.chainId,
to: currentRoutingAddress,
data: RoutingFee__factory.createInterface().encodeFunctionData('setFeeContract(uint32,address)', [this.multiProvider.getDomainId(chainName), deployedSubFee]),
});
}
}));
return updateTransactions;
}
createOwnershipUpdateTxs(actualConfig, expectedConfig) {
return transferOwnershipTransactions(this.multiProvider.getEvmChainId(this.args.chain), this.args.addresses.deployedFee, actualConfig, expectedConfig, `${expectedConfig.type} Warp Route`);
}
}
//# sourceMappingURL=EvmTokenFeeModule.js.map