Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: decode warp route information #176

Merged
merged 15 commits into from
Feb 5, 2025
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"author": "J M Rossy",
"dependencies": {
"@headlessui/react": "^2.2.0",
"@hyperlane-xyz/registry": "7.1.0",
"@hyperlane-xyz/sdk": "8.4.0",
"@hyperlane-xyz/utils": "8.4.0",
"@hyperlane-xyz/widgets": "8.4.0",
"@hyperlane-xyz/registry": "9.0.0",
"@hyperlane-xyz/sdk": "8.5.0",
"@hyperlane-xyz/utils": "8.5.0",
"@hyperlane-xyz/widgets": "8.5.0",
"@tanstack/react-query": "^5.62.3",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
Expand Down
9 changes: 4 additions & 5 deletions src/features/messages/MessageDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils';
import { SpinnerIcon } from '@hyperlane-xyz/widgets';
import Image from 'next/image';
import { useEffect } from 'react';
import { toast } from 'react-toastify';

import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils';

import { Card } from '../../components/layout/Card';
import CheckmarkIcon from '../../images/icons/checkmark-circle.svg';
import { useMultiProvider, useStore } from '../../store';
Expand All @@ -12,14 +11,13 @@ import { logger } from '../../utils/logger';
import { getHumanReadableDuration } from '../../utils/time';
import { getChainDisplayName, isEvmChain } from '../chains/utils';
import { useMessageDeliveryStatus } from '../deliveryStatus/useMessageDeliveryStatus';

import { SpinnerIcon } from '@hyperlane-xyz/widgets';
import { ContentDetailsCard } from './cards/ContentDetailsCard';
import { GasDetailsCard } from './cards/GasDetailsCard';
import { IcaDetailsCard } from './cards/IcaDetailsCard';
import { IsmDetailsCard } from './cards/IsmDetailsCard';
import { TimelineCard } from './cards/TimelineCard';
import { DestinationTransactionCard, OriginTransactionCard } from './cards/TransactionCard';
import { WarpTransferDetailsCard } from './cards/WarpTransferDetailsCard';
import { useIsIcaMessage } from './ica';
import { usePiChainMessageQuery } from './pi-queries/usePiChainMessageQuery';
import { PLACEHOLDER_MESSAGE } from './placeholderMessages';
Expand Down Expand Up @@ -126,6 +124,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
blur={blur}
/>
{showTimeline && <TimelineCard message={message} blur={blur} />}
<WarpTransferDetailsCard message={message} blur={blur} />
<ContentDetailsCard message={message} blur={blur} />
<GasDetailsCard
message={message}
Expand Down
7 changes: 2 additions & 5 deletions src/features/messages/cards/ContentDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react';

import { MAILBOX_VERSION } from '@hyperlane-xyz/sdk';
import { formatMessage } from '@hyperlane-xyz/utils';
import { SelectField, Tooltip } from '@hyperlane-xyz/widgets';

import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react';
import { Card } from '../../../components/layout/Card';
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';
import { KeyValueRow } from './KeyValueRow';

Expand Down
6 changes: 2 additions & 4 deletions src/features/messages/cards/GasDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { BigNumberMax, fromWei, toTitleCase } from '@hyperlane-xyz/utils';
import { Tooltip } from '@hyperlane-xyz/widgets';
import BigNumber from 'bignumber.js';
import { utils } from 'ethers';
import Image from 'next/image';
import { useMemo, useState } from 'react';

import { BigNumberMax, fromWei, toTitleCase } from '@hyperlane-xyz/utils';
import { Tooltip } from '@hyperlane-xyz/widgets';

import { RadioButtons } from '../../../components/buttons/RadioButtons';
import { Card } from '../../../components/layout/Card';
import { docLinks } from '../../../consts/links';
Expand Down
4 changes: 1 addition & 3 deletions src/features/messages/cards/IcaDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Tooltip } from '@hyperlane-xyz/widgets';
import Image from 'next/image';
import { useMemo } from 'react';

import { Tooltip } from '@hyperlane-xyz/widgets';

import { Card } from '../../../components/layout/Card';
import AccountStar from '../../../images/icons/account-star.svg';
import { Message } from '../../../types';
Expand Down
4 changes: 1 addition & 3 deletions src/features/messages/cards/IsmDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import Image from 'next/image';

import { isNullish } from '@hyperlane-xyz/utils';
import { Tooltip } from '@hyperlane-xyz/widgets';

import Image from 'next/image';
import { Card } from '../../../components/layout/Card';
import { docLinks } from '../../../consts/links';
import ShieldLock from '../../../images/icons/shield-lock.svg';
Expand Down
1 change: 0 additions & 1 deletion src/features/messages/cards/TimelineCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { MessageTimeline, useMessageStage } from '@hyperlane-xyz/widgets';

import { Card } from '../../../components/layout/Card';
import { Message } from '../../../types';

