Upgrade to hyp packages 1.5.1

Remove duplicated utilities
Upgrade some tooling including typescript and next
Fix error from missing background image
pull/52/head
J M Rossy 1 year ago
parent 131e38da65
commit 79a3101bd7
  1. 43
      package.json
  2. 6
      src/components/buttons/RadioButtons.tsx
  3. 4
      src/components/icons/ChainToChain.tsx
  4. 38
      src/components/icons/Identicon.tsx
  5. 4
      src/components/layout/AppLayout.tsx
  6. 2
      src/components/nav/Footer.tsx
  7. 2
      src/components/search/SearchFilterBar.tsx
  8. 11
      src/features/chains/ConfigureChains.tsx
  9. 20
      src/features/chains/chainConfig.ts
  10. 6
      src/features/chains/useChainConfigs.ts
  11. 34
      src/features/chains/utils.ts
  12. 24
      src/features/debugger/debugMessage.ts
  13. 7
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  14. 3
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  15. 3
      src/features/messages/MessageDetails.tsx
  16. 2
      src/features/messages/MessageTable.tsx
  17. 4
      src/features/messages/cards/ContentDetailsCard.tsx
  18. 33
      src/features/messages/cards/GasDetailsCard.tsx
  19. 3
      src/features/messages/cards/IsmDetailsCard.tsx
  20. 3
      src/features/messages/cards/KeyValueRow.tsx
  21. 4
      src/features/messages/ica.ts
  22. 3
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  23. 45
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  24. 6
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  25. 6
      src/features/messages/queries/encoding.ts
  26. 7
      src/features/messages/queries/useMessageQuery.ts
  27. 3
      src/features/messages/utils.ts
  28. 4
      src/features/providers/HyperlaneEtherscanProvider.ts
  29. 5
      src/features/providers/HyperlaneJsonRpcProvider.ts
  30. 4
      src/features/providers/SmartMultiProvider.ts
  31. 16
      src/features/providers/SmartProvider.test.ts
  32. 30
      src/features/providers/SmartProvider.ts
  33. 6
      src/features/providers/types.ts
  34. 14
      src/styles/global.css
  35. 69
      src/utils/addresses.ts
  36. 78
      src/utils/amount.ts
  37. 22
      src/utils/base64.ts
  38. 9
      src/utils/big-number.ts
  39. 16
      src/utils/errors.ts
  40. 3
      src/utils/explorers.ts
  41. 22
      src/utils/number.ts
  42. 30
      src/utils/objects.ts
  43. 20
      src/utils/retry.ts
  44. 31
      src/utils/string.ts
  45. 34
      src/utils/timeout.ts
  46. 4
      src/utils/typeof.ts
  47. 4482
      yarn.lock

