Merge pull request #40 from hyperlane-xyz/pi-gas-estimation

- Finish debugger IGP payment fetching impl
- Check mailbox for delivery status if logs are not found
- Refactor TransactionCard components
- Rename shouldBlur to blur for brevity
- Surface GraphQL messages even if some fields are missing
- Surface IGP payments from debugger in gas details card
pull/41/head
J M Rossy 2 years ago committed by GitHub
commit ee70ee0d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/features/chains/utils.ts
  2. 138
      src/features/debugger/debugMessage.ts
  3. 1
      src/features/debugger/strings.ts
  4. 41
      src/features/debugger/types.ts
  5. 119
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  6. 6
      src/features/deliveryStatus/types.ts
  7. 13
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  8. 44
      src/features/messages/MessageDetails.tsx
  9. 2
      src/features/messages/cards/CodeBlock.tsx
  10. 56
      src/features/messages/cards/ContentDetailsCard.tsx
  11. 96
      src/features/messages/cards/GasDetailsCard.tsx
  12. 12
      src/features/messages/cards/IcaDetailsCard.tsx
  13. 12
      src/features/messages/cards/KeyValueRow.tsx
  14. 6
      src/features/messages/cards/TimelineCard.tsx
  15. 279
      src/features/messages/cards/TransactionCard.tsx
  16. 8
      src/features/messages/placeholderMessages.ts
  17. 10
      src/features/messages/queries/parse.ts
  18. 1
      src/global.d.ts
  19. 6
      src/pages/api/latest-nonce.ts
  20. 7
      src/utils/amount.ts
  21. 12
      src/utils/number.ts
  22. 2
      src/utils/time.ts

@ -2,6 +2,7 @@ import {
type ChainMap,
type ChainName,
type MultiProvider,
chainIdToMetadata,
hyperlaneContractAddresses,
} from '@hyperlane-xyz/sdk';
@ -51,3 +52,7 @@ export function getContractAddress(
if (!addr) throw new Error(`No contract address found for ${contractName} on ${chainName}`);
return addr;
}
export function isPiChain(chainId: number) {
return !chainIdToMetadata[chainId];
}

