Surface IGP payments in table

pull/40/head
J M Rossy 2 years ago
parent d40eb028aa
commit 648f02a21f
  1. 5
      src/features/chains/utils.ts
  2. 107
      src/features/debugger/debugMessage.ts
  3. 1
      src/features/debugger/strings.ts
  4. 10
      src/features/debugger/types.ts
  5. 31
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  6. 6
      src/features/deliveryStatus/types.ts
  7. 9
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  8. 6
      src/features/messages/MessageDetails.tsx
  9. 86
      src/features/messages/cards/GasDetailsCard.tsx
  10. 10
      src/features/messages/cards/KeyValueRow.tsx
  11. 4
      src/features/messages/queries/parse.ts
  12. 1
      src/global.d.ts
  13. 6
      src/pages/api/latest-nonce.ts
  14. 7
      src/utils/amount.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,6 +1,6 @@
// 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, utils as ethersUtils, providers } from 'ethers';
import { BigNumber, providers } from 'ethers';
import { IMessageRecipient__factory, InterchainGasPaymaster__factory } from '@hyperlane-xyz/core';
import type { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
@ -12,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;
@ -37,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);
@ -59,22 +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;
logger.debug(`No errors found debugging message id: ${msgId}`);
return noErrorFound();
return { ...noErrorFound(), gasDetails: gasCheckResult.gasDetails };
}
async function isInvalidRecipient(provider: Provider, recipient: Address) {
@ -149,78 +142,56 @@ 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 { isFunded, igpDetails } = await tryCheckIgpGasFunded(
originProvider,
msgId,
deliveryGasEst,
totalGasAmount,
);
if (!isFunded) {
return {
status: MessageDebugStatus.GasUnderfunded,
details: igpDetails,
};
}
return false;
}
async function tryCheckIgpGasFunded(
originProvider: Provider,
messageId: string,
deliveryGasEst?: string,
originProvider: Provider,
deliveryGasEstimate?: string,
totalGasAmount?: string,
) {
try {
if (!deliveryGasEst) throw new Error('No gas estimate provided');
if (!deliveryGasEstimate) throw new Error('No gas estimate provided');
let gasAlreadyFunded = BigNumber.from(0);
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 { contractAddrToTotalGas, numPayments, numIGPs } = await fetchGasPaymentEvents(
originProvider,
messageId,
);
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(contractAddrToTotalGas)[0];
gasAlreadyFunded = Object.values(contractToTotalGas)[0];
} else if (numIGPs > 1) {
logger.warn(`>1 IGPs paid for msg ${messageId}. Unsure which to use, skipping check.`);
return {
isFunded: true,
igpDetails: '',
};
return { gasDetails };
}
}
logger.debug('Amount of gas paid for to IGP:', gasAlreadyFunded.toString());
logger.debug('Approximate 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 {};
}
}
@ -229,24 +200,28 @@ async function fetchGasPaymentEvents(provider: Provider, messageId: string) {
const paymentFragment = igpInterface.getEvent('GasPayment');
const paymentTopics = igpInterface.encodeFilterTopics(paymentFragment.name, [messageId]);
const paymentLogs = (await provider.getLogs({ topics: paymentTopics })) || [];
const contractAddrToEvents: Record<Address, ethersUtils.LogDescription[]> = {};
const contractAddrToTotalGas: Record<Address, BigNumber> = {};
const contractToPayments: AddressTo<GasPayment[]> = {};
const contractToTotalGas: AddressTo<BigNumber> = {};
let numPayments = 0;
for (const log of paymentLogs) {
const contractAddr = log.address;
const events = contractAddrToEvents[contractAddr] || [];
const total = contractAddrToTotalGas[contractAddr] || BigNumber.from(0);
const payments = contractToPayments[contractAddr] || [];
const total = contractToTotalGas[contractAddr] || BigNumber.from(0);
try {
const newEvent = igpInterface.parseLog(log);
contractAddrToEvents[contractAddr] = [...events, newEvent];
contractAddrToTotalGas[contractAddr] = total.add(newEvent.args.gasAmount);
const newPayment = {
gasAmount: BigNumber.from(newEvent.args.gasAmount).toString(),
paymentAmount: BigNumber.from(newEvent.args.payment).toString(),
};
contractToPayments[contractAddr] = [...payments, newPayment];
contractToTotalGas[contractAddr] = total.add(newEvent.args.gasAmount);
numPayments += 1;
} catch (error) {
logger.warn('Error parsing gas payment log', error);
}
}
const numIGPs = Object.keys(contractAddrToEvents).length;
return { contractAddrToEvents, contractAddrToTotalGas, numPayments, numIGPs };
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,5 +1,4 @@
export enum MessageDebugStatus {
AlreadyProcessed = 'alreadyProcessed',
NoErrorsFound = 'noErrorsFound',
RecipientNotContract = 'recipientNotContract',
RecipientNotHandler = 'recipientNotHandler',
@ -11,4 +10,13 @@ export enum MessageDebugStatus {
export interface MessageDebugDetails {
status: MessageDebugStatus;
details: string;
gasDetails?: {
deliveryGasEstimate?: string;
contractToPayments?: AddressTo<GasPayment[]>;
};
}
export interface GasPayment {
gasAmount: string;
paymentAmount: string;
}

@ -13,6 +13,7 @@ import { MessageDebugStatus } from '../debugger/types';
import {
MessageDeliveryFailingResult,
MessageDeliveryPendingResult,
MessageDeliveryStatusResponse,
MessageDeliverySuccessResult,
} from './types';
@ -60,20 +61,22 @@ export async function fetchDeliveryStatus(
};
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;
}
}

@ -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 =

@ -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,19 +63,21 @@ 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, isDeliveryStatusFetching: isFetching };

