diff --git a/README.md b/README.md index 77a43bf50a..a612834350 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Note this is the branch for Hyperlane v3. -V2 is on the main branch but is eventually being phased out. +V2 is still in operation but is not being actively developed. The code for V2 can be found in the [v2](https://github.com/hyperlane-xyz/hyperlane-monorepo/tree/v2) branch. -V1 has since been deprecated in favor of V2, but if you are looking for code relating to the existing V1 deployments of the `testnet2` or `mainnet` environments, refer to the [v1](https://github.com/hyperlane-xyz/hyperlane-monorepo/tree/v1) branch. +V1 has since been deprecated in favor of V2, but if you are looking for code relating to the existing V1 deployments, refer to the [v1](https://github.com/hyperlane-xyz/hyperlane-monorepo/tree/v1) branch. ## Overview diff --git a/package.json b/package.json index 4368bf595f..d0194f557c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "lodash": "^4.17.21", "recursive-readdir": "^2.2.3", "underscore": "^1.13", - "undici": "^5.11" + "undici": "^5.11", + "@trivago/prettier-plugin-sort-imports/@babel/parser": "^7.22.7" } } diff --git a/solidity/contracts/token/FastHypERC20.sol b/solidity/contracts/token/FastHypERC20.sol new file mode 100644 index 0000000000..9ec040809a --- /dev/null +++ b/solidity/contracts/token/FastHypERC20.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypERC20} from "./HypERC20.sol"; + +import {TokenRouter} from "./libs/TokenRouter.sol"; +import {FastTokenRouter} from "./libs/FastTokenRouter.sol"; +import {TokenMessage} from "./libs/TokenMessage.sol"; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +/** + * @title Hyperlane ERC20 Token Router that extends ERC20 with remote transfer functionality. + * @author Abacus Works + * @dev Supply on each chain is not constant but the aggregate supply across all chains is. + */ +contract FastHypERC20 is FastTokenRouter, HypERC20 { + constructor( + uint8 __decimals, + address _mailbox + ) HypERC20(__decimals, _mailbox) {} + + /** + * @dev delegates transfer logic to `_transferTo`. + * @inheritdoc TokenRouter + */ + function _handle( + uint32 _origin, + bytes32 _sender, + bytes calldata _message + ) internal virtual override(FastTokenRouter, TokenRouter) { + FastTokenRouter._handle(_origin, _sender, _message); + } + + /** + * @dev Mints `_amount` of tokens to `_recipient`. + * @inheritdoc FastTokenRouter + */ + function _fastTransferTo( + address _recipient, + uint256 _amount + ) internal override { + _mint(_recipient, _amount); + } + + /** + * @dev Burns `_amount` of tokens from `_recipient`. + * @inheritdoc FastTokenRouter + */ + function _fastRecieveFrom( + address _sender, + uint256 _amount + ) internal override { + _burn(_sender, _amount); + } + + function balanceOf( + address _account + ) public view virtual override(TokenRouter, HypERC20) returns (uint256) { + return HypERC20.balanceOf(_account); + } +} diff --git a/solidity/contracts/token/FastHypERC20Collateral.sol b/solidity/contracts/token/FastHypERC20Collateral.sol new file mode 100644 index 0000000000..434555c17a --- /dev/null +++ b/solidity/contracts/token/FastHypERC20Collateral.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypERC20Collateral} from "./HypERC20Collateral.sol"; +import {TokenRouter} from "./libs/TokenRouter.sol"; +import {FastTokenRouter} from "./libs/FastTokenRouter.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title Hyperlane ERC20 Token Collateral that wraps an existing ERC20 with remote transfer functionality. + * @author Abacus Works + */ +contract FastHypERC20Collateral is FastTokenRouter, HypERC20Collateral { + using SafeERC20 for IERC20; + + /** + * @notice Constructor + * @param erc20 Address of the token to keep as collateral + * @param _mailbox Address of the mailbox address + */ + constructor( + address erc20, + address _mailbox + ) HypERC20Collateral(erc20, _mailbox) {} + + /** + * @dev delegates transfer logic to `_transferTo`. + * @inheritdoc FastTokenRouter + */ + function _handle( + uint32 _origin, + bytes32 _sender, + bytes calldata _message + ) internal virtual override(FastTokenRouter, TokenRouter) { + FastTokenRouter._handle(_origin, _sender, _message); + } + + /** + * @dev Transfers `_amount` of `wrappedToken` to `_recipient`. + * @inheritdoc FastTokenRouter + */ + function _fastTransferTo( + address _recipient, + uint256 _amount + ) internal override { + wrappedToken.safeTransfer(_recipient, _amount); + } + + /** + * @dev Transfers in `_amount` of `wrappedToken` from `_recipient`. + * @inheritdoc FastTokenRouter + */ + function _fastRecieveFrom( + address _sender, + uint256 _amount + ) internal override { + wrappedToken.safeTransferFrom(_sender, address(this), _amount); + } +} diff --git a/solidity/package.json b/solidity/package.json index 89bef1c7fc..89b093839d 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "3.1.2", + "version": "3.1.3", "dependencies": { "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "3.1.2", + "@hyperlane-xyz/utils": "3.1.3", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^v4.9.3" }, diff --git a/typescript/cli/ci-test-docker.sh b/typescript/cli/ci-test-docker.sh index b2f7a0b024..416f3847ef 100755 --- a/typescript/cli/ci-test-docker.sh +++ b/typescript/cli/ci-test-docker.sh @@ -25,12 +25,12 @@ export DEBUG=hyperlane:* echo "Deploying contracts to anvil1 and anvil2" yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \ - --chain-configs ./examples/anvil-chains.yaml \ - --chains anvil1,anvil2 \ + --targets anvil1,anvil2 \ + --chains ./examples/anvil-chains.yaml \ --artifacts /tmp/empty-artifacts.json \ - --out /tmp \ --ism ./examples/multisig-ism.yaml \ --hook ./examples/hook-config.yaml \ + --out /tmp \ --key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --yes diff --git a/typescript/cli/ci-test.sh b/typescript/cli/ci-test.sh index 276c5c9391..2c185f8357 100755 --- a/typescript/cli/ci-test.sh +++ b/typescript/cli/ci-test.sh @@ -18,8 +18,8 @@ do chmod -R 777 /tmp/relayer /tmp/$CHAIN done -anvil --chain-id 31337 -p 8545 --state /tmp/anvil1/state > /dev/null & -anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state > /dev/null & +anvil --chain-id 31337 -p 8545 --state /tmp/anvil1/state --gas-price 1 > /dev/null & +anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state --gas-price 1 > /dev/null & sleep 1 set -e @@ -28,17 +28,25 @@ echo "{}" > /tmp/empty-artifacts.json export DEBUG=hyperlane:* +DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]') +BEFORE=$(cast balance $DEPLOYER --rpc-url http://localhost:8545) + echo "Deploying contracts to anvil1 and anvil2" yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \ - --chain-configs ./examples/anvil-chains.yaml \ - --chains anvil1,anvil2 \ + --targets anvil1,anvil2 \ + --chains ./examples/anvil-chains.yaml \ --artifacts /tmp/empty-artifacts.json \ - --out /tmp \ --ism ./examples/multisig-ism.yaml \ --hook ./examples/hook-config.yaml \ + --out /tmp \ --key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --yes +AFTER_CORE=$(cast balance $DEPLOYER --rpc-url http://localhost:8545) +GAS_PRICE=$(cast gas-price --rpc-url http://localhost:8545) +CORE_MIN_GAS=$(bc <<< "($BEFORE - $AFTER_CORE) / $GAS_PRICE") +echo "Gas used: $CORE_MIN_GAS" + CORE_ARTIFACTS_PATH=`find /tmp/core-deployment* -type f -exec ls -t1 {} + | head -1` echo "Core artifacts:" echo $CORE_ARTIFACTS_PATH @@ -54,6 +62,11 @@ yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \ --key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --yes +AFTER_WARP=$(cast balance $DEPLOYER --rpc-url http://localhost:8545) +GAS_PRICE=$(cast gas-price --rpc-url http://localhost:8545) +WARP_MIN_GAS=$(bc <<< "($AFTER_CORE - $AFTER_WARP) / $GAS_PRICE") +echo "Gas used: $WARP_MIN_GAS" + echo "Sending test message" yarn workspace @hyperlane-xyz/cli run hyperlane send message \ --origin anvil1 \ @@ -64,6 +77,11 @@ yarn workspace @hyperlane-xyz/cli run hyperlane send message \ --key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ | tee /tmp/message1 +AFTER_MSG=$(cast balance $DEPLOYER --rpc-url http://localhost:8545) +GAS_PRICE=$(cast gas-price --rpc-url http://localhost:8545) +MSG_MIN_GAS=$(bc <<< "($AFTER_WARP - $AFTER_MSG) / $GAS_PRICE") +echo "Gas used: $MSG_MIN_GAS" + MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'` echo "Message 1 ID: $MESSAGE1_ID" diff --git a/typescript/cli/examples/multisig-ism.yaml b/typescript/cli/examples/multisig-ism.yaml index 9705d44f44..dd7c65864e 100644 --- a/typescript/cli/examples/multisig-ism.yaml +++ b/typescript/cli/examples/multisig-ism.yaml @@ -4,12 +4,13 @@ # Valid module types: # routing # aggregation -# legacy_multisig -# merkle_root_multisig -# message_id_multisig +# merkleRootMultisig +# messageIdMultisigIsm + +# ism type don't work currently (sets to messageIdMultisigIsm for all) --- anvil1: - type: 'merkleRootMultisigIsm' + type: 'messageIdMultisigIsm' threshold: 1 # Number: Signatures required to approve a message validators: # Array: List of validator addresses - '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 3c3e25ac5f..73e945fa4a 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,9 +1,9 @@ { "name": "@hyperlane-xyz/cli", - "version": "3.1.2", + "version": "3.1.3", "description": "A command-line utility for common Hyperlane operations", "dependencies": { - "@hyperlane-xyz/sdk": "3.1.2", + "@hyperlane-xyz/sdk": "3.1.3", "@inquirer/prompts": "^3.0.0", "bignumber.js": "^9.1.1", "chalk": "^5.3.0", diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index 232f5dee1a..5ef6e1b508 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -1,7 +1,7 @@ import { CommandModule } from 'yargs'; import { log, logGreen } from '../../logger.js'; -import { createChainConfig, readChainConfig } from '../config/chain.js'; +import { createChainConfig, readChainConfigs } from '../config/chain.js'; import { createHookConfig } from '../config/hooks.js'; import { createMultisigConfig, @@ -39,21 +39,21 @@ const createCommand: CommandModule = { describe: 'Create a new Hyperlane config', builder: (yargs) => yargs - .command(createChainCommand) - .command(createMultisigCommand) + .command(createChainConfigCommand) + .command(createMultisigConfigCommand) .command(createHookConfigCommand) - .command(createWarpCommand) + .command(createWarpConfigCommand) .version(false) .demandCommand(), handler: () => log('Command required'), }; -const createChainCommand: CommandModule = { +const createChainConfigCommand: CommandModule = { command: 'chain', describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)', builder: (yargs) => yargs.options({ - output: outputFileOption('./configs/chain-config.yaml'), + output: outputFileOption('./configs/chains.yaml'), format: fileFormatOption, }), handler: async (argv: any) => { @@ -64,7 +64,7 @@ const createChainCommand: CommandModule = { }, }; -const createMultisigCommand: CommandModule = { +const createMultisigConfigCommand: CommandModule = { command: 'multisig', describe: 'Create a new Multisig ISM config', builder: (yargs) => @@ -87,7 +87,7 @@ const createHookConfigCommand: CommandModule = { describe: 'Create a new Hook config', builder: (yargs) => yargs.options({ - output: outputFileOption('./configs/hook-config.yaml'), + output: outputFileOption('./configs/hooks.yaml'), format: fileFormatOption, chains: chainsCommandOption, }), @@ -100,7 +100,7 @@ const createHookConfigCommand: CommandModule = { }, }; -const createWarpCommand: CommandModule = { +const createWarpConfigCommand: CommandModule = { command: 'warp', describe: 'Create a new Warp Route tokens config', builder: (yargs) => @@ -147,7 +147,7 @@ const validateChainCommand: CommandModule = { }), handler: async (argv) => { const path = argv.path as string; - readChainConfig(path); + readChainConfigs(path); process.exit(0); }, }; diff --git a/typescript/cli/src/commands/deploy.ts b/typescript/cli/src/commands/deploy.ts index 3f9fa46f58..e672398fb8 100644 --- a/typescript/cli/src/commands/deploy.ts +++ b/typescript/cli/src/commands/deploy.ts @@ -35,14 +35,12 @@ const coreCommand: CommandModule = { describe: 'Deploy core Hyperlane contracts', builder: (yargs) => yargs.options({ - key: keyCommandOption, - 'chain-configs': chainsCommandOption, - chains: { + targets: { type: 'string', description: 'Comma separated list of chain names to which contracts will be deployed', }, - out: outDirCommandOption, + chains: chainsCommandOption, artifacts: coreArtifactsOption, ism: { type: 'string', @@ -54,15 +52,17 @@ const coreCommand: CommandModule = { description: 'A path to a JSON or YAML file with Hook configs (for every chain)', }, + out: outDirCommandOption, + key: keyCommandOption, yes: skipConfirmationOption, }), handler: async (argv: any) => { logGray('Hyperlane permissionless core deployment'); logGray('----------------------------------------'); const key: string = argv.key || process.env.HYP_KEY; - const chainConfigPath: string = argv['chain-configs']; + const chainConfigPath: string = argv.chains; const outPath: string = argv.out; - const chains: string[] | undefined = argv.chains + const chains: string[] | undefined = argv.targets ?.split(',') .map((r: string) => r.trim()); const artifactsPath: string = argv.artifacts; @@ -91,14 +91,15 @@ const warpCommand: CommandModule = { describe: 'Deploy Warp Route contracts', builder: (yargs) => yargs.options({ - key: keyCommandOption, - chains: chainsCommandOption, - out: outDirCommandOption, - core: coreArtifactsOption, config: { type: 'string', description: 'A path to a JSON or YAML file with a warp config.', + default: './configs/warp-tokens.yaml', }, + core: coreArtifactsOption, + chains: chainsCommandOption, + out: outDirCommandOption, + key: keyCommandOption, yes: skipConfirmationOption, }), handler: async (argv: any) => { diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 0e34a8dd8d..ee0b2acfc0 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -11,8 +11,8 @@ export const keyCommandOption: Options = { export const chainsCommandOption: Options = { type: 'string', description: 'A path to a JSON or YAML file with chain configs', - default: './configs/chain-config.yaml', - alias: 'cc', + default: './configs/chains.yaml', + alias: 'c', }; export const outDirCommandOption: Options = { diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts index f9cf7f2191..90bd7e526e 100644 --- a/typescript/cli/src/commands/send.ts +++ b/typescript/cli/src/commands/send.ts @@ -32,8 +32,6 @@ export const sendCommand: CommandModule = { */ const messageOptions: { [k: string]: Options } = { key: keyCommandOption, - chains: chainsCommandOption, - core: coreArtifactsOption, origin: { type: 'string', description: 'Origin chain to send message from', @@ -44,6 +42,8 @@ const messageOptions: { [k: string]: Options } = { description: 'Destination chain to send message to', demandOption: true, }, + core: coreArtifactsOption, + chains: chainsCommandOption, timeout: { type: 'number', description: 'Timeout in seconds', diff --git a/typescript/cli/src/commands/status.ts b/typescript/cli/src/commands/status.ts index ab0a39b20a..6cc99db458 100644 --- a/typescript/cli/src/commands/status.ts +++ b/typescript/cli/src/commands/status.ts @@ -9,8 +9,6 @@ export const statusCommand: CommandModule = { describe: 'Check status of a message', builder: (yargs) => yargs.options({ - chains: chainsCommandOption, - core: coreArtifactsOption, id: { type: 'string', description: 'Message ID', @@ -21,6 +19,8 @@ export const statusCommand: CommandModule = { description: 'Destination chain name', demandOption: true, }, + chains: chainsCommandOption, + core: coreArtifactsOption, }), handler: async (argv: any) => { const chainConfigPath: string = argv.chains; diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index e8621e6086..7088fc8f14 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -12,7 +12,7 @@ import { errorRed, log, logBlue, logGreen } from '../../logger.js'; import { getMultiProvider } from '../context.js'; import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js'; -export function readChainConfig(filePath: string) { +export function readChainConfigs(filePath: string) { log(`Reading file configs in ${filePath}`); const chainToMetadata = readYamlOrJson>(filePath); @@ -47,12 +47,12 @@ export function readChainConfig(filePath: string) { return chainToMetadata; } -export function readChainConfigIfExists(filePath: string) { +export function readChainConfigsIfExists(filePath: string) { if (!fs.existsSync(filePath)) { log('No chain config file provided'); return {}; } else { - return readChainConfig(filePath); + return readChainConfigs(filePath); } } diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index a4e4e1f2af..10f8f12b34 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -13,7 +13,7 @@ import { MerkleTreeHookConfig, MultisigIsmConfig, ProtocolFeeHookConfig, - defaultMultisigIsmConfigs, + defaultMultisigConfigs, multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; import { @@ -27,7 +27,7 @@ import { errorRed, log, logBlue, logGreen, logRed } from '../../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js'; -import { readChainConfigIfExists } from './chain.js'; +import { readChainConfigsIfExists } from './chain.js'; const ProtocolFeeSchema = z.object({ type: z.literal(HookType.PROTOCOL_FEE), @@ -72,9 +72,9 @@ export function presetHookConfigs( if (ismConfig) { validatorThreshold = ismConfig.threshold; validatorCount = ismConfig.validators.length; - } else if (local in defaultMultisigIsmConfigs) { - validatorThreshold = defaultMultisigIsmConfigs[local].threshold; - validatorCount = defaultMultisigIsmConfigs[local].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'); } @@ -151,7 +151,7 @@ export async function createHookConfig({ chainConfigPath: string; }) { logBlue('Creating a new hook config'); - const customChains = readChainConfigIfExists(chainConfigPath); + const customChains = readChainConfigsIfExists(chainConfigPath); const chains = await runMultiChainSelectionStep(customChains); const result: HookConfigMap = {}; diff --git a/typescript/cli/src/config/multisig.ts b/typescript/cli/src/config/multisig.ts index 26f930d47b..c9483ae039 100644 --- a/typescript/cli/src/config/multisig.ts +++ b/typescript/cli/src/config/multisig.ts @@ -1,14 +1,14 @@ import { input, select } from '@inquirer/prompts'; import { z } from 'zod'; -import { ChainMap, IsmType, MultisigIsmConfig } from '@hyperlane-xyz/sdk'; +import { ChainMap, IsmType, MultisigConfig } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; import { errorRed, log, logBlue, logGreen } from '../../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js'; -import { readChainConfigIfExists } from './chain.js'; +import { readChainConfigsIfExists } from './chain.js'; const MultisigConfigMapSchema = z.object({}).catchall( z.object({ @@ -30,14 +30,14 @@ export function readMultisigConfig(filePath: string) { ); } const parsedConfig = result.data; - const formattedConfig: ChainMap = objMap( + const formattedConfig: ChainMap = objMap( parsedConfig, (_, config) => ({ type: config.type as IsmType, threshold: config.threshold, validators: config.validators, - } as MultisigIsmConfig), + } as MultisigConfig), ); logGreen(`All multisig configs in ${filePath} are valid`); @@ -58,7 +58,7 @@ export async function createMultisigConfig({ chainConfigPath: string; }) { logBlue('Creating a new multisig config'); - const customChains = readChainConfigIfExists(chainConfigPath); + const customChains = readChainConfigsIfExists(chainConfigPath); const chains = await runMultiChainSelectionStep(customChains); const result: MultisigConfigMap = {}; diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index c92ca291e2..1b456b21df 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -11,7 +11,7 @@ import { } from '../utils/chains.js'; import { FileFormat, readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; -import { readChainConfigIfExists } from './chain.js'; +import { readChainConfigsIfExists } from './chain.js'; const ConnectionConfigSchema = { mailbox: z.string().optional(), @@ -78,7 +78,7 @@ export async function createWarpConfig({ chainConfigPath: string; }) { logBlue('Creating a new warp route config'); - const customChains = readChainConfigIfExists(chainConfigPath); + const customChains = readChainConfigsIfExists(chainConfigPath); const baseChain = await runSingleChainSelectionStep( customChains, 'Select base chain with the original token to warp', diff --git a/typescript/cli/src/consts.ts b/typescript/cli/src/consts.ts index 2b93a4eb55..9e05f2fcb6 100644 --- a/typescript/cli/src/consts.ts +++ b/typescript/cli/src/consts.ts @@ -1,4 +1,3 @@ -// TODO revisit these rough balance requirements with more precise measurements -export const MINIMUM_CORE_DEPLOY_BALANCE = '500000000000000000'; // 0.5 ETH -export const MINIMUM_WARP_DEPLOY_BALANCE = '200000000000000000'; // 0.2 Eth -export const MINIMUM_TEST_SEND_BALANCE = '10000000000000000'; // 0.01 ETH +export const MINIMUM_CORE_DEPLOY_GAS = (1e8).toString(); +export const MINIMUM_WARP_DEPLOY_GAS = (1e7).toString(); +export const MINIMUM_TEST_SEND_GAS = (3e5).toString(); diff --git a/typescript/cli/src/context.ts b/typescript/cli/src/context.ts index 710e63b72e..da87c0db32 100644 --- a/typescript/cli/src/context.ts +++ b/typescript/cli/src/context.ts @@ -10,7 +10,7 @@ import { } from '@hyperlane-xyz/sdk'; import { objMerge } from '@hyperlane-xyz/utils'; -import { readChainConfigIfExists } from './config/chain.js'; +import { readChainConfigsIfExists } from './config/chain.js'; import { keyToSigner } from './utils/keys.js'; export const sdkContractAddressesMap = { @@ -28,14 +28,14 @@ export function getMergedContractAddresses( } export function getContext(chainConfigPath: string) { - const customChains = readChainConfigIfExists(chainConfigPath); + const customChains = readChainConfigsIfExists(chainConfigPath); const multiProvider = getMultiProvider(customChains); return { customChains, multiProvider }; } export function getContextWithSigner(key: string, chainConfigPath: string) { const signer = keyToSigner(key); - const customChains = readChainConfigIfExists(chainConfigPath); + const customChains = readChainConfigsIfExists(chainConfigPath); const multiProvider = getMultiProvider(customChains, signer); return { signer, customChains, multiProvider }; } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 50009ec801..35628592ee 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -18,11 +18,12 @@ import { IgpConfig, IsmType, MultiProvider, - MultisigIsmConfig, + MultisigConfig, RoutingIsmConfig, agentStartBlocks, buildAgentConfig, - defaultMultisigIsmConfigs, + buildMultisigIsmConfigs, + defaultMultisigConfigs, multisigIsmVerificationCost, serializeContractsMap, } from '@hyperlane-xyz/sdk'; @@ -32,7 +33,7 @@ import { log, logBlue, logGray, logGreen, logRed } from '../../logger.js'; import { readDeploymentArtifacts } from '../config/artifacts.js'; import { readHookConfig } from '../config/hooks.js'; import { readMultisigConfig } from '../config/multisig.js'; -import { MINIMUM_CORE_DEPLOY_BALANCE } from '../consts.js'; +import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; import { getContextWithSigner, getMergedContractAddresses, @@ -99,7 +100,7 @@ export async function runCoreDeploy({ await runDeployPlanStep(deploymentParams); await runPreflightChecksForChains({ ...deploymentParams, - minBalanceWei: MINIMUM_CORE_DEPLOY_BALANCE, + minGas: MINIMUM_CORE_DEPLOY_GAS, }); await executeDeploy(deploymentParams); } @@ -149,33 +150,33 @@ async function runIsmStep(selectedChains: ChainName[], ismConfigPath?: string) { ); } // first we check for user provided chains - const configs = readMultisigConfig(ismConfigPath); - const userProvidedConfigChains = Object.keys(configs).filter((c) => - selectedChains.includes(c), + const multisigConfigs = { + ...defaultMultisigConfigs, + ...readMultisigConfig(ismConfigPath), + } as ChainMap; + const requiredMultisigs = objFilter( + multisigConfigs, + (chain, config): config is MultisigConfig => selectedChains.includes(chain), ); - // then our SDK defaults - const configsUnion = [ - ...userProvidedConfigChains, - ...Object.keys(defaultMultisigIsmConfigs), - ]; - // in case where the chains provided - all_configs = missing_configs + // selected chains - (user configs + default configs) = missing config const missingConfigs = selectedChains.filter( - (c) => !configsUnion.includes(c), + (c) => !Object.keys(requiredMultisigs).includes(c), ); if (missingConfigs.length > 0) { throw new Error( `Missing ISM config for one or more chains: ${missingConfigs.join(', ')}`, ); } + log(`Found configs for chains: ${selectedChains.join(', ')}`); - return configs; + return requiredMultisigs; } async function runHookStep( _selectedChains: ChainName[], hookConfigPath?: string, ) { - if ('TODO: Skip this step for now as values are unsused') return; + if ('TODO: Skip this step for now as values are unused') return; // const presetConfigChains = Object.keys(presetHookConfigs); @@ -200,7 +201,7 @@ interface DeployParams { signer: ethers.Signer; multiProvider: MultiProvider; artifacts?: HyperlaneAddressesMap; - multisigConfig?: ChainMap; + multisigConfig?: ChainMap; outPath: string; skipConfirmation: boolean; } @@ -254,7 +255,7 @@ async function executeDeploy({ const mergedContractAddrs = getMergedContractAddresses(artifacts); // 1. Deploy ISM factories to all deployable chains that don't have them. - log('Deploying ISM factory contracts'); + logBlue('Deploying ISM factory contracts'); const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); ismFactoryDeployer.cacheAddressesMap(mergedContractAddrs); @@ -279,7 +280,7 @@ async function executeDeploy({ ); // 3. Deploy ISM contracts to remote deployable chains - log('Deploying ISMs'); + logBlue('Deploying ISMs'); const ismContracts: ChainMap<{ multisigIsm: DeployedIsm }> = {}; const defaultIsms: ChainMap
= {}; for (const ismOrigin of chains) { @@ -288,12 +289,8 @@ async function executeDeploy({ defaultIsms[ismOrigin] = artifacts[ismOrigin].multisigIsm; continue; } - log(`Deploying ISM to ${ismOrigin}`); - const ismConfig = buildIsmConfig( - owner, - chains.filter((r) => r !== ismOrigin), - multisigConfig, - ); + logBlue(`Deploying ISM to ${ismOrigin}`); + const ismConfig = buildIsmConfig(owner, ismOrigin, chains, multisigConfig); ismContracts[ismOrigin] = { multisigIsm: await ismFactory.deploy(ismOrigin, ismConfig), }; @@ -303,7 +300,7 @@ async function executeDeploy({ logGreen('ISM contracts deployed'); // 4. Deploy core contracts to chains - log(`Deploying core contracts to ${chains.join(', ')}`); + logBlue(`Deploying core contracts to ${chains.join(', ')}`); const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory); coreDeployer.cacheAddressesMap(artifacts); const coreConfigs = buildCoreConfigMap( @@ -342,19 +339,20 @@ async function executeDeploy({ function buildIsmConfig( owner: Address, - remotes: ChainName[], - multisigIsmConfigs: ChainMap, + local: ChainName, + chains: ChainName[], + multisigIsmConfigs: ChainMap, ): RoutingIsmConfig { - const mergedMultisigIsmConfig: ChainMap = objMerge( - defaultMultisigIsmConfigs, + const multisigConfigs = buildMultisigIsmConfigs( + IsmType.MESSAGE_ID_MULTISIG, + local, + chains, multisigIsmConfigs, ); return { owner, type: IsmType.ROUTING, - domains: Object.fromEntries( - remotes.map((remote) => [remote, mergedMultisigIsmConfig[remote]]), - ), + domains: multisigConfigs, }; } @@ -362,7 +360,7 @@ function buildCoreConfigMap( owner: Address, chains: ChainName[], defaultIsms: ChainMap
, - multisigConfig: ChainMap, + multisigConfig: ChainMap, ): ChainMap { return chains.reduce>((config, chain) => { const igpConfig = buildIgpConfigMap(owner, chains, multisigConfig); @@ -414,12 +412,8 @@ function buildTestRecipientConfigMap( function buildIgpConfigMap( owner: Address, chains: ChainName[], - multisigIsmConfigs: ChainMap, + multisigConfigs: ChainMap, ): ChainMap { - const mergedMultisigIsmConfig: ChainMap = objMerge( - defaultMultisigIsmConfigs, - multisigIsmConfigs, - ); const configMap: ChainMap = {}; for (const chain of chains) { const overhead: ChainMap = {}; @@ -427,8 +421,8 @@ function buildIgpConfigMap( for (const remote of chains) { if (chain === remote) continue; overhead[remote] = multisigIsmVerificationCost( - mergedMultisigIsmConfig[chain].threshold, - mergedMultisigIsmConfig[chain].validators.length, + multisigConfigs[chain].threshold, + multisigConfigs[chain].validators.length, ); gasOracleType[remote] = GasOracleContractType.StorageGasOracle; } diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 82c62c90a3..871a96c5c6 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -4,7 +4,7 @@ import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { log, logGreen } from '../../logger.js'; -import { assertNativeBalances } from '../utils/balances.js'; +import { assertGasBalances } from '../utils/balances.js'; import { assertSigner } from '../utils/keys.js'; export async function runPreflightChecks({ @@ -12,13 +12,13 @@ export async function runPreflightChecks({ remotes, signer, multiProvider, - minBalanceWei, + minGas, }: { origin: ChainName; remotes: ChainName[]; signer: ethers.Signer; multiProvider: MultiProvider; - minBalanceWei: string; + minGas: string; }) { log('Running pre-flight checks...'); @@ -29,7 +29,7 @@ export async function runPreflightChecks({ chains: [origin, ...remotes], signer, multiProvider, - minBalanceWei, + minGas, }); } @@ -37,12 +37,12 @@ export async function runPreflightChecksForChains({ chains, signer, multiProvider, - minBalanceWei, + minGas, }: { chains: ChainName[]; signer: ethers.Signer; multiProvider: MultiProvider; - minBalanceWei: string; + minGas: string; }) { log('Running pre-flight checks...'); @@ -58,6 +58,6 @@ export async function runPreflightChecksForChains({ assertSigner(signer); logGreen('Signer is valid ✅'); - await assertNativeBalances(multiProvider, signer, chains, minBalanceWei); + await assertGasBalances(multiProvider, signer, chains, minGas); logGreen('Balances are sufficient ✅'); } diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 3580b885cb..215c2903bc 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -22,7 +22,7 @@ import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils'; import { log, logBlue, logGray, logGreen } from '../../logger.js'; import { readDeploymentArtifacts } from '../config/artifacts.js'; import { WarpRouteConfig, readWarpRouteConfig } from '../config/warp.js'; -import { MINIMUM_WARP_DEPLOY_BALANCE } from '../consts.js'; +import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getContextWithSigner, getMergedContractAddresses, @@ -86,7 +86,7 @@ export async function runWarpDeploy({ await runDeployPlanStep(deploymentParams); await runPreflightChecks({ ...deploymentParams, - minBalanceWei: MINIMUM_WARP_DEPLOY_BALANCE, + minGas: MINIMUM_WARP_DEPLOY_GAS, }); await executeDeploy(deploymentParams); } diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index b3ca0f8fd1..1b90c346cb 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -10,7 +10,7 @@ import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; import { errorRed, log, logBlue, logGreen } from '../../logger.js'; import { readDeploymentArtifacts } from '../config/artifacts.js'; -import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js'; +import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { getContextWithSigner, getMergedContractAddresses, @@ -48,7 +48,7 @@ export async function sendTestMessage({ remotes: [destination], multiProvider, signer, - minBalanceWei: MINIMUM_TEST_SEND_BALANCE, + minGas: MINIMUM_TEST_SEND_GAS, }); await timeout( diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index 44e3f287d4..5e413309ba 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -17,7 +17,7 @@ import { Address, timeout } from '@hyperlane-xyz/utils'; import { log, logBlue, logGreen } from '../../logger.js'; import { readDeploymentArtifacts } from '../config/artifacts.js'; -import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js'; +import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { getContextWithSigner, getMergedContractAddresses, @@ -78,7 +78,7 @@ export async function sendTestTransfer({ remotes: [destination], multiProvider, signer, - minBalanceWei: MINIMUM_TEST_SEND_BALANCE, + minGas: MINIMUM_TEST_SEND_GAS, }); await timeout( diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 153cf657ba..68519eac8a 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -1,3 +1,4 @@ +import { confirm } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { ERC20__factory } from '@hyperlane-xyz/core'; @@ -18,10 +19,31 @@ export async function assertNativeBalances( .getProvider(chain) .getBalance(address); const balance = ethers.utils.formatEther(balanceWei); - if (balanceWei.lt(minBalanceWei)) - throw new Error( - `${address} has insufficient balance on ${chain}. At least ${minBalance} required but found ${balance.toString()} ETH`, - ); + if (balanceWei.lt(minBalanceWei)) { + const symbol = + multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH'; + const error = `${address} has insufficient balance on ${chain}. At least ${minBalance} required but found ${balance.toString()} ${symbol}`; + const isResume = await confirm({ + message: `WARNING: ${error} Continue?`, + }); + if (!isResume) throw new Error(error); + } + }), + ); +} + +export async function assertGasBalances( + multiProvider: MultiProvider, + signer: ethers.Signer, + chains: ChainName[], + minGas: string, +) { + await Promise.all( + chains.map(async (chain) => { + const provider = multiProvider.getProvider(chain); + const gasPrice = await provider.getGasPrice(); + const minBalanceWei = gasPrice.mul(minGas).toString(); + await assertNativeBalances(multiProvider, signer, [chain], minBalanceWei); }), ); } diff --git a/typescript/helloworld/package.json b/typescript/helloworld/package.json index 11eae05c05..21fe3d32ae 100644 --- a/typescript/helloworld/package.json +++ b/typescript/helloworld/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/helloworld", "description": "A basic skeleton of an Hyperlane app", - "version": "3.1.2", + "version": "3.1.3", "dependencies": { - "@hyperlane-xyz/core": "3.1.2", - "@hyperlane-xyz/sdk": "3.1.2", + "@hyperlane-xyz/core": "3.1.3", + "@hyperlane-xyz/sdk": "3.1.3", "@openzeppelin/contracts-upgradeable": "^4.9.3", "ethers": "^5.7.2" }, diff --git a/typescript/infra/config/aggregationIsm.ts b/typescript/infra/config/aggregationIsm.ts deleted file mode 100644 index a781d8b5f8..0000000000 --- a/typescript/infra/config/aggregationIsm.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AggregationIsmConfig, ChainName } from '@hyperlane-xyz/sdk'; -import { IsmType } from '@hyperlane-xyz/sdk/dist/ism/types'; - -import { Contexts } from './contexts'; -import { multisigIsm } from './multisigIsm'; - -// Merkle Root Message ID -export const aggregationIsm = ( - remote: ChainName, - context: Contexts, -): AggregationIsmConfig => { - return { - type: IsmType.AGGREGATION, - modules: [ - // Ordering matters to preserve determinism - multisigIsm(remote, IsmType.MERKLE_ROOT_MULTISIG, context), - multisigIsm(remote, IsmType.MESSAGE_ID_MULTISIG, context), - ], - threshold: 1, - }; -}; diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index 64f4981267..aee860b461 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -2,7 +2,7 @@ import { ChainMap, GasOracleContractType, IgpConfig, - defaultMultisigIsmConfigs, + defaultMultisigConfigs, multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; import { exclude, objMap } from '@hyperlane-xyz/utils'; @@ -37,8 +37,8 @@ export const igp: ChainMap = objMap(owners, (chain, owner) => ({ exclude(chain, ethereumChainNames).map((remote) => [ remote, multisigIsmVerificationCost( - defaultMultisigIsmConfigs[remote].threshold, - defaultMultisigIsmConfigs[remote].validators.length, + defaultMultisigConfigs[remote].threshold, + defaultMultisigConfigs[remote].validators.length, ), ]), ), diff --git a/typescript/infra/config/environments/testnet4/core.ts b/typescript/infra/config/environments/testnet4/core.ts index 7a2ae2c321..6198c55799 100644 --- a/typescript/infra/config/environments/testnet4/core.ts +++ b/typescript/infra/config/environments/testnet4/core.ts @@ -14,7 +14,7 @@ import { MultisigIsmConfig, ProtocolFeeHookConfig, RoutingIsmConfig, - defaultMultisigIsmConfigs, + defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; @@ -26,7 +26,7 @@ export const core: ChainMap = objMap(owners, (local, owner) => { const originMultisigs: ChainMap = Object.fromEntries( supportedChainNames .filter((chain) => chain !== local) - .map((origin) => [origin, defaultMultisigIsmConfigs[origin]]), + .map((origin) => [origin, defaultMultisigConfigs[origin]]), ); const merkleRoot = (multisig: MultisigConfig): MultisigIsmConfig => ({ diff --git a/typescript/infra/config/environments/testnet4/igp.ts b/typescript/infra/config/environments/testnet4/igp.ts index d5719b5964..c098d0af61 100644 --- a/typescript/infra/config/environments/testnet4/igp.ts +++ b/typescript/infra/config/environments/testnet4/igp.ts @@ -2,7 +2,7 @@ import { ChainMap, GasOracleContractType, IgpConfig, - defaultMultisigIsmConfigs, + defaultMultisigConfigs, multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; import { exclude, objMap } from '@hyperlane-xyz/utils'; @@ -30,8 +30,8 @@ export const igp: ChainMap = objMap(owners, (chain, owner) => { remote, multisigIsmVerificationCost( // TODO: parameterize this - defaultMultisigIsmConfigs[remote].threshold, - defaultMultisigIsmConfigs[remote].validators.length, + defaultMultisigConfigs[remote].threshold, + defaultMultisigConfigs[remote].validators.length, ), ]), ), diff --git a/typescript/infra/config/multisigIsm.ts b/typescript/infra/config/multisigIsm.ts index 4eee71d68f..8f3f23d6e0 100644 --- a/typescript/infra/config/multisigIsm.ts +++ b/typescript/infra/config/multisigIsm.ts @@ -2,9 +2,9 @@ import { ChainMap, ChainName, MultisigIsmConfig, - defaultMultisigIsmConfigs, + buildMultisigIsmConfigs, + defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; -import { objFilter, objMap } from '@hyperlane-xyz/utils'; import { DeployEnvironment } from '../src/config'; @@ -25,20 +25,13 @@ export const multisigIsms = ( local: ChainName, type: MultisigIsmConfig['type'], context: Contexts, -): ChainMap => - objMap( - objFilter( - context === Contexts.ReleaseCandidate - ? rcMultisigIsmConfigs - : defaultMultisigIsmConfigs, - (chain, config): config is MultisigIsmConfig => - chain !== local && chains[env].includes(chain), - ), - (_, config) => ({ - ...config, - type, - }), - ); +): ChainMap => { + const multisigConfigs = + context === Contexts.ReleaseCandidate + ? rcMultisigIsmConfigs + : defaultMultisigConfigs; + return buildMultisigIsmConfigs(type, local, chains[env], multisigConfigs); +}; export const multisigIsm = ( remote: ChainName, @@ -48,7 +41,7 @@ export const multisigIsm = ( const configs = context === Contexts.ReleaseCandidate ? rcMultisigIsmConfigs - : defaultMultisigIsmConfigs; + : defaultMultisigConfigs; return { ...configs[remote], diff --git a/typescript/infra/config/routingIsm.ts b/typescript/infra/config/routingIsm.ts index e822ea7197..71590813f9 100644 --- a/typescript/infra/config/routingIsm.ts +++ b/typescript/infra/config/routingIsm.ts @@ -4,30 +4,31 @@ import { ChainName, IsmConfig, IsmType, + ModuleType, RoutingIsmConfig, + TestChains, } from '@hyperlane-xyz/sdk'; import { DeployEnvironment } from '../src/config'; -import { aggregationIsm } from './aggregationIsm'; import { Contexts } from './contexts'; import { supportedChainNames as mainnet3Chains } from './environments/mainnet3/chains'; import { owners as mainnet3Owners } from './environments/mainnet3/owners'; -import { chainNames as testChains } from './environments/test/chains'; import { owners as testOwners } from './environments/test/owners'; import { supportedChainNames as testnet4Chains } from './environments/testnet4/chains'; import { owners as testnet4Owners } from './environments/testnet4/owners'; - -const chains = { - mainnet3: mainnet3Chains, - testnet4: testnet4Chains, - test: testChains, -}; +import { multisigIsm } from './multisigIsm'; const owners = { + test: testOwners, testnet4: testnet4Owners, mainnet3: mainnet3Owners, - test: testOwners, +}; + +const chains = { + test: TestChains, + testnet4: testnet4Chains, + mainnet3: mainnet3Chains, }; // Intended to be the "entrypoint" ISM. @@ -58,16 +59,38 @@ export const routingIsm = ( }; }; +// Aggregation (1/2) +// | | +// | | +// v v +// Merkle Root Message ID +export const aggregationIsm = ( + remote: ChainName, + context: Contexts, +): AggregationIsmConfig => { + return { + type: IsmType.AGGREGATION, + modules: [ + // Ordering matters to preserve determinism + multisigIsm(remote, IsmType.MERKLE_ROOT_MULTISIG, context), + multisigIsm(remote, IsmType.MESSAGE_ID_MULTISIG, context), + ], + threshold: 1, + }; +}; + const replacerEnum = (key: string, value: any) => { if (key === 'type') { switch (value) { - case IsmType.AGGREGATION: + case ModuleType.AGGREGATION: return 'AGGREGATION'; - case IsmType.ROUTING: + case ModuleType.ROUTING: return 'ROUTING'; - case IsmType.MERKLE_ROOT_MULTISIG: + case ModuleType.MERKLE_ROOT_MULTISIG: return 'MERKLE_ROOT_MULTISIG'; - case IsmType.MESSAGE_ID_MULTISIG: + case ModuleType.LEGACY_MULTISIG: + return 'LEGACY_MULTISIG'; + case ModuleType.MESSAGE_ID_MULTISIG: return 'MESSAGE_ID_MULTISIG'; default: return value; diff --git a/typescript/infra/package.json b/typescript/infra/package.json index 8c1e227a22..206c2ac336 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "3.1.2", + "version": "3.1.3", "dependencies": { "@arbitrum/sdk": "^3.0.0", "@aws-sdk/client-iam": "^3.74.0", @@ -11,9 +11,9 @@ "@ethersproject/experimental": "^5.7.0", "@ethersproject/hardware-wallets": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@hyperlane-xyz/helloworld": "3.1.2", - "@hyperlane-xyz/sdk": "3.1.2", - "@hyperlane-xyz/utils": "3.1.2", + "@hyperlane-xyz/helloworld": "3.1.3", + "@hyperlane-xyz/sdk": "3.1.3", + "@hyperlane-xyz/utils": "3.1.3", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@safe-global/api-kit": "^1.3.0", "@safe-global/protocol-kit": "^1.2.0", @@ -33,7 +33,7 @@ "@types/node": "^16.9.1", "@types/prompts": "^2.0.14", "@types/sinon-chai": "^3.2.12", - "@types/yargs": "^17.0.10", + "@types/yargs": "^17.0.24", "chai": "^4.3.4", "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", diff --git a/typescript/infra/scripts/module-can-verify.ts b/typescript/infra/scripts/module-can-verify.ts index 33cec97a22..7dfb3c70a1 100644 --- a/typescript/infra/scripts/module-can-verify.ts +++ b/typescript/infra/scripts/module-can-verify.ts @@ -5,10 +5,6 @@ import { deployEnvToSdkEnv } from '../src/config/environment'; import { getArgs, getEnvironmentConfig } from './utils'; -// Hacky temporary script just to make sure that default ISMs are correct. -// Testnet3 has already been updated, mainnet3 hasn't, so the above cache -// is used for mainnet3. - async function main() { const args = await getArgs().argv; diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index 41bf8ccac7..812314220a 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -1,7 +1,7 @@ import { BigNumber, ethers } from 'ethers'; import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; -import { convertDecimalsEthersBigNumber } from '@hyperlane-xyz/utils'; +import { convertDecimals } from '@hyperlane-xyz/utils'; import { mustGetChainNativeTokenDecimals } from '../utils/utils'; @@ -77,15 +77,17 @@ export function getTokenExchangeRateFromValues( localValue: BigNumber, remote: ChainName, remoteValue: BigNumber, -) { +): BigNumber { // This does not yet account for decimals! const exchangeRate = remoteValue .mul(TOKEN_EXCHANGE_RATE_MULTIPLIER) .div(localValue); - return convertDecimalsEthersBigNumber( - mustGetChainNativeTokenDecimals(remote), - mustGetChainNativeTokenDecimals(local), - exchangeRate, + return BigNumber.from( + convertDecimals( + mustGetChainNativeTokenDecimals(remote), + mustGetChainNativeTokenDecimals(local), + exchangeRate.toString(), + ), ); } diff --git a/typescript/sdk/logos/black/base.svg b/typescript/sdk/logos/black/base.svg new file mode 100644 index 0000000000..be9b3c0889 --- /dev/null +++ b/typescript/sdk/logos/black/base.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/logos/black/polygonzkevm.svg b/typescript/sdk/logos/black/polygonzkevm.svg new file mode 100644 index 0000000000..2457c2bfbc --- /dev/null +++ b/typescript/sdk/logos/black/polygonzkevm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/logos/black/scroll.svg b/typescript/sdk/logos/black/scroll.svg new file mode 100644 index 0000000000..77e7199a9d --- /dev/null +++ b/typescript/sdk/logos/black/scroll.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/logos/color/base.svg b/typescript/sdk/logos/color/base.svg new file mode 100644 index 0000000000..59182e5dd7 --- /dev/null +++ b/typescript/sdk/logos/color/base.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/logos/color/polygonzkevm.svg b/typescript/sdk/logos/color/polygonzkevm.svg new file mode 100644 index 0000000000..98cca40919 --- /dev/null +++ b/typescript/sdk/logos/color/polygonzkevm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/logos/color/scroll.svg b/typescript/sdk/logos/color/scroll.svg new file mode 100644 index 0000000000..541dc4433f --- /dev/null +++ b/typescript/sdk/logos/color/scroll.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index dd72fdf3b9..b5e6d8bae5 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,12 +1,12 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "3.1.2", + "version": "3.1.3", "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/stargate": "^0.31.3", - "@hyperlane-xyz/core": "3.1.2", - "@hyperlane-xyz/utils": "3.1.2", + "@hyperlane-xyz/core": "3.1.3", + "@hyperlane-xyz/utils": "3.1.3", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.0", "@types/coingecko-api": "^1.0.10", diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index 53ba756272..7cda9b5e23 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -1,7 +1,7 @@ import { MultisigConfig } from '../ism/types'; import { ChainMap } from '../types'; -export const defaultMultisigIsmConfigs: ChainMap = { +export const defaultMultisigConfigs: ChainMap = { // ----------------- Mainnets ----------------- celo: { threshold: 2, @@ -77,11 +77,6 @@ export const defaultMultisigIsmConfigs: ChainMap = { '0xdb96116d13a2fadde9742d7cc88474a5ed39a03a', // everstake ], }, - // solana: { - // threshold: 0, - // validators: [ - // ], - // }, base: { threshold: 2, validators: [ @@ -106,6 +101,14 @@ export const defaultMultisigIsmConfigs: ChainMap = { '0x57231619fea13d85270ca6943298046c75a6dd01', // everstake ], }, + solana: { + threshold: 2, + validators: [ + '0x3cd1a081f38874bbb075bf10b62adcb858db864c', // abacus + '0x2b0c45f6111ae1c1684d4287792e3bd6ebd1abcc', // ZKV + '0x7b9ec253a8ba38994457eb9dbe386938d545351a', // everstake + ], + }, // ----------------- Testnets ----------------- alfajores: { threshold: 2, diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 900b25eada..cd980c0fbb 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -30,7 +30,7 @@ export { hyperlaneContractAddresses, hyperlaneEnvironments, } from './consts/environments'; -export { defaultMultisigIsmConfigs } from './consts/multisigIsm'; +export { defaultMultisigConfigs } from './consts/multisigIsm'; export { SEALEVEL_SPL_NOOP_ADDRESS } from './consts/sealevel'; export { attachContracts, @@ -127,6 +127,7 @@ export { collectValidators, moduleCanCertainlyVerify, } from './ism/HyperlaneIsmFactory'; +export { buildMultisigIsmConfigs } from './ism/multisig'; export { AggregationIsmConfig, DeployedIsm, diff --git a/typescript/sdk/src/ism/multisig.ts b/typescript/sdk/src/ism/multisig.ts new file mode 100644 index 0000000000..2846f006ce --- /dev/null +++ b/typescript/sdk/src/ism/multisig.ts @@ -0,0 +1,27 @@ +import { objFilter, objMap } from '@hyperlane-xyz/utils'; + +import { ChainMap, ChainName } from '../types'; + +import { MultisigConfig, MultisigIsmConfig } from './types'; + +// build multisigIsmConfig from multisigConfig +// eg. for { sepolia (local), arbitrumsepolia, scrollsepolia } +// arbitrumsepolia => Ism, scrollsepolia => Ism +export const buildMultisigIsmConfigs = ( + type: MultisigIsmConfig['type'], + local: ChainName, + chains: ChainName[], + multisigConfigs: ChainMap, +): ChainMap => { + return objMap( + objFilter( + multisigConfigs, + (chain, config): config is MultisigConfig => + chain !== local && chains.includes(chain), + ), + (_, config) => ({ + ...config, + type, + }), + ); +}; diff --git a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts index 362288169c..b6d3b00244 100644 --- a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts @@ -162,7 +162,7 @@ export class SealevelTokenAdapter // we generously request 1M units. const TRANSFER_REMOTE_COMPUTE_LIMIT = 1_000_000; -abstract class SealevelHypTokenAdapter +export abstract class SealevelHypTokenAdapter extends SealevelTokenAdapter implements IHypTokenAdapter { diff --git a/typescript/utils/.mocharc.json b/typescript/utils/.mocharc.json new file mode 100644 index 0000000000..e516df998c --- /dev/null +++ b/typescript/utils/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": ["ts-node/register"] +} diff --git a/typescript/utils/index.ts b/typescript/utils/index.ts index 35e81292dd..08abee53a1 100644 --- a/typescript/utils/index.ts +++ b/typescript/utils/index.ts @@ -39,7 +39,6 @@ export { } from './src/addresses'; export { convertDecimals, - convertDecimalsEthersBigNumber, eqAmountApproximate, fromWei, fromWeiRounded, diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 0162e1446d..4d3947371f 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,12 +1,13 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "3.1.2", + "version": "3.1.3", "dependencies": { "@cosmjs/encoding": "^0.31.3", "@solana/web3.js": "^1.78.0", "bignumber.js": "^9.1.1", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "mocha": "^10.2.0" }, "devDependencies": { "chai": "^4.3.0", @@ -27,7 +28,8 @@ "build": "tsc", "clean": "rm -rf ./dist", "check": "tsc --noEmit", - "prettier": "prettier --write ./src" + "prettier": "prettier --write ./src", + "test": "mocha --config .mocharc.json './src/**/*.test.ts'" }, "sideEffects": false, "types": "dist/index.d.ts", diff --git a/typescript/utils/src/amount.test.ts b/typescript/utils/src/amount.test.ts new file mode 100644 index 0000000000..4ba6c9962a --- /dev/null +++ b/typescript/utils/src/amount.test.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; + +import { eqAmountApproximate, fromWei, fromWeiRounded, toWei } from './amount'; + +describe('fromWei', () => { + it('parses and converts correctly', () => { + expect(fromWei(1, 0)).to.equal('1'); + expect(fromWei('1000000', 6)).to.equal('1'); + expect(fromWei('1000000000000000000')).to.equal('1'); + expect(fromWei('1000000000000000000.1234')).to.equal('1'); + }); +}); + +describe('fromWeiRounded', () => { + it('parses and converts correctly', () => { + expect(fromWeiRounded(1, 0)).to.equal('1.0000'); + expect(fromWeiRounded('1000000', 6)).to.equal('1.0000'); + expect(fromWeiRounded('1000000000000000000')).to.equal('1.0000'); + expect(fromWeiRounded('1000000000000000000.1234')).to.equal('1.0000'); + }); + + it('rounds correctly', () => { + expect(fromWeiRounded(1234567890, 6, 2)).to.equal('1234.56'); + expect(fromWeiRounded('1234567890', 6, 4)).to.equal('1234.5678'); + expect(fromWeiRounded('10000000000000000000')).to.equal('10.0000'); + expect(fromWeiRounded('10000000000000000000', 18, 0)).to.equal('10'); + }); + + it('can drop decimals for large numbers', () => { + expect(fromWeiRounded('10001000000000000000000')).to.equal('10001.00'); + expect(fromWeiRounded('10001000000000000000', 15, 4)).to.equal( + '10001.0000', + ); + }); +}); + +describe('toWei', () => { + it('parses and converts correctly', () => { + expect(toWei(1, 0)).to.equal('1'); + expect(toWei('1', 6)).to.equal('1000000'); + expect(toWei('123.456')).to.equal('123456000000000000000'); + expect(toWei('1.00000000000000000001')).to.equal('1000000000000000000'); + expect(toWei('1.00000000000000000001', 6)).to.equal('1000000'); + }); +}); + +describe('eqAmountApproximate', () => { + it('compares correctly', () => { + expect(eqAmountApproximate(1, 1.001, 0.001)).to.be.true; + expect(eqAmountApproximate(9, 9.001, 0.01)).to.be.true; + expect(eqAmountApproximate('9876543210', '9876543210', '1')).to.be.true; + expect(eqAmountApproximate('9876543210', '9876543212', '1')).to.be.false; + }); +}); diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index 9aae2cad65..f46998230e 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -1,77 +1,84 @@ import { formatUnits, parseUnits } from '@ethersproject/units'; import BigNumber from 'bignumber.js'; -import { ethers } from 'ethers'; -const DEFAULT_MIN_ROUNDED_VALUE = 0.00001; const DEFAULT_DISPLAY_DECIMALS = 4; const DEFAULT_TOKEN_DECIMALS = 18; -type NumberT = BigNumber.Value; - +/** + * Convert the given Wei value to Ether value + * @param value The value to convert. + * @returns Converted value in string type. + */ export function fromWei( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, -): number { - if (!value) return 0; - const valueString = value.toString().trim(); - const flooredValue = new BigNumber(valueString).toFixed( - 0, - BigNumber.ROUND_FLOOR, - ); - return parseFloat(formatUnits(flooredValue, decimals)); +): string { + if (!value) return (0).toString(); + const valueString = value.toString(10).trim(); + const flooredValue = BigNumber(valueString).toFixed(0, BigNumber.ROUND_FLOOR); + return parseFloat(formatUnits(flooredValue, decimals)).toString(); } -// Similar to fromWei above but rounds to set number of decimals -// with a minimum floor, configured per token +/** + * Convert the given Wei value to Ether value, + * round to set number of decimals with a minimum floor, configured per token + * @param value The value to convert. + * @param decimals + * @returns Converted value in string type. + */ export function fromWeiRounded( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, - roundDownIfSmall = true, + displayDecimals?: number, ): string { if (!value) return '0'; - const flooredValue = new BigNumber(value).toFixed(0, BigNumber.ROUND_FLOOR); - const amount = new BigNumber(formatUnits(flooredValue, decimals)); + const flooredValue = BigNumber(value).toFixed(0, BigNumber.ROUND_FLOOR); + const amount = BigNumber(formatUnits(flooredValue, decimals)); if (amount.isZero()) return '0'; - - // If amount is less than min value - if (amount.lt(DEFAULT_MIN_ROUNDED_VALUE)) { - if (roundDownIfSmall) return '0'; - return amount.toString(10); - } - - const displayDecimals = amount.gte(10000) ? 2 : DEFAULT_DISPLAY_DECIMALS; - return amount.toFixed(displayDecimals).toString(); + displayDecimals ??= amount.gte(10000) ? 2 : DEFAULT_DISPLAY_DECIMALS; + return amount.toFixed(displayDecimals, BigNumber.ROUND_FLOOR); } +/** + * Convert the given value to Wei value + * @param value The value to convert. + * @returns Converted value in string type. + */ export function toWei( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, -): BigNumber { - if (!value) return new BigNumber(0); +): string { + if (!value) return BigNumber(0).toString(); // First convert to a BigNumber, and then call `toString` with the // explicit radix 10 such that the result is formatted as a base-10 string // and not in scientific notation. - const valueBN = new BigNumber(value); + const valueBN = BigNumber(value); const valueString = valueBN.toString(10).trim(); const components = valueString.split('.'); if (components.length === 1) { - return new BigNumber(parseUnits(valueString, decimals).toString()); + return parseUnits(valueString, decimals).toString(); } else if (components.length === 2) { const trimmedFraction = components[1].substring(0, decimals); - return new BigNumber( - parseUnits(`${components[0]}.${trimmedFraction}`, decimals).toString(), - ); + return parseUnits( + `${components[0]}.${trimmedFraction}`, + decimals, + ).toString(); } else { throw new Error(`Cannot convert ${valueString} to wei`); } } +/** + * Try to parse the given value into BigNumber.js BigNumber + * @param value The value to parse. + * @returns Parsed value in BigNumber.js BigNumber type. + */ export function tryParseAmount( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, ): BigNumber | null { try { if (!value) return null; - const parsed = new BigNumber(value); + const parsed = BigNumber(value); if (!parsed || parsed.isNaN() || !parsed.isFinite()) return null; else return parsed; } catch (error) { @@ -79,15 +86,20 @@ export function tryParseAmount( } } -// Checks if an amount is equal of nearly equal to balance within a small margin of error -// Necessary because amounts in the UI are often rounded +/** + * Checks if an amount is equal of nearly equal to balance within a small margin of error + * Necessary because amounts in the UI are often rounded + * @param amount1 The amount to compare. + * @param amount2 The amount to compare. + * @returns true/false. + */ export function eqAmountApproximate( - amountInWei1: BigNumber, - amountInWei2: NumberT, -) { - const minValueWei = toWei(DEFAULT_MIN_ROUNDED_VALUE); - // Is difference btwn amount and balance less than min amount shown for token - return amountInWei1.minus(amountInWei2).abs().lt(minValueWei); + amount1: BigNumber.Value, + amount2: BigNumber.Value, + maxDifference: BigNumber.Value, +): boolean { + // Is difference btwn amounts less than maxDifference + return BigNumber(amount1).minus(amount2).abs().lte(maxDifference); } /** @@ -96,50 +108,26 @@ export function eqAmountApproximate( * @param fromDecimals The number of decimals `value` has. * @param toDecimals The number of decimals to convert `value` to. * @param value The value to convert. - * @returns `value` represented with `toDecimals` decimals. + * @returns `value` represented with `toDecimals` decimals in string type. */ export function convertDecimals( fromDecimals: number, toDecimals: number, - value: NumberT, -) { - const amount = new BigNumber(value); + value: BigNumber.Value, +): string { + const amount = BigNumber(value); - if (fromDecimals === toDecimals) return amount; + if (fromDecimals === toDecimals) return amount.toString(10); else if (fromDecimals > toDecimals) { const difference = fromDecimals - toDecimals; return amount - .div(new BigNumber(10).pow(difference)) - .integerValue(BigNumber.ROUND_FLOOR); - } - // fromDecimals < toDecimals - else { - const difference = toDecimals - fromDecimals; - return amount.times(new BigNumber(10).pow(difference)); - } -} - -/** - * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. - * Incurs a loss of precision when `fromDecimals` > `toDecimals`. - * @param fromDecimals The number of decimals `value` has. - * @param toDecimals The number of decimals to convert `value` to. - * @param value The value to convert. - * @returns `value` represented with `toDecimals` decimals. - */ -export function convertDecimalsEthersBigNumber( - fromDecimals: number, - toDecimals: number, - value: ethers.BigNumber, -) { - if (fromDecimals === toDecimals) return value; - else if (fromDecimals > toDecimals) { - const difference = fromDecimals - toDecimals; - return value.div(ethers.BigNumber.from('10').pow(difference)); + .div(BigNumber(10).pow(difference)) + .integerValue(BigNumber.ROUND_FLOOR) + .toString(10); } // fromDecimals < toDecimals else { const difference = toDecimals - fromDecimals; - return value.mul(ethers.BigNumber.from('10').pow(difference)); + return amount.times(BigNumber(10).pow(difference)).toString(10); } } diff --git a/typescript/utils/src/big-numbers.test.ts b/typescript/utils/src/big-numbers.test.ts index f3358098cb..82efee036d 100644 --- a/typescript/utils/src/big-numbers.test.ts +++ b/typescript/utils/src/big-numbers.test.ts @@ -1,49 +1,118 @@ +import BigNumber from 'bignumber.js'; import { expect } from 'chai'; -import { BigNumber, FixedNumber } from 'ethers'; +import { FixedNumber } from 'ethers'; -import { bigToFixed, fixedToBig, mulBigAndFixed } from './big-numbers'; +import { + BigNumberMax, + BigNumberMin, + bigToFixed, + fixedToBig, + isBigNumberish, + isZeroish, + mulBigAndFixed, +} from './big-numbers'; -describe('utils', () => { - describe('bigToFixed', () => { - it('converts a BigNumber to a FixedNumber', () => { - const big = BigNumber.from('1234'); - const fixed = bigToFixed(big); +describe('isBigNumberish', () => { + const testCases = [ + { expect: false, context: 'invalid number', case: 'invalidNumber' }, + { expect: false, context: 'NaN', case: NaN }, + { expect: false, context: 'undefined', case: undefined }, + { expect: false, context: 'null', case: null }, + { expect: true, context: 'decimal', case: 123.123 }, + { expect: true, context: 'integer', case: 300_000 }, + { expect: true, context: 'hex 0', case: 0x00 }, + { expect: true, context: 'hex 0', case: 0x000 }, + { + expect: true, + context: 'address 0', + case: 0x0000000000000000000000000000000000000000, + }, + ]; + testCases.forEach((tc) => { + it(`returns ${tc.expect} for ${tc.case}`, () => { + expect(isBigNumberish(tc.case!)).to.equal(tc.expect); + }); + }); +}); - expect(fixed.toUnsafeFloat()).to.equal(1234); +describe('isZeroish', () => { + const testCases = [ + { expect: false, context: 'invalid number', case: 'invalidNumber' }, + { expect: false, context: 'NaN', case: NaN }, + { expect: false, context: 'undefined', case: undefined }, + { expect: false, context: 'null', case: null }, + { expect: false, context: 'non 0 decimal', case: 123.123 }, + { expect: false, context: 'non 0 integer', case: 123 }, + { expect: true, context: 'hex 0', case: 0x00 }, + { expect: true, context: 'hex 0', case: 0x000 }, + { + expect: true, + context: 'address 0', + case: 0x0000000000000000000000000000000000000000, + }, + ]; + testCases.forEach((tc) => { + it(`returns ${tc.expect} for ${tc.case}`, () => { + expect(isZeroish(tc.case!)).to.equal(tc.expect); }); }); +}); - describe('fixedToBig', () => { - it('converts a FixedNumber to a floored BigNumber', () => { - const fixed = FixedNumber.from('12.34'); - const big = fixedToBig(fixed); +describe('bigToFixed', () => { + it('converts a BigNumber to a FixedNumber', () => { + const big = BigNumber('7.5e-10'); + const fixed = bigToFixed(big); - expect(big.toNumber()).to.equal(12); - }); + expect(fixed.toUnsafeFloat()).to.equal(7.5e-10); + }); +}); - it('converts a FixedNumber to a ceilinged BigNumber', () => { - const fixed = FixedNumber.from('12.34'); - const big = fixedToBig(fixed, true); +describe('fixedToBig', () => { + it('converts a FixedNumber to a floored BigNumber', () => { + const fixed = FixedNumber.from('12.34'); + const big = fixedToBig(fixed); - expect(big.toNumber()).to.equal(13); - }); + expect(big.toNumber()).to.equal(12); }); - describe('mulBigAndFixed', () => { - it('gets the floored product of a BigNumber and FixedNumber', () => { - const big = BigNumber.from('1000'); - const fixed = FixedNumber.from('1.2345'); - const product = mulBigAndFixed(big, fixed); + it('converts a FixedNumber to a ceilinged BigNumber', () => { + const fixed = FixedNumber.from('12.34'); + const big = fixedToBig(fixed, true); - expect(product.toNumber()).to.equal(1234); - }); + expect(big.toNumber()).to.equal(13); + }); +}); - it('gets the ceilinged product of a BigNumber and FixedNumber', () => { - const big = BigNumber.from('1000'); - const fixed = FixedNumber.from('1.2345'); - const product = mulBigAndFixed(big, fixed, true); +describe('mulBigAndFixed', () => { + it('gets the floored product of a BigNumber and FixedNumber', () => { + const big = BigNumber('1000'); + const fixed = FixedNumber.from('1.2345'); + const product = mulBigAndFixed(big, fixed); - expect(product.toNumber()).to.equal(1235); - }); + expect(product).to.equal((1234).toString()); + }); + + it('gets the ceilinged product of a BigNumber and FixedNumber', () => { + const big = BigNumber('1000'); + const fixed = FixedNumber.from('1.2345'); + const product = mulBigAndFixed(big, fixed, true); + + expect(product).to.equal((1235).toString()); + }); +}); + +describe('BigNumberMin', () => { + it('gets the min between the two BigNumber', () => { + const big = BigNumber('1000'); + const bigger = BigNumber('10000'); + expect(BigNumberMin(big, bigger)).to.equal(big.toString()); + }); +}); + +describe('BigNumberMax', () => { + it('gets the max between the two BigNumber', () => { + const big = BigNumber('1000'); + const bigger = BigNumber('10000'); + expect(BigNumberMax(big, bigger)).to.equal(bigger.toString()); }); }); diff --git a/typescript/utils/src/big-numbers.ts b/typescript/utils/src/big-numbers.ts index 4e8b6dd733..3a85e289bf 100644 --- a/typescript/utils/src/big-numbers.ts +++ b/typescript/utils/src/big-numbers.ts @@ -1,26 +1,33 @@ -import { BigNumber, BigNumberish, FixedNumber, constants } from 'ethers'; +import BigNumber from 'bignumber.js'; +import { FixedNumber } from 'ethers'; -import { isNullish } from './typeof'; +// Use toString(10) on bignumber.js to prevent ethers.js bigNumber error +// when parsing exponential string over e21 -export function isBigNumberish(value: any): value is BigNumberish { +/** + * Check if a value is bigNumberish (e.g. valid numbers, bigNumber). + * @param value The value to check. + * @returns true/false. + */ +export function isBigNumberish( + value: BigNumber.Value | undefined | null, +): boolean { try { - if (isNullish(value)) return false; - return BigNumber.from(value)._isBigNumber; + const val = BigNumber(value!); + return !val.isNaN() && val.isFinite() && BigNumber.isBigNumber(val); } catch (error) { return false; } } -// If a value (e.g. hex string or number) is zeroish (0, 0x0, 0x00, etc.) -export function isZeroish(value: BigNumberish) { +/** + * Check if a value (e.g. hex string or number) is zeroish (0, 0x0, 0x00, etc.). + * @param value The value to check. + * @returns true/false. + */ +export function isZeroish(value: BigNumber.Value): boolean { try { - if ( - !value || - value === constants.HashZero || - value === constants.AddressZero - ) - return true; - return BigNumber.from(value).isZero(); + return BigNumber(value).isZero(); } catch (error) { return false; } @@ -31,8 +38,8 @@ export function isZeroish(value: BigNumberish) { * @param big The BigNumber to convert. * @returns A FixedNumber representation of a BigNumber. */ -export function bigToFixed(big: BigNumber): FixedNumber { - return FixedNumber.from(big.toString()); +export function bigToFixed(big: BigNumber.Value): FixedNumber { + return FixedNumber.from(big.toString(10)); } /** @@ -43,7 +50,7 @@ export function bigToFixed(big: BigNumber): FixedNumber { */ export function fixedToBig(fixed: FixedNumber, ceil = false): BigNumber { const fixedAsInteger = ceil ? fixed.ceiling() : fixed.floor(); - return BigNumber.from(fixedAsInteger.toFormat('fixed256x0').toString()); + return BigNumber(fixedAsInteger.toFormat('fixed256x0').toString()); } /** @@ -51,21 +58,40 @@ export function fixedToBig(fixed: FixedNumber, ceil = false): BigNumber { * @param big The BigNumber to multiply. * @param fixed The FixedNumber to multiply. * @param ceil If true, the ceiling of the product is used. Otherwise, the floor is used. - * @returns The BigNumber product. + * @returns The BigNumber product in string type. */ export function mulBigAndFixed( - big: BigNumber, + big: BigNumber.Value, fixed: FixedNumber, ceil = false, -): BigNumber { +): string { // Converts big to a FixedNumber, multiplies it by fixed, and converts the product back // to a BigNumber. - return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil); + return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil).toString(10); } -export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) { - return bn1.gte(bn2) ? bn2 : bn1; +/** + * Return the smaller in the given two BigNumbers. + * @param bn1 The BigNumber to compare. + * @param bn2 The BigNumber to compare. + * @returns The smaller BigNumber in string type. + */ +export function BigNumberMin( + bn1: BigNumber.Value, + bn2: BigNumber.Value, +): string { + return BigNumber(bn1).gte(bn2) ? bn2.toString(10) : bn1.toString(10); } -export function BigNumberMax(bn1: BigNumber, bn2: BigNumber) { - return bn1.lte(bn2) ? bn2 : bn1; + +/** + * Return the bigger in the given two BigNumbers. + * @param bn1 The BigNumber to compare. + * @param bn2 The BigNumber to compare. + * @returns The bigger BigNumber in string type. + */ +export function BigNumberMax( + bn1: BigNumber.Value, + bn2: BigNumber.Value, +): string { + return BigNumber(bn1).lte(bn2) ? bn2.toString(10) : bn1.toString(10); } diff --git a/typescript/utils/src/multisig.ts b/typescript/utils/src/multisig.ts index 0f0f005657..334ef9ad9d 100644 --- a/typescript/utils/src/multisig.ts +++ b/typescript/utils/src/multisig.ts @@ -1,4 +1,5 @@ -import { BigNumber, utils } from 'ethers'; +import BigNumber from 'bignumber.js'; +import { utils } from 'ethers'; import { addressToBytes32 } from './addresses'; import { ParsedLegacyMultisigIsmMetadata } from './types'; @@ -18,7 +19,7 @@ export const parseLegacyMultisigIsmMetadata = ( const checkpointRoot = utils.hexlify( buf.slice(MERKLE_ROOT_OFFSET, MERKLE_INDEX_OFFSET), ); - const checkpointIndex = BigNumber.from( + const checkpointIndex = BigNumber( utils.hexlify(buf.slice(MERKLE_INDEX_OFFSET, ORIGIN_MAILBOX_OFFSET)), ).toNumber(); const originMailbox = utils.hexlify( @@ -30,7 +31,7 @@ export const parseLegacyMultisigIsmMetadata = ( ); }; const proof = parseBytesArray(MERKLE_PROOF_OFFSET, 32, 32); - const threshold = BigNumber.from( + const threshold = BigNumber( utils.hexlify(buf.slice(THRESHOLD_OFFSET, SIGNATURES_OFFSET)), ).toNumber(); const signatures = parseBytesArray( diff --git a/yarn.lock b/yarn.lock index 2d7e692f9a..114076b1c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2546,21 +2546,30 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.23.0": - version: 7.23.3 - resolution: "@babel/parser@npm:7.23.3" +"@babel/parser@npm:^7.22.15": + version: 7.22.16 + resolution: "@babel/parser@npm:7.22.16" bin: parser: ./bin/babel-parser.js - checksum: 284c22ec1d939df66fb94929959d2160c30df1ba5778f212668dfb2f4aa8ac176f628c6073a2c9ea7ab2a1701d2ebdafb0dfb173dc737db9dc6708d5d2f49e0a + checksum: 220df7dc0dbe8bc73540e66123f9c45ae3e5db40738fc1e97579205364240bed3e9724fc737c0828f9d46c96ce9b23728314f598e5bf8a62566ccef539d15bdf languageName: node linkType: hard -"@babel/parser@npm:^7.22.15": - version: 7.22.16 - resolution: "@babel/parser@npm:7.22.16" +"@babel/parser@npm:^7.22.7": + version: 7.22.7 + resolution: "@babel/parser@npm:7.22.7" bin: parser: ./bin/babel-parser.js - checksum: 220df7dc0dbe8bc73540e66123f9c45ae3e5db40738fc1e97579205364240bed3e9724fc737c0828f9d46c96ce9b23728314f598e5bf8a62566ccef539d15bdf + checksum: f420f89ea8e5803a44f76a57630002ca5721fbde719c10ac4eaebf1d01fad102447cd90a7721c97b1176bde33ec9bc2b68fe8c7d541668dc6610727ba79c8862 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.23.0": + version: 7.23.3 + resolution: "@babel/parser@npm:7.23.3" + bin: + parser: ./bin/babel-parser.js + checksum: 284c22ec1d939df66fb94929959d2160c30df1ba5778f212668dfb2f4aa8ac176f628c6073a2c9ea7ab2a1701d2ebdafb0dfb173dc737db9dc6708d5d2f49e0a languageName: node linkType: hard @@ -3978,7 +3987,7 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/cli@workspace:typescript/cli" dependencies: - "@hyperlane-xyz/sdk": "npm:3.1.2" + "@hyperlane-xyz/sdk": "npm:3.1.3" "@inquirer/prompts": "npm:^3.0.0" "@types/node": "npm:^18.14.5" "@types/yargs": "npm:^17.0.24" @@ -3999,12 +4008,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:3.1.2, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:3.1.3, @hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.1.2" + "@hyperlane-xyz/utils": "npm:3.1.3" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts": "npm:^4.9.3" @@ -4031,12 +4040,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:3.1.2, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:3.1.3, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: - "@hyperlane-xyz/core": "npm:3.1.2" - "@hyperlane-xyz/sdk": "npm:3.1.2" + "@hyperlane-xyz/core": "npm:3.1.3" + "@hyperlane-xyz/sdk": "npm:3.1.3" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -4081,9 +4090,9 @@ __metadata: "@ethersproject/experimental": "npm:^5.7.0" "@ethersproject/hardware-wallets": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.2" - "@hyperlane-xyz/helloworld": "npm:3.1.2" - "@hyperlane-xyz/sdk": "npm:3.1.2" - "@hyperlane-xyz/utils": "npm:3.1.2" + "@hyperlane-xyz/helloworld": "npm:3.1.3" + "@hyperlane-xyz/sdk": "npm:3.1.3" + "@hyperlane-xyz/utils": "npm:3.1.3" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -4095,7 +4104,7 @@ __metadata: "@types/node": "npm:^16.9.1" "@types/prompts": "npm:^2.0.14" "@types/sinon-chai": "npm:^3.2.12" - "@types/yargs": "npm:^17.0.10" + "@types/yargs": "npm:^17.0.24" asn1.js: "npm:5.4.1" aws-kms-ethers-signer: "npm:^0.1.3" chai: "npm:^4.3.4" @@ -4130,14 +4139,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/sdk@npm:3.1.2, @hyperlane-xyz/sdk@workspace:typescript/sdk": +"@hyperlane-xyz/sdk@npm:3.1.3, @hyperlane-xyz/sdk@workspace:typescript/sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" dependencies: "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.1.2" - "@hyperlane-xyz/utils": "npm:3.1.2" + "@hyperlane-xyz/core": "npm:3.1.3" + "@hyperlane-xyz/utils": "npm:3.1.3" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@solana/spl-token": "npm:^0.3.8" @@ -4173,7 +4182,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:3.1.2, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:3.1.3, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: @@ -4182,6 +4191,7 @@ __metadata: bignumber.js: "npm:^9.1.1" chai: "npm:^4.3.0" ethers: "npm:^5.7.2" + mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" typescript: "npm:5.1.6" languageName: unknown @@ -6156,15 +6166,6 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.10": - version: 17.0.31 - resolution: "@types/yargs@npm:17.0.31" - dependencies: - "@types/yargs-parser": "npm:*" - checksum: 9b180fc289cc7342a584e9e552dd667d60e6e0a436be67ed8feee681cb22de1ebffacd6a1e29c5905049850c10bb1c9db45d957cfd01a97f58b33e290b233b1f - languageName: node - linkType: hard - "@types/yargs@npm:^17.0.24": version: 17.0.24 resolution: "@types/yargs@npm:17.0.24"