@ -1,48 +1,49 @@
{
"name": "@hyperlane-xyz/explorer",
"description": "An interchain explorer for the Hyperlane protocol and network.",
"version": "1.0.0",
"version": "1.5.1",
"author": "J M Rossy",
"dependencies": {
"@headlessui/react": "^1.7.11",
"@hyperlane-xyz/sdk": "1.3.4",
"@hyperlane-xyz/widgets": "1.3.4",
"@headlessui/react": "^1.7.17",
"@hyperlane-xyz/sdk": "1.5.1",
"@hyperlane-xyz/utils": "1.5.1",
"@hyperlane-xyz/widgets": "1.5.0",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "^0.11.0",
"@rainbow-me/rainbowkit": "0.12.16",
"@tanstack/react-query": "^4.24.10",
"bignumber.js": "^9.1.1",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"graphql": "^16.6.0",
"next": "^13.2.0",
"nextjs-cors": "latest",
"next": "^13.4.19",
"nextjs-cors": "^2.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^9.1.1",
"urql": "^3.0.3",
"wagmi": "^0.11.6",
"wagmi": "0.12.18",
"zod": "^3.21.2",
"zustand": "^4.3.8"
"zustand": "4.3.8"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/jest": "^29.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/jest": "^29.5.3",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"eslint-config-next": "^13.2.0",
"eslint-config-prettier": "^8.6.0",
"jest": "^29.4.3",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"autoprefixer": "^10.4.15",
"eslint": "^8.41.0",
"eslint-config-next": "^13.4.19",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.6.3",
"postcss": "^8.4.21",
"prettier": "^2.8.4",
"tailwindcss": "^3.2.7",
"tailwindcss": "^3.3.3",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.1.6"
},
"homepage": "https://www.hyperlane.xyz",
"license": "Apache-2.0",

@ -1,9 +1,9 @@
import { RadioGroup } from '@headlessui/react';
interface Props {
options: Array<{ value: string; display: string }>;
selected: string;
onChange: (value: string) => void;
options: Array<{ value: string | number; display: string }>;
selected: string | number;
onChange: (value: string | number) => void;
label?: string;
}

@ -6,7 +6,7 @@ import { useIsMobile } from '../../styles/mediaQueries';
import { ChainLogo } from './ChainLogo';
function _ChainToChain({
function ChainToChain_({
originChainId,
destinationChainId,
size = 32,
@ -36,4 +36,4 @@ function _ChainToChain({
);
}
export const ChainToChain = memo(_ChainToChain);
export const ChainToChain = memo(ChainToChain_);

@ -1,38 +0,0 @@
import jazzicon from '@metamask/jazzicon';
import { CSSProperties, memo } from 'react';
import { isValidAddress, normalizeAddress } from '../../utils/addresses';
type Props = {
address: string;
size?: number;
styles?: CSSProperties;
};
// This should match metamask: https://github.com/MetaMask/metamask-extension/blob/master/ui/helpers/utils/icon-factory.js#L84
function addressToSeed(address: string) {
const addrStub = normalizeAddress(address).slice(2, 10);
return parseInt(addrStub, 16);
}
function _Identicon({ address, size: _size, styles }: Props) {
const size = _size ?? 34;
if (!isValidAddress(address)) return null;
const jazziconResult = jazzicon(size, addressToSeed(address));
return (
<div
style={{ height: size, ...styles }}
ref={(nodeElement) => {
if (nodeElement) {
nodeElement.innerHTML = '';
nodeElement.appendChild(jazziconResult);
}
}}
></div>
);
}
export const Identicon = memo(_Identicon);

@ -1,7 +1,8 @@
import Head from 'next/head';
import { PropsWithChildren } from 'react';
import { toTitleCase } from '../../utils/string';
import { toTitleCase } from '@hyperlane-xyz/utils';
import { Footer } from '../nav/Footer';
import { Header } from '../nav/Header';
@ -19,7 +20,6 @@ export function AppLayout({ pathName, children }: PropsWithChildren<Props>) {
</Head>
<div
style={styles.container}
id="app-content"
className="relative w-full min-w-screen h-full min-h-screen flex flex-col justify-between bg-blue-500"
>
{/* <InfoBanner /> */}

@ -39,7 +39,7 @@ export function Footer() {
<div className="relative">
<Image className="relative z-0 w-full" src={FooterBg} alt="background" />
<Image
className="absolute z-10 bottom-[1.5rem] w-full h-auto"
className="absolute z-10 bottom-[1.6rem] w-full h-auto"
src={FooterTopBorder}
alt="border"
/>

@ -3,12 +3,12 @@ import Link from 'next/link';
import { useState } from 'react';
import { ChainMetadata, mainnetChainsMetadata, testnetChainsMetadata } from '@hyperlane-xyz/sdk';
import { arrayToObject } from '@hyperlane-xyz/utils';
import { getChainDisplayName } from '../../features/chains/utils';
import { useMultiProvider } from '../../features/providers/multiProvider';
import GearIcon from '../../images/icons/gear.svg';
import { Color } from '../../styles/Color';
import { arrayToObject } from '../../utils/objects';
import { SolidButton } from '../buttons/SolidButton';
import { TextButton } from '../buttons/TextButton';
import { ChainLogo } from '../icons/ChainLogo';

@ -120,7 +120,7 @@ export function ConfigureChains() {
<td className={styles.value}>{chain.domainId || chain.chainId}</td>
<td className={styles.value}>{chain.displayName || chain.name}</td>
<td className={styles.value + ' hidden sm:table-cell'}>
{chain.publicRpcUrls?.[0]?.http || 'Unknown'}
{chain.rpcUrls?.[0]?.http || 'Unknown'}
</td>
<td className={styles.value + ' hidden md:table-cell'}>
{chain.blockExplorers?.[0]?.url || 'Unknown'}
@ -184,7 +184,8 @@ export function ConfigureChains() {
const customChainTextareaPlaceholder = `{
"chainId": 5,
"name": "goerli",
"publicRpcUrls": [{ "http": "https://foobar.com" }],
"protocol": "ethereum",
"rpcUrls": [{ "http": "https://foobar.com" }],
"blockExplorers": [ {
"name": "GoerliScan",
"family": "etherscan",
@ -193,10 +194,8 @@ const customChainTextareaPlaceholder = `{
"apiKey": "12345"
} ],
"blocks": { "confirmations": 1, "estimateBlockTime": 13 },
"contracts": {
"mailbox": "0x123...",
"interchainGasPaymaster": "0x123..."
}
"mailbox": "0x123...",
"interchainGasPaymaster": "0x123..."
}
`;

@ -1,19 +1,15 @@
import { z } from 'zod';
import { ChainMetadata, ChainMetadataSchema, MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMetadataSchema, MultiProvider } from '@hyperlane-xyz/sdk';
import { logger } from '../../utils/logger';
export const chainContractsSchema = z.object({
mailbox: z.string(),
export const ChainConfigSchema = ChainMetadataSchema.extend({
mailbox: z.string().optional(),
interchainGasPaymaster: z.string().optional(),
// interchainAccountRouter: z.string().optional(),
});
export type ChainConfig = ChainMetadata & { contracts: z.infer<typeof chainContractsSchema> };
export const chainConfigSchema = ChainMetadataSchema.extend({
contracts: chainContractsSchema,
});
export type ChainConfig = z.infer<typeof ChainConfigSchema>;
type ParseResult =
| {
@ -37,7 +33,7 @@ export function tryParseChainConfig(input: string, mp?: MultiProvider): ParseRes
};
}
const result = chainConfigSchema.safeParse(data);
const result = ChainConfigSchema.safeParse(data);
if (!result.success) {
logger.error('Error validating chain config', result.error);
@ -50,8 +46,8 @@ export function tryParseChainConfig(input: string, mp?: MultiProvider): ParseRes
const chainConfig = result.data as ChainConfig;
// Ensure https is used for RPCs (and not http)
const rpcUrls = chainConfig.publicRpcUrls;
// Ensure http is used for RPCs (and not http)
const rpcUrls = chainConfig.rpcUrls;
if (rpcUrls?.some((r) => !r.http.startsWith('https://'))) {
return {
success: false,
@ -84,7 +80,7 @@ export function tryParseChainConfig(input: string, mp?: MultiProvider): ParseRes
) {
return {
success: false,
error: 'chainId, name, or domainId is already in use',
error: 'chainId, domainId, or name is already in use',
};
}

@ -1,10 +1,10 @@
import { useEffect } from 'react';
import { z } from 'zod';
import { ChainMap, ChainMetadata, ChainMetadataSchema, objMerge } from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk';
import { fromBase64, objMerge } from '@hyperlane-xyz/utils';
import { useStore } from '../../store';
import { fromBase64 } from '../../utils/base64';
import { logger } from '../../utils/logger';
import { useQueryParam } from '../../utils/queryParams';
@ -54,7 +54,7 @@ export function useQueryParamChainConfigSync() {
(acc, chainMetadata) => {
// TODO would be great if we could get contract addrs here too
// But would require apps like warp template to get that from devs
acc[chainMetadata.name] = { ...chainMetadata, contracts: { mailbox: '' } };
acc[chainMetadata.name] = chainMetadata;
return acc;
},
{},

@ -1,15 +1,7 @@
import {
type ChainMap,
type ChainName,
type MultiProvider,
chainIdToMetadata,
hyperlaneContractAddresses,
} from '@hyperlane-xyz/sdk';
import { type MultiProvider, chainIdToMetadata } from '@hyperlane-xyz/sdk';
import { toTitleCase } from '@hyperlane-xyz/utils';
import { Environment } from '../../consts/environments';
import { toTitleCase } from '../../utils/string';
import type { ChainConfig } from './chainConfig';
export function getChainName(mp: MultiProvider, chainId?: number) {
return mp.tryGetChainName(chainId || 0) || undefined;
@ -32,28 +24,6 @@ export function getChainEnvironment(mp: MultiProvider, chainIdOrName: number | s
return isTestnet ? Environment.Testnet : Environment.Mainnet;
}
export function tryGetContractAddress(
customChainConfigs: ChainMap<ChainConfig>,
chainName: ChainName,
contractName: keyof ChainConfig['contracts'],
): Address | undefined {
return (
customChainConfigs[chainName]?.contracts?.[contractName] ||
hyperlaneContractAddresses[chainName]?.[contractName] ||
undefined
);
}
export function getContractAddress(
customChainConfigs: ChainMap<ChainConfig>,
chainName: ChainName,
contractName: keyof ChainConfig['contracts'],
): Address {
const addr = tryGetContractAddress(customChainConfigs, chainName, contractName);
if (!addr) throw new Error(`No contract address found for ${contractName} on ${chainName}`);
return addr;
}
export function isPiChain(chainId: number) {
return !chainIdToMetadata[chainId];
}

@ -10,16 +10,19 @@ import {
InterchainGasPaymaster__factory,
} from '@hyperlane-xyz/core';
import type { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import {
addressToBytes32,
errorToString,
formatMessage,
isValidAddress,
strip0x,
trimToLength,
} from '@hyperlane-xyz/utils';
import { MAILBOX_VERSION } from '../../consts/environments';
import { Message } from '../../types';
import { isValidAddress, trimLeading0x } from '../../utils/addresses';
import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { trimToLength } from '../../utils/string';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { isIcaMessage, tryDecodeIcaBody, tryFetchIcaAddress } from '../messages/ica';
import { GasPayment, IsmModuleTypes, MessageDebugResult, MessageDebugStatus } from './types';
@ -45,7 +48,7 @@ export async function debugMessage(
logger.debug(`Debugging message id: ${msgId}`);
// Prepare some useful data/encodings
const messageBytes = utils.formatMessage(
const messageBytes = formatMessage(
MAILBOX_VERSION,
nonce,
originDomain,
@ -57,7 +60,7 @@ export async function debugMessage(
const destName = multiProvider.tryGetChainName(destDomain)!;
const originProvider = multiProvider.getProvider(originDomain);
const destProvider = multiProvider.getProvider(destDomain);
const senderBytes = utils.addressToBytes32(sender);
const senderBytes = addressToBytes32(sender);
// Create a bag to hold all the useful info collected along the way
const details: Omit<MessageDebugResult, 'status' | 'description'> = {};
@ -65,7 +68,10 @@ export async function debugMessage(
const recipInvalid = await isInvalidRecipient(destProvider, recipient);
if (recipInvalid) return recipInvalid;
const destMailbox = getContractAddress(customChainConfigs, destName, 'mailbox');
const destMailbox = customChainConfigs[destName]?.mailbox;
if (!destMailbox)
throw new Error(`Cannot debug message, no mailbox address provided for chain ${destName}`);
const deliveryResult = await debugMessageDelivery(
originDomain,
destMailbox,
@ -318,7 +324,7 @@ async function tryCheckBytecodeHandle(provider: Provider, recipientAddress: stri
const msgRecipientInterface = IMessageRecipient__factory.createInterface();
const handleFunction = msgRecipientInterface.functions[HANDLE_FUNCTION_SIG];
const handleSignature = msgRecipientInterface.getSighash(handleFunction);
return bytecode.includes(trimLeading0x(handleSignature));
return bytecode.includes(strip0x(handleSignature));
} catch (error) {
logger.error('Error checking bytecode for handle fn', error);
return true;

@ -7,7 +7,6 @@ import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { toDecimalNumber } from '../../utils/number';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { debugMessage } from '../debugger/debugMessage';
import { MessageDebugStatus } from '../debugger/types';
@ -24,7 +23,11 @@ export async function fetchDeliveryStatus(
message: Message,
): Promise<MessageDeliveryStatusResponse> {
const destName = multiProvider.getChainName(message.destinationChainId);
const destMailboxAddr = getContractAddress(customChainConfigs, destName, 'mailbox');
const destMailboxAddr = customChainConfigs[destName]?.mailbox;
if (!destMailboxAddr)
throw new Error(
`Cannot check delivery status, no mailbox address provided for chain ${destName}`,
);
const { isDelivered, blockNumber, transactionHash } = await checkIsMessageDelivered(
multiProvider,

@ -2,8 +2,9 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo } from 'react';
import { toast } from 'react-toastify';
import { errorToString } from '@hyperlane-xyz/utils';
import { Message, MessageStatus } from '../../types';
import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { MissingChainConfigToast } from '../chains/MissingChainConfigToast';
import { useChainConfigs } from '../chains/useChainConfigs';

@ -2,13 +2,14 @@ import Image from 'next/image';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils';
import { Spinner } from '../../components/animations/Spinner';
import { Card } from '../../components/layout/Card';
import CheckmarkIcon from '../../images/icons/checkmark-circle.svg';
import { useStore } from '../../store';
import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { toTitleCase, trimToLength } from '../../utils/string';
import { getChainDisplayName } from '../chains/utils';
import { useMessageDeliveryStatus } from '../deliveryStatus/useMessageDeliveryStatus';
import { useMultiProvider } from '../providers/multiProvider';

@ -2,10 +2,10 @@ import Link from 'next/link';
import { PropsWithChildren } from 'react';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { shortenAddress } from '@hyperlane-xyz/utils';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { MessageStatus, MessageStub } from '../../types';
import { shortenAddress } from '../../utils/addresses';
import { getHumanReadableDuration, getHumanReadableTimeString } from '../../utils/time';
import { getChainDisplayName } from '../chains/utils';
import { useMultiProvider } from '../providers/multiProvider';

@ -1,7 +1,7 @@
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react';
import { utils } from '@hyperlane-xyz/utils';
import { formatMessage } from '@hyperlane-xyz/utils';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { SelectField } from '../../../components/input/SelectField';
@ -51,7 +51,7 @@ export function ContentDetailsCard({
const rawBytes = useMemo(() => {
try {
return utils.formatMessage(
return formatMessage(
MAILBOX_VERSION,
nonce,
originDomainId,

@ -3,15 +3,16 @@ import { utils } from 'ethers';
import Image from 'next/image';
import { useMemo, useState } from 'react';
import { fromWei, toTitleCase } from '@hyperlane-xyz/utils';
import { RadioButtons } from '../../../components/buttons/RadioButtons';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import { links } from '../../../consts/links';
import FuelPump from '../../../images/icons/fuel-pump.svg';
import { Message } from '../../../types';
import { BigNumberMax, fromWei } from '../../../utils/amount';
import { BigNumberMax } from '../../../utils/big-number';
import { logger } from '../../../utils/logger';
import { toTitleCase } from '../../../utils/string';
import { GasPayment } from '../../debugger/types';
import { useMultiProvider } from '../../providers/multiProvider';
@ -29,13 +30,13 @@ export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
const originMetadata = multiProvider.tryGetChainMetadata(message.originChainId);
const nativeCurrencyName = originMetadata?.nativeToken?.symbol || 'Eth';
return [
{ value: 'ether', display: toTitleCase(nativeCurrencyName) },
{ value: 'gwei', display: 'Gwei' },
{ value: 'wei', display: 'Wei' },
{ value: 18, display: toTitleCase(nativeCurrencyName) },
{ value: 9, display: 'Gwei' },
{ value: 0, display: 'Wei' },
];
}, [message, multiProvider]);
const [unit, setUnit] = useState(unitOptions[0].value);
const [decimals, setDecimals] = useState(unitOptions[0].value);
const { totalGasAmount, paymentFormatted, numPayments, avgPrice, paymentsWithAddr } =
useMemo(() => {
@ -43,7 +44,7 @@ export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
.map((contract) =>
igpPayments[contract].map((p) => ({
gasAmount: p.gasAmount,
paymentAmount: fromWei(p.paymentAmount, unit).toString(),
paymentAmount: fromWei(p.paymentAmount, decimals).toString(),
contract,
})),
)
@ -63,10 +64,10 @@ export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
totalPaymentWei = BigNumberMax(totalPaymentWei, new BigNumber(message.totalPayment || 0));
numPayments = Math.max(numPayments, message.numPayments || 0);
const paymentFormatted = fromWei(totalPaymentWei.toString(), unit).toString();
const avgPrice = computeAvgGasPrice(unit, totalGasAmount, totalPaymentWei);
const paymentFormatted = fromWei(totalPaymentWei.toString(), decimals).toString();
const avgPrice = computeAvgGasPrice(decimals, totalGasAmount, totalPaymentWei);
return { totalGasAmount, paymentFormatted, numPayments, avgPrice, paymentsWithAddr };
}, [unit, message, igpPayments]);
}, [decimals, message, igpPayments]);
return (
<Card className="w-full space-y-4 relative">
@ -133,8 +134,8 @@ export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
<div className="absolute right-2 bottom-2">
<RadioButtons
options={unitOptions}
selected={unit}
onChange={(value) => setUnit(value)}
selected={decimals}
onChange={(value) => setDecimals(parseInt(value.toString(), 10))}
label="Gas unit"
/>
</div>
@ -165,14 +166,18 @@ function IgpPaymentsTable({ payments }: { payments: Array<GasPayment & { contrac
);
}
function computeAvgGasPrice(unit: string, gasAmount?: BigNumber.Value, payment?: BigNumber.Value) {
function computeAvgGasPrice(
decimals: number,
gasAmount?: BigNumber.Value,
payment?: BigNumber.Value,
) {
try {
if (!gasAmount || !payment) return null;
const gasBN = new BigNumber(gasAmount);
const paymentBN = new BigNumber(payment);
if (gasBN.isZero() || paymentBN.isZero()) return null;
const wei = paymentBN.div(gasAmount).toFixed(0);
const formatted = utils.formatUnits(wei, unit).toString();
const formatted = utils.formatUnits(wei, decimals).toString();
return { wei, formatted };
} catch (error) {
logger.debug('Error computing avg gas price', error);

@ -1,10 +1,11 @@
import Image from 'next/image';
import { isNullish } from '@hyperlane-xyz/utils';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import { links } from '../../../consts/links';
import ShieldLock from '../../../images/icons/shield-lock.svg';
import { isNullish } from '../../../utils/typeof';
import { IsmModuleTypes, MessageDebugResult } from '../../debugger/types';
import { KeyValueRow } from './KeyValueRow';

@ -1,5 +1,6 @@
import { isZeroish } from '@hyperlane-xyz/utils';
import { CopyButton } from '../../../components/buttons/CopyButton';
import { isZeroish } from '../../../utils/number';
interface Props {
label: string;

@ -4,8 +4,8 @@ import { useMemo } from 'react';
import { InterchainAccountRouter__factory } from '@hyperlane-xyz/core';
import { hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { eqAddress, isValidAddress } from '@hyperlane-xyz/utils';
import { areAddressesEqual, isValidAddress } from '../../utils/addresses';
import { logger } from '../../utils/logger';
import { useMultiProvider } from '../providers/multiProvider';
@ -32,7 +32,7 @@ export function isIcaMessage({ sender, recipient }: { sender: Address; recipient
function isAddressIcaRouter(addr: Address) {
try {
// TODO PI support
return ICA_ADDRESS && areAddressesEqual(addr, ICA_ADDRESS);
return ICA_ADDRESS && eqAddress(addr, ICA_ADDRESS);
} catch (error) {
logger.warn('Error checking if address is ICA router', error, addr);
return false;

@ -16,7 +16,8 @@ const goerliMailbox = hyperlaneEnvironments.testnet.goerli.mailbox;
const goerliIgp = hyperlaneEnvironments.testnet.goerli.interchainGasPaymaster;
const goerliConfigWithExplorer: ChainConfig = {
...chainMetadata.goerli,
contracts: { mailbox: goerliMailbox, interchainGasPaymaster: goerliIgp },
mailbox: goerliMailbox,
interchainGasPaymaster: goerliIgp,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { blockExplorers, ...goerliConfigNoExplorer } = goerliConfigWithExplorer;

@ -2,10 +2,17 @@ import { BigNumber, constants, ethers, providers } from 'ethers';
import { IInterchainGasPaymaster__factory, Mailbox__factory } from '@hyperlane-xyz/core';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import {
addressToBytes32,
bytes32ToAddress,
isValidAddress,
isValidTransactionHash,
messageId,
normalizeAddress,
parseMessage,
} from '@hyperlane-xyz/utils';
import { ExtendedLog, Message, MessageStatus } from '../../../types';
import { isValidAddress, isValidTransactionHash, normalizeAddress } from '../../../utils/addresses';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
@ -97,14 +104,14 @@ export async function fetchMessagesFromPiChain(
}
async function fetchLogsForAddress(
{ chainId, contracts }: ChainConfig,
{ chainId, mailbox }: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<ExtendedLog[]> {
const address = query.input;
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox;
const dispatchTopic = utils.addressToBytes32(address);
if (!mailbox) throw new Error(`No mailbox address found for chain ${chainId}}`);
const dispatchTopic = addressToBytes32(address);
return fetchLogsFromProvider(
[
@ -113,7 +120,7 @@ async function fetchLogsForAddress(
// [processTopic0, dispatchTopic],
// [processTopic0, null, null, dispatchTopic],
],
mailboxAddr,
mailbox,
chainId,
query,
multiProvider,
@ -150,17 +157,17 @@ async function fetchLogsForMsgId(
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<ExtendedLog[]> {
const { contracts, chainId } = chainConfig;
const { mailbox, chainId } = chainConfig;
const msgId = query.input;
logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox;
if (!mailbox) throw new Error(`No mailbox address found for chain ${chainId}}`);
const topic1 = msgId;
const logs: ExtendedLog[] = await fetchLogsFromProvider(
[
[dispatchIdTopic0, topic1],
// [processIdTopic0, topic1],
],
mailboxAddr,
mailbox,
chainId,
query,
multiProvider,
@ -246,10 +253,10 @@ function logToMessage(
try {
const bytes = logDesc.args['message'];
const message = utils.parseMessage(bytes);
const msgId = utils.messageId(bytes);
const sender = normalizeAddress(utils.bytes32ToAddress(message.sender));
const recipient = normalizeAddress(utils.bytes32ToAddress(message.recipient));
const message = parseMessage(bytes);
const msgId = messageId(bytes);
const sender = normalizeAddress(bytes32ToAddress(message.sender));
const recipient = normalizeAddress(bytes32ToAddress(message.recipient));
const originChainId = multiProvider.getChainId(message.origin);
const destinationChainId = multiProvider.getChainId(message.destination);
@ -272,7 +279,7 @@ function logToMessage(
to: log.to ? normalizeAddress(log.to) : constants.AddressZero,
blockHash: log.blockHash,
blockNumber: BigNumber.from(log.blockNumber).toNumber(),
mailbox: chainConfig.contracts.mailbox,
mailbox: chainConfig.mailbox || constants.AddressZero,
nonce: 0,
// TODO get more gas info from tx
gasLimit: 0,
@ -297,14 +304,16 @@ async function tryFetchIgpGasPayments(
chainConfig: ChainConfig,
multiProvider: MultiProvider,
): Promise<Message> {
const { chainId, contracts } = chainConfig;
const igpAddr = contracts.interchainGasPaymaster;
if (!igpAddr || !isValidAddress(igpAddr)) {
const { chainId, interchainGasPaymaster } = chainConfig;
if (!interchainGasPaymaster || !isValidAddress(interchainGasPaymaster)) {
logger.warn('No IGP address found for chain:', chainId);
return message;
}
const igp = IInterchainGasPaymaster__factory.connect(igpAddr, multiProvider.getProvider(chainId));
const igp = IInterchainGasPaymaster__factory.connect(
interchainGasPaymaster,
multiProvider.getProvider(chainId),
);
const filter = igp.filters.GasPayment(message.msgId);
const matchedEvents = (await igp.queryFilter(filter)) || [];
logger.debug(`Found ${matchedEvents.length} payments to IGP for msg ${message.msgId}`);

@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ensure0x } from '@hyperlane-xyz/utils';
import { Message } from '../../../types';
import { ensureLeading0x } from '../../../utils/addresses';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
import { useChainConfigs } from '../../chains/useChainConfigs';
@ -42,7 +42,7 @@ export function usePiChainMessageSearchQuery({
if (pause || !hasInput || !isValidInput || !Object.keys(chainConfigs).length) return [];
logger.debug('Starting PI Chain message search for:', sanitizedInput);
// TODO convert timestamps to from/to blocks here
const query = { input: ensureLeading0x(sanitizedInput) };
const query = { input: ensure0x(sanitizedInput) };
try {
const messages = await Promise.any(
Object.values(chainConfigs).map((c) => fetchMessagesOrThrow(c, query, multiProvider)),
@ -79,7 +79,7 @@ export function usePiChainMessageQuery({
async () => {
if (pause || !messageId || !Object.keys(chainConfigs).length) return [];
logger.debug('Starting PI Chain message query for:', messageId);
const query = { input: ensureLeading0x(messageId) };
const query = { input: ensure0x(messageId) };
try {
const messages = await Promise.any(
Object.values(chainConfigs).map((c) =>

@ -1,12 +1,12 @@
import { ensureLeading0x, trimLeading0x } from '../../../utils/addresses';
import { ensure0x, strip0x } from '@hyperlane-xyz/utils';
export function stringToPostgresBytea(hexString: string) {
const trimmed = trimLeading0x(hexString).toLowerCase();
const trimmed = strip0x(hexString).toLowerCase();
const prefix = `\\x`;
return `${prefix}${trimmed}`;
}
export function postgresByteaToString(byteString: string) {
if (!byteString || byteString.length < 4) throw new Error('Invalid byte string');
return ensureLeading0x(byteString.substring(2));
return ensure0x(byteString.substring(2));
}

@ -1,8 +1,9 @@
import { useCallback, useMemo } from 'react';
import { useQuery } from 'urql';
import { isAddressEvm, isValidTransactionHashEvm } from '@hyperlane-xyz/utils';
import { MessageStatus } from '../../../types';
import { isValidAddressFast, isValidTransactionHash } from '../../../utils/addresses';
import { useInterval } from '../../../utils/useInterval';
import { useMultiProvider } from '../../providers/multiProvider';
import {
@ -20,8 +21,8 @@ const SEARCH_QUERY_LIMIT = 50;
export function isValidSearchQuery(input: string, allowAddress?: boolean) {
if (!input) return false;
if (isValidTransactionHash(input)) return true;
if (allowAddress && isValidAddressFast(input)) return true;
if (isValidTransactionHashEvm(input)) return true;
if (allowAddress && isAddressEvm(input)) return true;
return false;
}

@ -1,5 +1,6 @@
import { fromBase64, toBase64 } from '@hyperlane-xyz/utils';
import { Message, MessageStub } from '../../types';
import { fromBase64, toBase64 } from '../../utils/base64';
export function serializeMessage(msg: MessageStub | Message): string | undefined {
return toBase64(msg);

@ -1,9 +1,9 @@
import { providers } from 'ethers';
import { ChainMetadata, objFilter } from '@hyperlane-xyz/sdk';
import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { objFilter, sleep } from '@hyperlane-xyz/utils';
import { logger } from '../../utils/logger';
import { sleep } from '../../utils/timeout';
import { IProviderMethods, ProviderMethod, excludeMethods } from './ProviderMethods';

@ -1,9 +1,8 @@
import { BigNumber, providers } from 'ethers';
import { chunk, isBigNumberish, isNullish } from '@hyperlane-xyz/utils';
import { logger } from '../../utils/logger';
import { isBigNumberish } from '../../utils/number';
import { chunk } from '../../utils/string';
import { isNullish } from '../../utils/typeof';
import { AllProviderMethods, IProviderMethods, ProviderMethod } from './ProviderMethods';
import { RpcConfigWithConnectionInfo } from './types';

@ -19,9 +19,9 @@ export class SmartMultiProvider extends MultiProvider {
override tryGetProvider(chainNameOrId: ChainName | number): HyperlaneSmartProvider | null {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata) return null;
const { name, publicRpcUrls, blockExplorers } = metadata;
const { name, rpcUrls, blockExplorers } = metadata;
if (!this.providers[name] && (publicRpcUrls?.length || blockExplorers?.length)) {
if (!this.providers[name] && (rpcUrls?.length || blockExplorers?.length)) {
this.providers[name] = new HyperlaneSmartProvider(metadata);
}

@ -1,8 +1,8 @@
import { ethers } from 'ethers';
import { ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import { eqAddress } from '@hyperlane-xyz/utils';
import { areAddressesEqual } from '../../utils/addresses';
import { logger } from '../../utils/logger';
import { ProviderMethod } from './ProviderMethods';
@ -17,19 +17,19 @@ const WETH_TRANSFER_TOPIC0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a116
const TRANSFER_TX_HASH = '0x45a586f90ffd5d0f8e618f0f3703b14c2c9e4611af6231d6fed32c62776b6c1b';
const goerliRpcConfig = {
...chainMetadata.goerli.publicRpcUrls[0],
...chainMetadata.goerli.rpcUrls[0],
pagination: {
maxBlockRange: 1000,
minBlockNumber: MIN_BLOCK_NUM,
},
};
const justExplorersConfig: ChainMetadata = { ...chainMetadata.goerli, publicRpcUrls: [] };
const justExplorersConfig: ChainMetadata = { ...chainMetadata.goerli, rpcUrls: [] as any };
const justRpcsConfig: ChainMetadata = {
...chainMetadata.goerli,
publicRpcUrls: [goerliRpcConfig],
rpcUrls: [goerliRpcConfig],
blockExplorers: [],
};
const combinedConfig: ChainMetadata = { ...chainMetadata.goerli, publicRpcUrls: [goerliRpcConfig] };
const combinedConfig: ChainMetadata = { ...chainMetadata.goerli, rpcUrls: [goerliRpcConfig] };
const configs: [string, ChainMetadata][] = [
['Just Explorers', justExplorersConfig],
['Just RPCs', justRpcsConfig],
@ -118,7 +118,7 @@ describe('SmartProvider', () => {
});
logger.debug('Logs found', result1.length);
expect(result1.length).toBeGreaterThan(100);
expect(areAddressesEqual(result1[0].address, WETH_CONTRACT)).toBeTruthy();
expect(eqAddress(result1[0].address, WETH_CONTRACT)).toBeTruthy();
logger.debug('Testing logs with small from/to range');
const result2 = await provider.getLogs({
@ -128,7 +128,7 @@ describe('SmartProvider', () => {
toBlock: MIN_BLOCK_NUM + 100,
});
expect(result2.length).toBeGreaterThan(10);
expect(areAddressesEqual(result2[0].address, WETH_CONTRACT)).toBeTruthy();
expect(eqAddress(result2[0].address, WETH_CONTRACT)).toBeTruthy();
logger.debug('Testing logs with large from/to range');
const result3 = await provider.getLogs({
@ -138,7 +138,7 @@ describe('SmartProvider', () => {
toBlock: 'latest',
});
expect(result3.length).toBeGreaterThan(10);
expect(areAddressesEqual(result3[0].address, WETH_CONTRACT)).toBeTruthy();
expect(eqAddress(result3[0].address, WETH_CONTRACT)).toBeTruthy();
});
itDoesIfSupported(ProviderMethod.EstimateGas, async () => {

@ -3,7 +3,6 @@ import { providers } from 'ethers';
import { ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk';
import { logger } from '../../utils/logger';
import { timeout } from '../../utils/timeout';
import { HyperlaneEtherscanProvider } from './HyperlaneEtherscanProvider';
import { HyperlaneJsonRpcProvider } from './HyperlaneJsonRpcProvider';
@ -48,8 +47,8 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
this.explorerProviders = [];
}
if (chainMetadata.publicRpcUrls?.length) {
this.rpcProviders = chainMetadata.publicRpcUrls.map((rpcConfig) => {
if (chainMetadata.rpcUrls?.length) {
this.rpcProviders = chainMetadata.rpcUrls.map((rpcConfig) => {
const newProvider = new HyperlaneJsonRpcProvider(rpcConfig, network);
newProvider.supportedMethods.forEach((m) => supportedMethods.add(m));
return newProvider;
@ -108,9 +107,7 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
}
const resultPromise = wrapProviderPerform(provider, providerUrl, method, params, reqId);
const timeoutPromise = timeout<ProviderTimeoutResult>(PROVIDER_STAGGER_DELAY_MS, {
status: ProviderStatus.Timeout,
});
const timeoutPromise = timeoutResult(1);
const result = await Promise.race([resultPromise, timeoutPromise]);
if (result.status === ProviderStatus.Success) {
@ -136,9 +133,7 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
}
} else if (providerResultPromises.length > 0) {
// All providers already triggered, wait for one to complete or all to fail/timeout
const timeoutPromise = timeout<ProviderTimeoutResult>(PROVIDER_STAGGER_DELAY_MS * 20, {
status: ProviderStatus.Timeout,
});
const timeoutPromise = timeoutResult(20);
const resultPromise = waitForProviderSuccess(providerResultPromises);
const result = await Promise.race([resultPromise, timeoutPromise]);
@ -227,10 +222,25 @@ function throwCombinedProviderErrors(errors: unknown[], fallbackMsg: string): vo
else throw new Error(fallbackMsg);
}
function chainMetadataToProviderNetwork(chainMetadata: ChainMetadata): providers.Network {
function chainMetadataToProviderNetwork(
chainMetadata: ChainMetadata | ChainMetadataWithRpcConnectionInfo,
): providers.Network {
return {
name: chainMetadata.name,
chainId: chainMetadata.chainId,
// @ts-ignore add ensAddress to ChainMetadata
ensAddress: chainMetadata.ensAddress,
};
}
function timeoutResult(multiplier: number) {
return new Promise<ProviderTimeoutResult>((resolve) =>
setTimeout(
() =>
resolve({
status: ProviderStatus.Timeout,
}),
PROVIDER_STAGGER_DELAY_MS * multiplier,
),
);
}

@ -2,12 +2,12 @@ import type { utils } from 'ethers';
import type { ChainMetadata } from '@hyperlane-xyz/sdk';
export type RpcConfigWithConnectionInfo = ChainMetadata['publicRpcUrls'][number] & {
export type RpcConfigWithConnectionInfo = ChainMetadata['rpcUrls'][number] & {
connection?: utils.ConnectionInfo;
};
export interface ChainMetadataWithRpcConnectionInfo extends ChainMetadata {
publicRpcUrls: RpcConfigWithConnectionInfo[];
export interface ChainMetadataWithRpcConnectionInfo extends Omit<ChainMetadata, 'rpcUrls'> {
rpcUrls: Array<RpcConfigWithConnectionInfo>;
}
export enum ProviderStatus {

@ -42,20 +42,6 @@ body {
background-color: #f3f4f6;
}
#app-content::before {
content: '';
position: fixed;
z-index: -1;
display: block;
width: 100vw;
height: 100vh;
background-color: #f3f4f6;
background-image: url('/background-texture.png');
background-size: cover;
background-repeat: no-repeat;
background-position: center top;
}
/*
Text and shadows
================

@ -1,69 +0,0 @@
import { getAddress, isAddress } from '@ethersproject/address';
import { logger } from './logger';
// Validates content and checksum
export function isValidAddress(address: string) {
// Need to catch because ethers' isAddress throws in some cases (bad checksum)
try {
const isValid = address && isAddress(address);
return !!isValid;
} catch (error) {
logger.warn('Invalid address', error, address);
return false;
}
}
// Faster then above and avoids exceptions but less thorough
const addressRegex = /^(0x)?[a-fA-F0-9]{40}$/;
export function isValidAddressFast(address: string) {
return addressRegex.test(address);
}
export function validateAddress(address: string, context: string) {
if (!address || !isAddress(address)) {
const errorMsg = `Invalid addresses for ${context}: ${address}`;
logger.error(errorMsg);
throw new Error(errorMsg);
}
}
export function normalizeAddress(address: string) {
validateAddress(address, 'normalize');
return getAddress(address);
}
export function shortenAddress(address: string, capitalize?: boolean) {
try {
const normalized = normalizeAddress(address);
const shortened =
normalized.substring(0, 5) + '...' + normalized.substring(normalized.length - 4);
return capitalize ? capitalizeAddress(shortened) : shortened;
} catch (error) {
logger.error('Unable to shorten invalid address', address, error);
return null;
}
}
export function capitalizeAddress(address: string) {
return '0x' + address.substring(2).toUpperCase();
}
export function areAddressesEqual(a1: string, a2: string) {
validateAddress(a1, 'compare');
validateAddress(a2, 'compare');
return getAddress(a1) === getAddress(a2);
}
export function trimLeading0x(input: string) {
return input.startsWith('0x') ? input.substring(2) : input;
}
export function ensureLeading0x(input: string) {
return input.startsWith('0x') ? input : `0x${input}`;
}
const txHashRegex = /^(0x)?([A-Fa-f0-9]{64})$/;
export function isValidTransactionHash(input: string) {
return txHashRegex.test(input);
}

@ -1,78 +0,0 @@
import { formatUnits, parseUnits } from '@ethersproject/units';
import BigNumber from 'bignumber.js';
import { DISPLAY_DECIMALS, MIN_ROUNDED_VALUE } from '../consts/values';
import { logger } from './logger';
export type NumberT = BigNumber.Value;
export function fromWei(value: NumberT | null | undefined, toUnitName?: string): number {
if (!value) return 0;
const valueString = value.toString().trim();
const flooredValue = new BigNumber(valueString).toFixed(0, BigNumber.ROUND_FLOOR);
return parseFloat(formatUnits(flooredValue, toUnitName));
}
// Similar to fromWei above but rounds to set number of decimals
// with a minimum floor, configured per token
export function fromWeiRounded(
value: NumberT | null | undefined,
toUnitName?: string,
roundDownIfSmall = true,
decimals = DISPLAY_DECIMALS,
): string {
if (!value) return '0';
const flooredValue = new BigNumber(value).toFixed(0, BigNumber.ROUND_FLOOR);
const amount = new BigNumber(formatUnits(flooredValue, toUnitName));
if (amount.isZero()) return '0';
// If amount is less than min value
if (amount.lt(MIN_ROUNDED_VALUE)) {
if (roundDownIfSmall) return '0';
else return MIN_ROUNDED_VALUE.toString();
}
return amount.toFixed(decimals).toString();
}
export function toWei(value: NumberT | null | undefined): BigNumber {
if (!value) return new BigNumber(0);
const valueString = value.toString().trim();
const components = valueString.split('.');
if (components.length === 1) {
return new BigNumber(parseUnits(valueString).toString());
} else if (components.length === 2) {
const trimmedFraction = components[1].substring(0);
return new BigNumber(parseUnits(`${components[0]}.${trimmedFraction}`).toString());
} else {
throw new Error(`Cannot convert ${valueString} to wei`);
}
}
export function tryParseAmount(value: NumberT | null | undefined): BigNumber | null {
try {
if (!value) return null;
const parsed = new BigNumber(value);
if (!parsed || parsed.isNaN() || !parsed.isFinite()) return null;
else return parsed;
} catch (error) {
logger.warn('Error parsing amount', value);
return null;
}
}
// 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
export function areAmountsNearlyEqual(amountInWei1: BigNumber, amountInWei2: NumberT) {
const minValueWei = toWei(MIN_ROUNDED_VALUE);
// Is difference btwn amount and balance less than min amount shown for token
return amountInWei1.minus(amountInWei2).abs().lt(minValueWei);
}
export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) {
return bn1.gte(bn2) ? bn2 : bn1;
}
export function BigNumberMax(bn1: BigNumber, bn2: BigNumber) {
return bn1.lte(bn2) ? bn2 : bn1;
}

@ -1,22 +0,0 @@
import { logger } from './logger';
export function toBase64(data: any): string | undefined {
try {
if (!data) throw new Error('No data to encode');
return btoa(JSON.stringify(data));
} catch (error) {
logger.error('Unable to serialize + encode data to base64', data);
return undefined;
}
}
export function fromBase64<T>(data: string | string[]): T | undefined {
try {
if (!data) throw new Error('No data to decode');
const msg = Array.isArray(data) ? data[0] : data;
return JSON.parse(atob(msg));
} catch (error) {
logger.error('Unable to decode + deserialize data from base64', data);
return undefined;
}
}

@ -0,0 +1,9 @@
import BigNumber from 'bignumber.js';
export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) {
return bn1.gte(bn2) ? bn2 : bn1;
}
export function BigNumberMax(bn1: BigNumber, bn2: BigNumber) {
return bn1.lte(bn2) ? bn2 : bn1;
}

@ -1,16 +0,0 @@
import { logger } from './logger';
import { trimToLength } from './string';
export function errorToString(error: any, maxLength = 300) {
if (!error) return 'Unknown Error';
if (typeof error === 'string') return trimToLength(error, maxLength);
if (typeof error === 'number') return `Error code: ${error}`;
const details = error.message || error.reason || error;
if (typeof details === 'string') return trimToLength(details, maxLength);
return trimToLength(JSON.stringify(details), maxLength);
}
export function logAndThrow(message: string, error?: any) {
logger.error(message, error);
throw new Error(message);
}

@ -1,13 +1,14 @@
import { BigNumber, providers } from 'ethers';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { sleep } from '@hyperlane-xyz/utils';
import { config } from '../consts/config';
import type { ExtendedLog } from '../types';
import { logger } from './logger';
import { toDecimalNumber, tryToDecimalNumber } from './number';
import { fetchWithTimeout, sleep } from './timeout';
import { fetchWithTimeout } from './timeout';
const BLOCK_EXPLORER_RATE_LIMIT = 6000; // once every 6 seconds
// Used for crude rate-limiting of explorer queries without API keys

@ -1,7 +1,6 @@
import { BigNumber, BigNumberish, constants } from 'ethers';
import { BigNumber, BigNumberish } from 'ethers';
import { logger } from './logger';
import { isNullish } from './typeof';
export function tryToDecimalNumber(value: BigNumberish) {
try {
@ -17,22 +16,3 @@ export function toDecimalNumber(value: BigNumberish) {
if (result === null || result === undefined) throw new Error(`Error parsing hex number ${value}`);
return result;
}
export function isBigNumberish(value: any): value is BigNumberish {
try {
if (isNullish(value)) return false;
return BigNumber.from(value)._isBigNumber;
} catch (error) {
return false;
}
}
// If a value (e.g. hex string or number) is zeroish (0, 0x0, 0x00, etc.)
export function isZeroish(value: BigNumberish) {
try {
if (!value || value === constants.HashZero || value === constants.AddressZero) return true;
return BigNumber.from(value).isZero();
} catch (error) {
return false;
}
}

@ -1,30 +0,0 @@
export function invertKeysAndValues(data: any) {
return Object.fromEntries(Object.entries(data).map(([key, value]) => [value, key]));
}
// Get the subset of the object from key list
export function pick<K extends string | number, V = any>(obj: Record<K, V>, keys: K[]) {
const ret: Partial<Record<K, V>> = {};
for (const key of keys) {
ret[key] = obj[key];
}
return ret as Record<K, V>;
}
// Remove a particular key from an object if it exists
export function omit<K extends string | number, V = any>(obj: Record<K, V>, key: K) {
const ret: Partial<Record<K, V>> = {};
for (const k of Object.keys(obj)) {
if (k === key) continue;
ret[k] = obj[k];
}
return ret as Record<K, V>;
}
// Returns an object with the keys as values from an array and value set to true
export function arrayToObject(keys: Array<string | number>, val = true) {
return keys.reduce((result, k) => {
result[k] = val;
return result;
}, {});
}

@ -1,20 +0,0 @@
import { logger } from './logger';
import { sleep } from './timeout';
// If all the tries fail it raises the last thrown exception
export async function retryAsync<T>(runner: () => T, attempts = 3, delay = 500) {
let saveError;
for (let i = 0; i < attempts; i++) {
try {
const result = await runner();
if (result) return result;
else throw new Error('Empty result');
} catch (error) {
logger.error(`retryAsync: Failed to execute function on attempt #${i}:`, error);
saveError = error;
await sleep(delay * (i + 1));
}
}
logger.error(`retryAsync: All attempts failed`);
throw saveError;
}

@ -1,12 +1,6 @@
import { trimLeading0x } from './addresses';
export function toTitleCase(str: string) {
return str.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
// TODO add unit tests
import { strip0x } from '@hyperlane-xyz/utils';
// Only allows letters and numbers
const alphanumericRgex = /[^a-zA-Z0-9]/gi;
export function sanitizeString(str: string) {
@ -14,30 +8,11 @@ export function sanitizeString(str: string) {
return str.replaceAll(alphanumericRgex, '').toLowerCase();
}
export function trimToLength(value: string, maxLength: number) {
if (!value) return '';
const trimmed = value.trim();
return trimmed.length > maxLength ? trimmed.substring(0, maxLength) + '...' : trimmed;
}
interface Sliceable {
length: number;
slice: (i: number, j: number) => any;
}
export function chunk<T extends Sliceable>(str: T, size: number) {
const R: Array<T> = [];
for (let i = 0; i < str.length; i += size) {
R.push(str.slice(i, i + size));
}
return R;
}
export function tryUtf8DecodeBytes(value: string, fatal = true) {
if (!value) return undefined;
try {
const decoder = new TextDecoder('utf-8', { fatal });
const decodedBody = decoder.decode(Buffer.from(trimLeading0x(value), 'hex'));
const decodedBody = decoder.decode(Buffer.from(strip0x(value), 'hex'));
return decodedBody;
} catch (error) {
return undefined;

@ -39,37 +39,3 @@ export async function fetchWithTimeout(
clearTimeout(id);
return response;
}
export function timeout<T>(milliseconds: number, resolveValue: T): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(resolveValue), milliseconds));
}
export function sleep(milliseconds: number): Promise<true> {
return timeout(milliseconds, true);
}
export const PROMISE_TIMEOUT = '__promise_timeout__';
export async function promiseTimeout<T>(promise: Promise<T>, milliseconds: number) {
// Create a promise that rejects in <ms> milliseconds
const timeout = new Promise<T>((_resolve, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(new Error(PROMISE_TIMEOUT));
}, milliseconds);
});
// Awaits the race, which will throw on timeout
const result = await Promise.race([promise, timeout]);
return result;
}
export function asyncTimeout<P extends Array<any>, R>(
inner: (...args: P) => Promise<R>,
timeout: number,
) {
return async (...args: P): Promise<R> => {
const resultP = inner(...args);
const result = await promiseTimeout(resultP, timeout);
return result;
};
}

@ -1,4 +0,0 @@
export function isNullish<T>(val: T) {
if (val === null || val === undefined) return true;
else return false;
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save