Expand Down
7 changes: 2 additions & 5 deletions src/features/messages/cards/TransactionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import BigNumber from 'bignumber.js';
import { PropsWithChildren, ReactNode, useState } from 'react';

import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType, isAddress, isZeroish, strip0x } from '@hyperlane-xyz/utils';
import { Modal, SpinnerIcon, Tooltip, useModal } from '@hyperlane-xyz/widgets';

import BigNumber from 'bignumber.js';
import { PropsWithChildren, ReactNode, useState } from 'react';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { Card } from '../../../components/layout/Card';
import { links } from '../../../consts/links';
Expand All @@ -15,7 +13,6 @@ import { ChainSearchModal } from '../../chains/ChainSearchModal';
import { getChainDisplayName, isEvmChain } from '../../chains/utils';
import { debugStatusToDesc } from '../../debugger/strings';
import { MessageDebugResult } from '../../debugger/types';

import { LabelAndCodeBlock } from './CodeBlock';
import { KeyValueRow } from './KeyValueRow';

Expand Down
144 changes: 144 additions & 0 deletions src/features/messages/cards/WarpTransferDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { MultiProvider } from '@hyperlane-xyz/sdk';
import {
bytesToProtocolAddress,
fromHexString,
fromWei,
parseWarpRouteMessage,
} from '@hyperlane-xyz/utils';
import { Tooltip } from '@hyperlane-xyz/widgets';
import Image from 'next/image';
import { Card } from '../../../components/layout/Card';
import SendMoney from '../../../images/icons/send-money.svg';
import { useMultiProvider, useStore } from '../../../store';
import { Message, WarpRouteChainAddressMap, WarpRouteDetails } from '../../../types';
import { logger } from '../../../utils/logger';
import { getTokenFromWarpRouteChainAddressMap } from '../../../utils/token';
import { KeyValueRow } from './KeyValueRow';

interface Props {
message: Message;
blur: boolean;
}

export function WarpTransferDetailsCard({ message, blur }: Props) {
const multiProvider = useMultiProvider();
const { warpRouteChainAddressMap } = useStore((s) => ({
warpRouteChainAddressMap: s.warpRouteChainAddressMap,
}));
const warpRouteDetails = parseWarpRouteDetails(message, warpRouteChainAddressMap, multiProvider);

if (!warpRouteDetails) return null;

const {
amount,
destinationTokenAddress,
transferRecipient,
originTokenAddress,
originTokenSymbol,
} = warpRouteDetails;

return (
<Card className="w-full space-y-4">
<div className="flex items-center justify-between">
<Image src={SendMoney} width={28} height={28} alt="" className="opacity-80" />
<div className="flex items-center pb-1">
<h3 className="mr-2 text-md font-medium text-blue-500">Warp Transfer Details</h3>
<Tooltip
id="warp-route-info"
content="Information about the warp route transfer such as the end recipient and amount transferred"
/>
</div>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-4">
<KeyValueRow
label="Amount:"
labelWidth="w-20 sm:w-32"
display={`${amount} ${originTokenSymbol}`}
displayWidth="w-64 sm:w-96"
blurValue={blur}
showCopy
/>
<KeyValueRow
label="Origin token:"
labelWidth="w-20 sm:w-32"
display={originTokenAddress}
displayWidth="w-64 sm:w-96"
showCopy={true}
blurValue={blur}
/>
<KeyValueRow
label="Destination token:"
labelWidth="w-20 sm:w-32"
display={destinationTokenAddress}
displayWidth="w-64 sm:w-96"
showCopy={true}
blurValue={blur}
/>
<KeyValueRow
label="Transfer recipient:"
labelWidth="w-20 sm:w-32"
display={transferRecipient}
displayWidth="w-64 sm:w-96"
blurValue={blur}
showCopy
/>
</div>
</Card>
);
}

export function parseWarpRouteDetails(
message: Message,
warpRouteChainAddressMap: WarpRouteChainAddressMap,
multiProvider: MultiProvider,
): WarpRouteDetails | undefined {
try {
const {
body,
origin: { to },
originDomainId,
destinationDomainId,
recipient,
} = message;

const originMetadata = multiProvider.tryGetChainMetadata(originDomainId);
const destinationMetadata = multiProvider.tryGetChainMetadata(destinationDomainId);

if (!body || !originMetadata || !destinationMetadata) return undefined;

const originToken = getTokenFromWarpRouteChainAddressMap(
originMetadata,
to,
warpRouteChainAddressMap,
);
const destinationToken = getTokenFromWarpRouteChainAddressMap(
destinationMetadata,
recipient,
warpRouteChainAddressMap,
);

// If tokens are not found with the addresses, it means the message
// is not a warp transfer between tokens known to the registry
if (!originToken || !destinationToken) return undefined;

const parsedMessage = parseWarpRouteMessage(body);
const bytes = fromHexString(parsedMessage.recipient);
const address = bytesToProtocolAddress(
bytes,
destinationMetadata.protocol,
destinationMetadata.bech32Prefix,
);

return {
amount: fromWei(parsedMessage.amount.toString(), originToken.decimals || 18),
transferRecipient: address,
originTokenAddress: to,
originTokenSymbol: originToken.symbol,
destinationTokenAddress: recipient,
destinationTokenSymbol: destinationToken.symbol,
};
} catch (err) {
logger.error('Error parsing warp route details:', err);
return undefined;
}
}
4 changes: 1 addition & 3 deletions src/features/messages/queries/parse.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { MultiProvider } from '@hyperlane-xyz/sdk';