@ -1,12 +1,8 @@
// Forked from debug script in monorepo but mostly rewritten
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts
import { BigNumber, providers } from 'ethers';
import { BigNumber, utils as ethersUtils, providers } from 'ethers';
import {
type IInterchainGasPaymaster,
IMessageRecipient__factory,
InterchainGasPaymaster__factory,
} from '@hyperlane-xyz/core';
import { IMessageRecipient__factory, InterchainGasPaymaster__factory } from '@hyperlane-xyz/core';
import type { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
@ -16,10 +12,10 @@ import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { trimToLength } from '../../utils/string';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress, tryGetContractAddress } from '../chains/utils';
import { getContractAddress } from '../chains/utils';
import { isIcaMessage, tryDecodeIcaBody, tryFetchIcaAddress } from '../messages/ica';
import { MessageDebugDetails, MessageDebugStatus } from './types';
import { GasPayment, MessageDebugDetails, MessageDebugStatus } from './types';
type Provider = providers.Provider;
@ -41,7 +37,6 @@ export async function debugExplorerMessage(
} = message;
logger.debug(`Debugging message id: ${msgId}`);
const originName = multiProvider.getChainName(originDomain);
const destName = multiProvider.tryGetChainName(destDomain)!;
const originProvider = multiProvider.getProvider(originDomain);
const destProvider = multiProvider.getProvider(destDomain);
@ -63,21 +58,16 @@ export async function debugExplorerMessage(
if (deliveryResult.status && deliveryResult.details) return deliveryResult;
const gasEstimate = deliveryResult.gasEstimate;
const igpAddress = tryGetContractAddress(
customChainConfigs,
originName,
'interchainGasPaymaster',
);
const insufficientGas = await isIgpUnderfunded(
const gasCheckResult = await tryCheckIgpGasFunded(
msgId,
originProvider,
igpAddress,
gasEstimate,
totalGasAmount,
);
if (insufficientGas) return insufficientGas;
if (gasCheckResult.status && gasCheckResult.details) return gasCheckResult;
return noErrorFound();
logger.debug(`No errors found debugging message id: ${msgId}`);
return { ...noErrorFound(), gasDetails: gasCheckResult.gasDetails };
}
async function isInvalidRecipient(provider: Provider, recipient: Address) {
@ -152,72 +142,90 @@ async function debugMessageDelivery(
}
}
async function isIgpUnderfunded(
msgId: string,
originProvider: Provider,
igpAddress?: Address,
deliveryGasEst?: string,
totalGasAmount?: string,
) {
if (!igpAddress) {
logger.debug('No IGP address provided, skipping gas funding check');
return false;
}
const igpContract = InterchainGasPaymaster__factory.connect(igpAddress, originProvider);
const { isFunded, igpDetails } = await tryCheckIgpGasFunded(
igpContract,
msgId,
deliveryGasEst,
totalGasAmount,
);
if (!isFunded) {
return {
status: MessageDebugStatus.GasUnderfunded,
details: igpDetails,
};
}
return false;
}
async function tryCheckIgpGasFunded(
igp: IInterchainGasPaymaster,
messageId: string,
deliveryGasEst?: string,
originProvider: Provider,
deliveryGasEstimate?: string,
totalGasAmount?: string,
) {
if (!deliveryGasEstimate) {
logger.warn('No gas estimate provided, skipping IGP check');
return {};
}
try {
if (!deliveryGasEst) throw new Error('No gas estimate provided');
let gasAlreadyFunded = BigNumber.from(0);
if (totalGasAmount) {
const filter = igp.filters.GasPayment(messageId, null, null);
const matchedEvents = (await igp.queryFilter(filter)) || [];
logger.debug(`Found ${matchedEvents.length} payments to IGP for msg ${messageId}`);
logger.debug(matchedEvents);
for (const payment of matchedEvents) {
gasAlreadyFunded = gasAlreadyFunded.add(payment.args.gasAmount);
}
} else {
let gasDetails: MessageDebugDetails['gasDetails'] = {
deliveryGasEstimate,
};
if (totalGasAmount && BigNumber.from(totalGasAmount).gt(0)) {
logger.debug(`Using totalGasAmount info from message: ${totalGasAmount}`);
gasAlreadyFunded = BigNumber.from(totalGasAmount);
} else {
logger.debug('Querying for gas payments events for msg to any contract');
const { contractToPayments, contractToTotalGas, numPayments, numIGPs } =
await fetchGasPaymentEvents(originProvider, messageId);
gasDetails = { deliveryGasEstimate, contractToPayments };
logger.debug(`Found ${numPayments} payments to ${numIGPs} IGPs for msg ${messageId}`);
if (numIGPs === 1) {
gasAlreadyFunded = Object.values(contractToTotalGas)[0];
} else if (numIGPs > 1) {
logger.warn(`>1 IGPs paid for msg ${messageId}. Unsure which to use, skipping check.`);
return { gasDetails };
}
}
logger.debug('Amount of gas paid for to IGP:', gasAlreadyFunded.toString());
logger.debug('Amount of gas required:', deliveryGasEst);
logger.debug('Approximate amount of gas required:', deliveryGasEstimate);
if (gasAlreadyFunded.lte(0)) {
return { isFunded: false, igpDetails: 'Origin IGP has not received any gas payments' };
} else if (gasAlreadyFunded.lte(deliveryGasEst)) {
return {
isFunded: false,
igpDetails: `Origin IGP gas amount is ${gasAlreadyFunded.toString()} but requires ${deliveryGasEst}`,
status: MessageDebugStatus.GasUnderfunded,
details: 'Origin IGP has not received any gas payments',
gasDetails,
};
} else if (gasAlreadyFunded.lte(deliveryGasEstimate)) {
return {
status: MessageDebugStatus.GasUnderfunded,
details: `Origin IGP gas amount is ${gasAlreadyFunded.toString()} but requires ${deliveryGasEstimate}`,
gasDetails,
};
} else {
return { isFunded: true, igpDetails: '' };
return { gasDetails };
}
} catch (error) {
logger.warn('Error estimating delivery gas cost for message', error);
return { isFunded: true, igpDetails: '' };
return {};
}
}
async function fetchGasPaymentEvents(provider: Provider, messageId: string) {
const igpInterface = InterchainGasPaymaster__factory.createInterface();
const paymentFragment = igpInterface.getEvent('GasPayment');
const paymentTopics = igpInterface.encodeFilterTopics(paymentFragment.name, [messageId]);
const paymentLogs = (await provider.getLogs({ topics: paymentTopics })) || [];
const contractToPayments: AddressTo<GasPayment[]> = {};
const contractToTotalGas: AddressTo<BigNumber> = {};
let numPayments = 0;
for (const log of paymentLogs) {
const contractAddr = log.address;
let newEvent: ethersUtils.LogDescription;
try {
newEvent = igpInterface.parseLog(log);
} catch (error) {
logger.warn('Error parsing gas payment log', error);
continue;
}
const newPayment = {
gasAmount: BigNumber.from(newEvent.args.gasAmount).toString(),
paymentAmount: BigNumber.from(newEvent.args.payment).toString(),
};
contractToPayments[contractAddr] = [...(contractToPayments[contractAddr] || []), newPayment];
contractToTotalGas[contractAddr] = (contractToTotalGas[contractAddr] || BigNumber.from(0)).add(
newEvent.args.gasAmount,
);
numPayments += 1;
}
const numIGPs = Object.keys(contractToPayments).length;
return { contractToPayments, contractToTotalGas, numPayments, numIGPs };
}
async function tryCheckBytecodeHandle(provider: Provider, recipientAddress: string) {

@ -1,7 +1,6 @@
import { MessageDebugStatus } from './types';
export const debugStatusToDesc: Record<MessageDebugStatus, string> = {
[MessageDebugStatus.AlreadyProcessed]: 'No errors found, message already processed',
[MessageDebugStatus.NoErrorsFound]: 'No errors found, message appears to be deliverable',
[MessageDebugStatus.RecipientNotContract]: 'Recipient address is not a contract',
[MessageDebugStatus.RecipientNotHandler]:

@ -1,11 +1,4 @@
export enum TxDebugStatus {
NotFound = 'notFound',
NoMessages = 'noMessages',
MessagesFound = 'messagesFound',
}
export enum MessageDebugStatus {
AlreadyProcessed = 'alreadyProcessed',
NoErrorsFound = 'noErrorsFound',
RecipientNotContract = 'recipientNotContract',
RecipientNotHandler = 'recipientNotHandler',
@ -14,36 +7,16 @@ export enum MessageDebugStatus {
GasUnderfunded = 'gasUnderfunded',
}
export interface DebugNotFoundResult {
status: TxDebugStatus.NotFound;
details: string;
}
export interface DebugNoMessagesResult {
status: TxDebugStatus.NoMessages;
chainName: string;
details: string;
explorerLink?: string;
}
export interface LinkProperty {
url: string;
text: string;
}
export interface MessageDebugDetails {
status: MessageDebugStatus;
details: string;
gasDetails?: {
deliveryGasEstimate?: string;
contractToPayments?: AddressTo<GasPayment[]>;
};
}
export interface DebugMessagesFoundResult {
status: TxDebugStatus.MessagesFound;
chainName: string;
explorerLink?: string;
messageDetails: MessageDebugDetails[];
export interface GasPayment {
gasAmount: string;
paymentAmount: string;
}
export type MessageDebugResult =
| DebugNotFoundResult
| DebugNoMessagesResult
| DebugMessagesFoundResult;

@ -1,5 +1,6 @@
import { constants } from 'ethers';
import { IMailbox__factory } from '@hyperlane-xyz/core';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../types';
@ -9,20 +10,14 @@ import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { debugExplorerMessage } from '../debugger/debugMessage';
import { MessageDebugStatus } from '../debugger/types';
import { TX_HASH_ZERO } from '../messages/placeholderMessages';
import {
MessageDeliveryFailingResult,
MessageDeliveryPendingResult,
MessageDeliveryStatusResponse,
MessageDeliverySuccessResult,
} from './types';
// The keccak-256 hash of the ProcessId event: ProcessId(bytes32)
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/1.0.0-beta0/solidity/contracts/Mailbox.sol#L84
// https://emn178.github.io/online-tools/keccak_256.html
// Alternatively could get this by creating the Mailbox contract object via SDK
const PROCESS_TOPIC_0 = '0x1cae38cdd3d3919489272725a5ae62a4f48b2989b0dae843d3c279fee18073a9';
export async function fetchDeliveryStatus(
multiProvider: MultiProvider,
customChainConfigs: ChainMap<ChainConfig>,
@ -31,67 +26,95 @@ export async function fetchDeliveryStatus(
const destName = multiProvider.getChainName(message.destinationChainId);
const destMailboxAddr = getContractAddress(customChainConfigs, destName, 'mailbox');
const logs = await fetchMessageLogs(multiProvider, message, destMailboxAddr);
const { isDelivered, blockNumber, transactionHash } = await checkIsMessageDelivered(
multiProvider,
message,
destMailboxAddr,
);
if (logs?.length) {
logger.debug(`Found delivery log for tx ${message.origin.hash}`);
const log = logs[0]; // Should only be 1 log per message delivery
if (isDelivered) {
const txDetails = await fetchTransactionDetails(
multiProvider,
message.destinationChainId,
log.transactionHash,
transactionHash,
);
// If a delivery (aka process) tx is found, mark as success
const result: MessageDeliverySuccessResult = {
status: MessageStatus.Delivered,
deliveryTransaction: {
timestamp: toDecimalNumber(txDetails.timestamp || 0) * 1000,
hash: log.transactionHash,
from: txDetails.from || constants.AddressZero,
to: txDetails.to || constants.AddressZero,
blockHash: txDetails.blockHash || TX_HASH_ZERO,
blockNumber: toDecimalNumber(log.blockNumber),
timestamp: toDecimalNumber(txDetails?.timestamp || 0) * 1000,
hash: transactionHash || constants.HashZero,
from: txDetails?.from || constants.AddressZero,
to: txDetails?.to || constants.AddressZero,
blockHash: txDetails?.blockHash || constants.HashZero,
blockNumber: toDecimalNumber(blockNumber || 0),
mailbox: constants.AddressZero,
nonce: txDetails.nonce || 0,
gasLimit: toDecimalNumber(txDetails.gasLimit || 0),
gasPrice: toDecimalNumber(txDetails.gasPrice || 0),
effectiveGasPrice: toDecimalNumber(txDetails.gasPrice || 0),
gasUsed: toDecimalNumber(txDetails.gasLimit || 0),
cumulativeGasUsed: toDecimalNumber(txDetails.gasLimit || 0),
maxFeePerGas: toDecimalNumber(txDetails.maxFeePerGas || 0),
maxPriorityPerGas: toDecimalNumber(txDetails.maxPriorityFeePerGas || 0),
nonce: txDetails?.nonce || 0,
gasLimit: toDecimalNumber(txDetails?.gasLimit || 0),
gasPrice: toDecimalNumber(txDetails?.gasPrice || 0),
effectiveGasPrice: toDecimalNumber(txDetails?.gasPrice || 0),
gasUsed: toDecimalNumber(txDetails?.gasLimit || 0),
cumulativeGasUsed: toDecimalNumber(txDetails?.gasLimit || 0),
maxFeePerGas: toDecimalNumber(txDetails?.maxFeePerGas || 0),
maxPriorityPerGas: toDecimalNumber(txDetails?.maxPriorityFeePerGas || 0),
},
};
return result;
} else {
const debugResult = await debugExplorerMessage(multiProvider, customChainConfigs, message);
if (
debugResult.status === MessageDebugStatus.NoErrorsFound ||
debugResult.status === MessageDebugStatus.AlreadyProcessed
) {
return { status: MessageStatus.Pending };
} else {
const result: MessageDeliveryFailingResult = {
status: MessageStatus.Failing,
debugStatus: debugResult.status,
debugDetails: debugResult.details,
};
return result;
}
const {
status: debugStatus,
details: debugDetails,
gasDetails,
} = await debugExplorerMessage(multiProvider, customChainConfigs, message);
const messageStatus =
debugStatus === MessageDebugStatus.NoErrorsFound
? MessageStatus.Pending
: MessageStatus.Failing;
const result: MessageDeliveryPendingResult | MessageDeliveryFailingResult = {
status: messageStatus,
debugStatus,
debugDetails,
gasDetails,
};
return result;
}
}
function fetchMessageLogs(multiProvider: MultiProvider, message: Message, mailboxAddr: Address) {
const { msgId, origin, destinationChainId } = message;
logger.debug(`Searching for delivery logs for tx ${origin.hash}`);
async function checkIsMessageDelivered(
multiProvider: MultiProvider,
message: Message,
mailboxAddr: Address,
) {
const { msgId, destinationChainId } = message;
const provider = multiProvider.getProvider(destinationChainId);
return provider.getLogs({
topics: [PROCESS_TOPIC_0, msgId],
address: mailboxAddr,
});
const mailbox = IMailbox__factory.connect(mailboxAddr, provider);
// Try finding logs first as they have more info
try {
logger.debug(`Searching for process logs for msgId ${msgId}`);
const logs = await mailbox.queryFilter(mailbox.filters.ProcessId(msgId));
if (logs?.length) {
logger.debug(`Found process log for ${msgId}}`);
const log = logs[0]; // Should only be 1 log per message delivery
return {
isDelivered: true,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
};
}
} catch (error) {
logger.warn(`Error querying for process logs for msgId ${msgId}`, error);
}
// Logs are unreliable so check the mailbox itself as a fallback
logger.debug(`Querying mailbox about msgId ${msgId}`);
const isDelivered = await mailbox.delivered(msgId);
logger.debug(`Mailbox delivery status for ${msgId}: ${isDelivered}}`);
return { isDelivered };
}
function fetchTransactionDetails(multiProvider: MultiProvider, chainId: ChainId, txHash: string) {
function fetchTransactionDetails(multiProvider: MultiProvider, chainId: ChainId, txHash?: string) {
if (!txHash) return null;
logger.debug(`Searching for transaction details for ${txHash}`);
const provider = multiProvider.getProvider(chainId);
return provider.getTransaction(txHash);

@ -1,5 +1,5 @@
import type { MessageStatus, MessageTx } from '../../types';
import type { MessageDebugStatus } from '../debugger/types';
import type { MessageDebugDetails, MessageDebugStatus } from '../debugger/types';
interface MessageDeliveryResult {
status: MessageStatus;
@ -14,10 +14,14 @@ export interface MessageDeliveryFailingResult extends MessageDeliveryResult {
status: MessageStatus.Failing;
debugStatus: MessageDebugStatus;
debugDetails: string;
gasDetails: MessageDebugDetails['gasDetails'];
}
export interface MessageDeliveryPendingResult extends MessageDeliveryResult {
status: MessageStatus.Pending;
debugStatus: MessageDebugStatus;
debugDetails: string;
gasDetails: MessageDebugDetails['gasDetails'];
}
export type MessageDeliveryStatusResponse =

@ -16,7 +16,7 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
const multiProvider = useMultiProvider();
const serializedMessage = JSON.stringify(message);
const { data, error } = useQuery(
const { data, error, isFetching } = useQuery(
['messageDeliveryStatus', serializedMessage, pause],
async () => {
if (pause || !message || message.status === MessageStatus.Delivered) return null;
@ -41,7 +41,6 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
logger.debug('Fetching message delivery status for:', message.id);
const deliverStatus = await fetchDeliveryStatus(multiProvider, chainConfigs, message);
logger.debug('Message delivery status result', deliverStatus);
return deliverStatus;
},
{ retry: false },
@ -64,20 +63,22 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
destination: data.deliveryTransaction,
},
];
} else if (data?.status === MessageStatus.Failing) {
} else if (data?.status === MessageStatus.Failing || data?.status === MessageStatus.Pending) {
return [
{
...message,
status: MessageStatus.Failing,
status: data.status,
},
{
status: data.debugStatus,
details: data.debugDetails,
gasDetails: data.gasDetails,
},
];
} else {
return [message];
}
return [message];
}, [message, data]);
return { messageWithDeliveryStatus, debugInfo };
return { messageWithDeliveryStatus, debugInfo, isDeliveryStatusFetching: isFetching };
}

@ -16,7 +16,7 @@ import { ContentDetailsCard } from './cards/ContentDetailsCard';
import { GasDetailsCard } from './cards/GasDetailsCard';
import { IcaDetailsCard } from './cards/IcaDetailsCard';
import { TimelineCard } from './cards/TimelineCard';
import { TransactionCard } from './cards/TransactionCard';
import { DestinationTransactionCard, OriginTransactionCard } from './cards/TransactionCard';
import { useIsIcaMessage } from './ica';
import { usePiChainMessageQuery } from './pi-queries/usePiChainMessageQuery';
import { PLACEHOLDER_MESSAGE } from './placeholderMessages';
@ -60,12 +60,16 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
const isMessageFound = !!messageFromUrlParams || isGraphQlMessageFound || isPiMessageFound;
const isFetching = isGraphQlFetching || isPiFetching;
const isError = isGraphQlError || isPiError;
const shouldBlur = !isMessageFound;
const blur = !isMessageFound;
const isIcaMsg = useIsIcaMessage(_message);
// If message isn't delivered, attempt to check for
// more recent updates and possibly debug info
const { messageWithDeliveryStatus: message, debugInfo } = useMessageDeliveryStatus({
const {
messageWithDeliveryStatus: message,
debugInfo,
isDeliveryStatusFetching,
} = useMessageDeliveryStatus({
message: _message,
pause: !isMessageFound,
});
@ -94,28 +98,24 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
/>
</div>
<div className="flex flex-wrap items-stretch justify-between mt-5 gap-3">
<TransactionCard
title="Origin Transaction"
chainId={originChainId}
status={status}
transaction={origin}
helpText={helpText.origin}
shouldBlur={shouldBlur}
/>
<TransactionCard
title="Destination Transaction"
<OriginTransactionCard chainId={originChainId} transaction={origin} blur={blur} />
<DestinationTransactionCard
chainId={destChainId}
status={status}
transaction={destination}
debugInfo={debugInfo}
isStatusFetching={isDeliveryStatusFetching}
isPiMsg={message.isPiMsg}
helpText={helpText.destination}
shouldBlur={shouldBlur}
blur={blur}
/>
{!message.isPiMsg && <TimelineCard message={message} shouldBlur={shouldBlur} />}
<ContentDetailsCard message={message} shouldBlur={shouldBlur} />
<GasDetailsCard message={message} shouldBlur={shouldBlur} />
{isIcaMsg && <IcaDetailsCard message={message} shouldBlur={shouldBlur} />}
{!message.isPiMsg && <TimelineCard message={message} blur={blur} />}
<ContentDetailsCard message={message} blur={blur} />
<GasDetailsCard
message={message}
igpPayments={debugInfo?.gasDetails?.contractToPayments}
blur={blur}
/>
{isIcaMsg && <IcaDetailsCard message={message} blur={blur} />}
</div>
</>
);
@ -191,9 +191,3 @@ function useDynamicBannerColor(
return () => setBanner('');
}, [setBanner]);
}
const helpText = {
origin: 'Info about the transaction that initiated the message placement into the outbox.',
destination:
'Info about the transaction that triggered the delivery of the message from an inbox.',
};

@ -11,7 +11,7 @@ export function LabelAndCodeBlock({ label, value }: { label: string; value: stri
export function CodeBlock({ value }: { value: string }) {
return (
<div className="relative max-w-full break-words py-2 pl-2 pr-9 mt-2 bg-gray-100 text-sm font-mono rounded">
<div className="relative max-w-full break-words py-2 pl-2 pr-9 mt-2 min-h-[2rem] bg-gray-100 text-sm font-mono rounded">
{value}
<CopyButton
copyValue={value}

@ -1,5 +1,5 @@
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { utils } from '@hyperlane-xyz/utils';
@ -9,6 +9,7 @@ import { Card } from '../../../components/layout/Card';
import { MAILBOX_VERSION } from '../../../consts/environments';
import EnvelopeInfo from '../../../images/icons/envelope-info.svg';
import { Message } from '../../../types';
import { logger } from '../../../utils/logger';
import { tryUtf8DecodeBytes } from '../../../utils/string';
import { CodeBlock, LabelAndCodeBlock } from './CodeBlock';
@ -16,7 +17,7 @@ import { KeyValueRow } from './KeyValueRow';
interface Props {
message: Message;
shouldBlur: boolean;
blur: boolean;
}
export function ContentDetailsCard({
@ -30,7 +31,7 @@ export function ContentDetailsCard({
body,
decodedBody,
},
shouldBlur,
blur,
}: Props) {
const [bodyDecodeType, setBodyDecodeType] = useState<string>(decodedBody ? 'utf8' : 'hex');
useEffect(() => {
@ -40,20 +41,30 @@ export function ContentDetailsCard({
setBodyDecodeType(value);
};
const bodyDisplay =
bodyDecodeType === 'hex'
? body
: decodedBody || tryUtf8DecodeBytes(body, false) || 'Unable to decode';
const bodyDisplay = useMemo(() => {
return (
(bodyDecodeType === 'hex'
? body
: decodedBody || tryUtf8DecodeBytes(body, false) || 'Unable to decode') || ''
);
}, [bodyDecodeType, decodedBody, body]);
const rawBytes = utils.formatMessage(
MAILBOX_VERSION,
nonce,
originDomainId,
sender,
destinationDomainId,
recipient,
body,
);
const rawBytes = useMemo(() => {
try {
return utils.formatMessage(
MAILBOX_VERSION,
nonce,
originDomainId,
sender,
destinationDomainId,
recipient,
body,
);
} catch (error) {
logger.warn('Error formatting message', error);
return '';
}
}, [nonce, originDomainId, sender, destinationDomainId, recipient, body]);
return (
<Card classes="w-full space-y-4">
@ -74,21 +85,16 @@ export function ContentDetailsCard({
display={msgId}
displayWidth="w-64 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
/>
<KeyValueRow
label="Nonce:"
labelWidth="w-16"
display={nonce.toString()}
blurValue={shouldBlur}
blurValue={blur}
/>
<KeyValueRow label="Nonce:" labelWidth="w-16" display={nonce.toString()} blurValue={blur} />
<KeyValueRow
label="Sender:"
labelWidth="w-16"
display={sender}
displayWidth="w-64 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
<KeyValueRow
label="Recipient:"
@ -96,7 +102,7 @@ export function ContentDetailsCard({
display={recipient}
displayWidth="w-64 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
</div>
<div>

@ -1,7 +1,7 @@
import BigNumber from 'bignumber.js';
import { utils } from 'ethers';
import Image from 'next/image';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { RadioButtons } from '../../../components/buttons/RadioButtons';
import { HelpIcon } from '../../../components/icons/HelpIcon';
@ -9,14 +9,16 @@ import { Card } from '../../../components/layout/Card';
import { links } from '../../../consts/links';
import FuelPump from '../../../images/icons/fuel-pump.svg';
import { Message } from '../../../types';
import { fromWei } from '../../../utils/amount';
import { BigNumberMax, fromWei } from '../../../utils/amount';
import { logger } from '../../../utils/logger';
import { GasPayment } from '../../debugger/types';
import { KeyValueRow } from './KeyValueRow';
interface Props {
message: Message;
shouldBlur: boolean;
igpPayments?: AddressTo<GasPayment[]>;
blur: boolean;
}
const unitOptions = [
@ -25,12 +27,39 @@ const unitOptions = [
{ value: 'wei', display: 'Wei' },
];
export function GasDetailsCard({ message, shouldBlur }: Props) {
export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
const [unit, setUnit] = useState(unitOptions[0].value);
const { totalGasAmount, totalPayment: totalPaymentWei, numPayments } = message;
const paymentFormatted = fromWei(totalPaymentWei, unit).toString();
const avgPrice = computeAvgGasPrice(unit, totalGasAmount, totalPaymentWei);
const { totalGasAmount, paymentFormatted, numPayments, avgPrice, paymentsWithAddr } =
useMemo(() => {
const paymentsWithAddr = Object.keys(igpPayments)
.map((contract) =>
igpPayments[contract].map((p) => ({
gasAmount: p.gasAmount,
paymentAmount: fromWei(p.paymentAmount, unit).toString(),
contract,
})),
)
.flat();
let totalGasAmount = paymentsWithAddr.reduce(
(sum, val) => sum.plus(val.gasAmount),
new BigNumber(0),
);
let totalPaymentWei = paymentsWithAddr.reduce(
(sum, val) => sum.plus(val.paymentAmount),
new BigNumber(0),
);
let numPayments = paymentsWithAddr.length;
totalGasAmount = BigNumberMax(totalGasAmount, new BigNumber(message.totalGasAmount || 0));
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);
return { totalGasAmount, paymentFormatted, numPayments, avgPrice, paymentsWithAddr };
}, [unit, message, igpPayments]);
return (
<Card classes="w-full space-y-4 relative">
@ -59,32 +88,41 @@ export function GasDetailsCard({ message, shouldBlur }: Props) {
<KeyValueRow
label="Payment count:"
labelWidth="w-28"
display={numPayments?.toString() || '0'}
blurValue={shouldBlur}
display={numPayments.toString()}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
<KeyValueRow
label="Total gas amount:"
labelWidth="w-28"
display={totalGasAmount?.toString() || '0'}
blurValue={shouldBlur}
display={totalGasAmount.toString()}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
<KeyValueRow
label="Total paid:"
labelWidth="w-28"
display={totalPaymentWei ? paymentFormatted : '0'}
blurValue={shouldBlur}
display={paymentFormatted}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
<KeyValueRow
label="Average price:"
labelWidth="w-28"
display={avgPrice ? avgPrice.formatted : '-'}
blurValue={shouldBlur}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
</div>
{!!paymentsWithAddr.length && (
<div className="md:pt-2 pb-8 md:pb-6">
<IgpPaymentsTable payments={paymentsWithAddr} />
</div>
)}
<div className="absolute right-2 bottom-2">
<RadioButtons
options={unitOptions}
@ -97,7 +135,30 @@ export function GasDetailsCard({ message, shouldBlur }: Props) {
);
}
function computeAvgGasPrice(unit: string, gasAmount?: string, payment?: string) {
function IgpPaymentsTable({ payments }: { payments: Array<GasPayment & { contract: Address }> }) {
return (
<table className="rounded border-collapse overflow-hidden">
<thead>
<tr>
<th className={style.th}>IGP Address</th>
<th className={style.th}>Gas amount</th>
<th className={style.th}>Payment</th>
</tr>
</thead>
<tbody>
{payments.map((p, i) => (
<tr key={`igp-payment-${i}`}>
<td className={style.td}>{p.contract}</td>
<td className={style.td}>{p.gasAmount}</td>
<td className={style.td}>{p.paymentAmount}</td>
</tr>
))}
</tbody>
</table>
);
}
function computeAvgGasPrice(unit: string, gasAmount?: BigNumber.Value, payment?: BigNumber.Value) {
try {
if (!gasAmount || !payment) return null;
const gasBN = new BigNumber(gasAmount);
@ -111,3 +172,8 @@ function computeAvgGasPrice(unit: string, gasAmount?: string, payment?: string)
return null;
}
}
const style = {
th: 'p-1 md:p-2 text-sm text-gray-500 font-normal text-left border border-gray-200 rounded',
td: 'p-1 md:p-2 text-xs md:text-sm text-gray-700 text-left border border-gray-200 rounded',
};

@ -11,10 +11,10 @@ import { KeyValueRow } from './KeyValueRow';
interface Props {
message: Message;
shouldBlur: boolean;
blur: boolean;
}
export function IcaDetailsCard({ message: { originDomainId, body }, shouldBlur }: Props) {
export function IcaDetailsCard({ message: { originDomainId, body }, blur }: Props) {
const decodeResult = useMemo(() => tryDecodeIcaBody(body), [body]);
const {
@ -42,7 +42,7 @@ export function IcaDetailsCard({ message: { originDomainId, body }, shouldBlur }
display={decodeResult.sender}
displayWidth="w-60 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
<KeyValueRow
label="ICA Address:"
@ -58,7 +58,7 @@ export function IcaDetailsCard({ message: { originDomainId, body }, shouldBlur }
}
displayWidth="w-60 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
{decodeResult.calls.length ? (
decodeResult.calls.map((c, i) => (
@ -73,7 +73,7 @@ export function IcaDetailsCard({ message: { originDomainId, body }, shouldBlur }
display={c.destinationAddress}
displayWidth="w-60 sm:w-80"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
<KeyValueRow
label="Raw call bytes:"
@ -81,7 +81,7 @@ export function IcaDetailsCard({ message: { originDomainId, body }, shouldBlur }
display={c.callBytes}
displayWidth="w-60 sm:w-96 lg:w-112"
showCopy={true}
blurValue={shouldBlur}
blurValue={blur}
/>
</div>
</div>

@ -1,4 +1,5 @@
import { CopyButton } from '../../../components/buttons/CopyButton';
import { isZeroish } from '../../../utils/number';
interface Props {
label: string;
@ -9,6 +10,7 @@ interface Props {
showCopy?: boolean;
blurValue?: boolean;
classes?: string;
allowZeroish?: boolean;
}
export function KeyValueRow({
@ -20,15 +22,19 @@ export function KeyValueRow({
showCopy,
blurValue,
classes,
allowZeroish = false,
}: Props) {
const useFallbackVal = isZeroish(display) && !allowZeroish;
return (
<div className={`flex items-center pl-px ${classes}`}>
<label className={`text-sm text-gray-500 ${labelWidth}`}>{label}</label>
<div className={`text-sm ml-1 truncate ${displayWidth || ''} ${blurValue && 'blur-xs'}`}>
<span>{display}</span>
{subDisplay && <span className="text-xs ml-2">{subDisplay}</span>}
<span>{!useFallbackVal ? display : 'Unknown'}</span>
{subDisplay && !useFallbackVal && <span className="text-xs ml-2">{subDisplay}</span>}
</div>
{showCopy && <CopyButton copyValue={display} width={13} height={13} classes="ml-1" />}
{showCopy && !useFallbackVal && (
<CopyButton copyValue={display} width={13} height={13} classes="ml-1" />
)}
</div>
);
}

@ -5,10 +5,10 @@ import { Message } from '../../../types';
interface Props {
message: Message;
shouldBlur?: boolean;
blur?: boolean;
}
export function TimelineCard({ message, shouldBlur }: Props) {
export function TimelineCard({ message, blur }: Props) {
const { stage, timings } = useMessageStage({ message });
return (
@ -17,7 +17,7 @@ export function TimelineCard({ message, shouldBlur }: Props) {
<h3 className="text-gray-500 font-medium text-md mr-2">Delivery Timeline</h3>
<HelpIcon size={16} text="A breakdown of the stages for delivering a message" />
</div> */}
<div className={`-mx-2 sm:mx-0 -my-2 ${shouldBlur && 'blur-xs'}`}>
<div className={`-mx-2 sm:mx-0 -my-2 ${blur && 'blur-xs'}`}>
<MessageTimeline status={message.status} stage={stage} timings={timings} />
</div>
</Card>

@ -1,3 +1,5 @@
import { PropsWithChildren, ReactNode } from 'react';
import { Spinner } from '../../../components/animation/Spinner';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { HelpIcon } from '../../../components/icons/HelpIcon';
@ -11,35 +13,109 @@ import { useMultiProvider } from '../../providers/multiProvider';
import { KeyValueRow } from './KeyValueRow';
interface TransactionCardProps {
title: string;
export function OriginTransactionCard({
chainId,
transaction,
blur,
}: {
chainId: ChainId;
status: MessageStatus;
transaction?: MessageTx;
debugInfo?: TransactionCardDebugInfo;
isPiMsg?: boolean;
helpText: string;
shouldBlur: boolean;
}
export interface TransactionCardDebugInfo {
status: MessageDebugStatus;
details: string;
transaction: MessageTx;
blur: boolean;
}) {
return (
<TransactionCard chainId={chainId} title="Origin Transaction" helpText={helpText.origin}>
<TransactionDetails chainId={chainId} transaction={transaction} blur={blur} />
</TransactionCard>
);
}
export function TransactionCard({
title,
export function DestinationTransactionCard({
chainId,
status,
transaction,
debugInfo,
isStatusFetching,
isPiMsg,
blur,
}: {
chainId: ChainId;
status: MessageStatus;
transaction?: MessageTx;
debugInfo?: {
status: MessageDebugStatus;
details: string;
};
isStatusFetching: boolean;
isPiMsg?: boolean;
blur: boolean;
}) {
let content: ReactNode;
if (transaction) {
content = <TransactionDetails chainId={chainId} transaction={transaction} blur={blur} />;
} else if (!debugInfo && isStatusFetching) {
content = (
<DeliveryStatus>
<div>Checking delivery status and inspecting message</div>
<Spinner classes="mt-4 scale-75" />
</DeliveryStatus>
);
} else if (status === MessageStatus.Failing) {
content = (
<DeliveryStatus>
<div className="text-gray-700">Delivery to destination chain is currently failing</div>
{debugInfo && (
<>
<div className="mt-4 text-gray-700 text-center">
{debugStatusToDesc[debugInfo.status]}
</div>
<div className="mt-4 text-gray-700 text-sm max-w-sm text-center break-words">
{debugInfo.details}
</div>
</>
)}
</DeliveryStatus>
);
} else if (status === MessageStatus.Pending) {
content = (
<DeliveryStatus>
<div>Delivery to destination chain still in progress.</div>
{isPiMsg && (
<div className="mt-2 text-gray-700 text-sm max-w-xs">
Please ensure a relayer is running for this chain.
</div>
)}
<Spinner classes="mt-4 scale-75" />
</DeliveryStatus>
);
} else {
content = (
<DeliveryStatus>
<div className="text-gray-700">{`Delivery to status is currently unknown. ${
isPiMsg
? 'Please ensure your chain config is correct and check back later.'
: 'Please check again later'
}`}</div>
</DeliveryStatus>
);
}
return (
<TransactionCard
chainId={chainId}
title="Destination Transaction"
helpText={helpText.destination}
>
{content}
</TransactionCard>
);
}
function TransactionCard({
chainId,
title,
helpText,
shouldBlur,
}: TransactionCardProps) {
const multiProvider = useMultiProvider();
const hash = transaction?.hash;
const txExplorerLink = hash ? multiProvider.tryGetExplorerTxUrl(chainId, { hash }) : null;
children,
}: PropsWithChildren<{ chainId: ChainId; title: string; helpText: string }>) {
return (
<Card classes="flex-1 min-w-fit space-y-3">
<div className="flex items-center justify-between">
@ -51,88 +127,87 @@ export function TransactionCard({
<HelpIcon size={16} text={helpText} />
</div>
</div>
{transaction && (
<>
<KeyValueRow
label="Chain:"
labelWidth="w-16"
display={`${getChainDisplayName(multiProvider, chainId)} (${chainId})`}
displayWidth="w-60 sm:w-64"
blurValue={shouldBlur}
/>
<KeyValueRow
label="Tx hash:"
labelWidth="w-16"
display={transaction.hash}
displayWidth="w-60 sm:w-64"
showCopy={true}
blurValue={shouldBlur}
/>
<KeyValueRow
label="From:"
labelWidth="w-16"
display={transaction.from}
displayWidth="w-60 sm:w-64"
showCopy={true}
blurValue={shouldBlur}
/>
<KeyValueRow
label="Time:"
labelWidth="w-16"
display={getHumanReadableTimeString(transaction.timestamp)}
subDisplay={`(${getDateTimeString(transaction.timestamp)})`}
displayWidth="w-60 sm:w-64"
blurValue={shouldBlur}
/>
<KeyValueRow
label="Block:"
labelWidth="w-16"
display={transaction.blockNumber.toString()}
displayWidth="w-60 sm:w-64"
blurValue={shouldBlur}
/>
{txExplorerLink && (
<a
className="block text-sm text-gray-500 pl-px underline"
href={txExplorerLink}
target="_blank"
rel="noopener noreferrer"
>
View in block explorer
</a>
)}
</>
)}
{!transaction && status === MessageStatus.Failing && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-700 text-center">
Delivery to destination chain is currently failing
</div>
{debugInfo && (
<>
<div className="mt-4 text-gray-700 text-center">
{debugStatusToDesc[debugInfo.status]}
</div>
<div className="mt-4 text-gray-700 text-sm max-w-sm text-center break-words">
{debugInfo.details}
</div>
</>
)}
</div>
)}
{!transaction && (status === MessageStatus.Pending || status === MessageStatus.Unknown) && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-500 text-center max-w-xs">
Delivery to destination chain still in progress.
</div>
{isPiMsg && (
<div className="mt-2 text-gray-500 text-center text-sm max-w-xs">
Please ensure a relayer is running for this chain.
</div>
)}
<Spinner classes="mt-4 scale-75" />
</div>
)}
{children}
</Card>
);
}
function TransactionDetails({
chainId,
transaction,
blur,
}: {
chainId: ChainId;
transaction: MessageTx;
blur: boolean;
}) {
const { hash, from, timestamp, blockNumber } = transaction;
const multiProvider = useMultiProvider();
const txExplorerLink = hash ? multiProvider.tryGetExplorerTxUrl(chainId, { hash }) : null;
return (
<>
<KeyValueRow
label="Chain:"
labelWidth="w-16"
display={`${getChainDisplayName(multiProvider, chainId)} (${chainId})`}
displayWidth="w-60 sm:w-64"
blurValue={blur}
/>
<KeyValueRow
label="Tx hash:"
labelWidth="w-16"
display={hash}
displayWidth="w-60 sm:w-64"
showCopy={true}
blurValue={blur}
/>
<KeyValueRow
label="From:"
labelWidth="w-16"
display={from}
displayWidth="w-60 sm:w-64"
showCopy={true}
blurValue={blur}
/>
<KeyValueRow
label="Time:"
labelWidth="w-16"
display={getHumanReadableTimeString(timestamp)}
subDisplay={`(${getDateTimeString(timestamp)})`}
displayWidth="w-60 sm:w-64"
blurValue={blur}
/>
<KeyValueRow
label="Block:"
labelWidth="w-16"
display={blockNumber?.toString()}
displayWidth="w-60 sm:w-64"
blurValue={blur}
/>
{txExplorerLink && (
<a
className="block text-sm text-gray-500 pl-px underline"
href={txExplorerLink}
target="_blank"
rel="noopener noreferrer"
>
View in block explorer
</a>
)}
</>
);
}
function DeliveryStatus({ children }: PropsWithChildren<unknown>) {
return (
<div className="py-5 flex flex-col items-center text-gray-500 text-center">
<div className="max-w-xs">{children}</div>
</div>
);
}
const helpText = {
origin: 'Info about the transaction that initiated the message placement into the outbox.',
destination:
'Info about the transaction that triggered the delivery of the message from an inbox.',
};

@ -2,14 +2,12 @@ import { constants } from 'ethers';
import { Message, MessageStatus, MessageTx } from '../../types';
export const TX_HASH_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const TX_ZERO: MessageTx = {
timestamp: Date.now(),
hash: TX_HASH_ZERO,
hash: constants.HashZero,
from: constants.AddressZero,
to: constants.AddressZero,
blockHash: TX_HASH_ZERO,
blockHash: constants.HashZero,
blockNumber: 123456789,
mailbox: constants.AddressZero,
nonce: 0,
@ -27,7 +25,7 @@ const BODY_ZERO =
export const PLACEHOLDER_MESSAGE: Message = {
id: '1',
msgId: TX_HASH_ZERO,
msgId: constants.HashZero,
nonce: 1,
status: MessageStatus.Pending,
sender: constants.AddressZero,

@ -3,6 +3,7 @@ import { MultiProvider } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus, MessageStub } from '../../../types';
import { logger } from '../../../utils/logger';
import { tryUtf8DecodeBytes } from '../../../utils/string';
import { isPiChain } from '../../chains/utils';
import { postgresByteaToString } from './encoding';
import {
@ -42,12 +43,14 @@ export function parseMessageQueryResult(
function parseMessageStub(multiProvider: MultiProvider, m: MessageStubEntry): MessageStub | null {
try {
const destinationDomainId = m.destination_domain_id;
const destinationChainId =
let destinationChainId =
m.destination_chain_id || multiProvider.tryGetChainId(destinationDomainId);
if (!destinationChainId) {
logger.warn(`No dest chain id known for domain ${destinationDomainId}. Skipping message.`);
return null;
logger.warn(`No chainId known for domain ${destinationDomainId}. Using domain as chainId`);
destinationChainId = destinationDomainId;
}
const isPiMsg = isPiChain(m.origin_chain_id) || isPiChain(destinationChainId);
return {
status: getMessageStatus(m),
id: m.id.toString(),
@ -71,6 +74,7 @@ function parseMessageStub(multiProvider: MultiProvider, m: MessageStubEntry): Me
from: postgresByteaToString(m.destination_tx_sender!),
}
: undefined,
isPiMsg,
};
} catch (error) {
logger.error('Error parsing message stub', error);

1
src/global.d.ts vendored

@ -1,3 +1,4 @@
declare type Address = string;
declare type ChainId = number;
declare type DomainId = number;
declare type AddressTo<T> = Record<Address, T>;

@ -2,10 +2,10 @@ import { BigNumber } from 'ethers';
import type { NextApiRequest, NextApiResponse } from 'next';
import NextCors from 'nextjs-cors';
import { MultiProvider, chainIdToMetadata } from '@hyperlane-xyz/sdk';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { Environment } from '../../consts/environments';
import { getChainEnvironment } from '../../features/chains/utils';
import { getChainEnvironment, isPiChain } from '../../features/chains/utils';
import { logger } from '../../utils/logger';
import { fetchWithTimeout } from '../../utils/timeout';
@ -22,7 +22,7 @@ export default async function handler(
const body = req.body as { chainId: ChainId };
if (!body.chainId) throw new Error('No chainId in body');
// TODO PI support here
if (!chainIdToMetadata[body.chainId]) throw new Error('ChainId is unsupported');
if (isPiChain(body.chainId)) throw new Error('ChainId is unsupported');
const multiProvider = new MultiProvider();
const nonce = await fetchLatestNonce(multiProvider, body.chainId);
res.status(200).json({ nonce });

@ -69,3 +69,10 @@ export function areAmountsNearlyEqual(amountInWei1: BigNumber, amountInWei2: Num
// 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,4 +1,4 @@
import { BigNumber, BigNumberish } from 'ethers';
import { BigNumber, BigNumberish, constants } from 'ethers';
import { logger } from './logger';
import { isNullish } from './typeof';
@ -26,3 +26,13 @@ export function isBigNumberish(value: any): value is BigNumberish {
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,5 +1,7 @@
// Inspired by https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
export function getHumanReadableTimeString(timestamp: number) {
if (timestamp <= 0) return '';
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds <= 1) {

Loading…
Cancel
Save