@ -110,7 +110,11 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
/>
{!message.isPiMsg && <TimelineCard message={message} blur={blur} />}
<ContentDetailsCard message={message} blur={blur} />
<GasDetailsCard message={message} blur={blur} />
<GasDetailsCard
message={message}
igpPayments={debugInfo?.gasDetails?.contractToPayments}
blur={blur}
/>
{isIcaMsg && <IcaDetailsCard message={message} blur={blur} />}
</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,13 +9,15 @@ 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;
igpPayments?: AddressTo<GasPayment[]>;
blur: boolean;
}
@ -25,12 +27,39 @@ const unitOptions = [
{ value: 'wei', display: 'Wei' },
];
export function GasDetailsCard({ message, blur }: 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,21 +88,24 @@ export function GasDetailsCard({ message, blur }: Props) {
<KeyValueRow
label="Payment count:"
labelWidth="w-28"
display={numPayments?.toString() || '0'}
display={numPayments.toString()}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
<KeyValueRow
label="Total gas amount:"
labelWidth="w-28"
display={totalGasAmount?.toString() || '0'}
display={totalGasAmount.toString()}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
<KeyValueRow
label="Total paid:"
labelWidth="w-28"
display={totalPaymentWei ? paymentFormatted : '0'}
display={paymentFormatted}
allowZeroish={true}
blurValue={blur}
classes="basis-5/12"
/>
@ -81,10 +113,16 @@ export function GasDetailsCard({ message, blur }: Props) {
label="Average price:"
labelWidth="w-28"
display={avgPrice ? avgPrice.formatted : '-'}
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, blur }: 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',
};

@ -10,6 +10,7 @@ interface Props {
showCopy?: boolean;
blurValue?: boolean;
classes?: string;
allowZeroish?: boolean;
}
export function KeyValueRow({
@ -21,16 +22,17 @@ export function KeyValueRow({
showCopy,
blurValue,
classes,
allowZeroish = false,
}: Props) {
const isValueZeroish = isZeroish(display);
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>{!isValueZeroish ? display : 'Unknown'}</span>
{subDisplay && !isValueZeroish && <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 && !isValueZeroish && (
{showCopy && !useFallbackVal && (
<CopyButton copyValue={display} width={13} height={13} classes="ml-1" />
)}
</div>

@ -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 {
@ -48,6 +49,8 @@ function parseMessageStub(multiProvider: MultiProvider, m: MessageStubEntry): Me
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;
}

Loading…
Cancel
Save