import { Message, MessageStatus, MessageStub } from '../../../types';
import { logger } from '../../../utils/logger';
import { tryUtf8DecodeBytes } from '../../../utils/string';
import { DomainsEntry } from '../../chains/queries/fragments';
import { isPiChain } from '../../chains/utils';

import { postgresByteaToAddress, postgresByteaToString, postgresByteaToTxHash } from './encoding';
import {
MessageEntry,
MessageStubEntry,
MessagesQueryResult,
MessagesStubQueryResult,
MessageStubEntry,
} from './fragments';

/**
Expand Down
1 change: 1 addition & 0 deletions src/images/icons/send-money.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 23 additions & 5 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry';
import { GithubRegistry, IRegistry, warpRouteConfigs } from '@hyperlane-xyz/registry';
import { ChainMap, ChainMetadata, MultiProvider, mergeChainMetadataMap } from '@hyperlane-xyz/sdk';
import { objFilter, objMap, promiseObjAll } from '@hyperlane-xyz/utils';

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { config } from './consts/config';
import { DomainsEntry } from './features/chains/queries/fragments';
import { WarpRouteChainAddressMap } from './types';
import { logger } from './utils/logger';

// Increment this when persist state has breaking changes
Expand All @@ -27,6 +26,8 @@ interface AppState {
setRegistry: (registry: IRegistry) => void;
bannerClassName: string;
setBanner: (className: string) => void;
warpRouteChainAddressMap: WarpRouteChainAddressMap;
setWarpRoutChainAddresseMap: (warpRouteChainAddressMap: WarpRouteChainAddressMap) => void;
}

export const useStore = create<AppState>()(
Expand Down Expand Up @@ -56,6 +57,10 @@ export const useStore = create<AppState>()(
},
bannerClassName: '',
setBanner: (className: string) => set({ bannerClassName: className }),
warpRouteChainAddressMap: {},
setWarpRoutChainAddresseMap: (warpRouteChainAddressMap: WarpRouteChainAddressMap) => {
set({ warpRouteChainAddressMap });
},
}),
{
name: 'hyperlane', // name in storage
Expand All @@ -75,6 +80,7 @@ export const useStore = create<AppState>()(
logger.debug('Rehydration complete');
})
.catch((e) => logger.error('Error building MultiProvider', e));
state.setWarpRoutChainAddresseMap(buildWarpRouteChainAddressMap());
};
},
},
Expand Down Expand Up @@ -118,3 +124,15 @@ async function buildMultiProvider(
const mergedMetadata = mergeChainMetadataMap(metadataWithLogos, overrideChainMetadata);
return { metadata: metadataWithLogos, multiProvider: new MultiProvider(mergedMetadata) };
}

// TODO: Get the most up to date data from the registry instead of using the warpRouteConfigs
export function buildWarpRouteChainAddressMap(): WarpRouteChainAddressMap {
return Object.values(warpRouteConfigs).reduce((acc, { tokens }) => {
tokens.forEach((token) => {
const { chainName, addressOrDenom } = token;
acc[chainName] ||= {};
acc[chainName][addressOrDenom] = token;
});
return acc;
}, {});
}
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChainMap, TokenArgs } from '@hyperlane-xyz/sdk';
import type { providers } from 'ethers';

// TODO consider reconciling with SDK's MessageStatus
Expand Down Expand Up @@ -60,3 +61,14 @@ export interface ExtendedLog extends providers.Log {
from?: Address;
to?: Address;
}

export interface WarpRouteDetails {
amount: string;
transferRecipient: string;
originTokenAddress: string;
originTokenSymbol: string;
destinationTokenAddress: string;
destinationTokenSymbol: string;
}

export type WarpRouteChainAddressMap = ChainMap<Record<Address, TokenArgs>>;
19 changes: 19 additions & 0 deletions src/utils/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { objKeys } from '@hyperlane-xyz/utils';
import { WarpRouteChainAddressMap } from '../types';

export function getTokenFromWarpRouteChainAddressMap(
chainMetadata: ChainMetadata,
address: Address,
warpRouteChainAddressMap: WarpRouteChainAddressMap,
) {
const { name } = chainMetadata;
if (objKeys(warpRouteChainAddressMap).includes(name)) {
const chain = warpRouteChainAddressMap[name];
if (objKeys(chain).includes(address)) {
return chain[address];
}
}

return undefined;
}
Loading