Skip to content

Commit

Permalink
feat: decode warp route information (#176)
Browse files Browse the repository at this point in the history
Leverage #144, to implement this:

Decodes warp route message to show more information about the warp token
transfer
Fixes #78 

- Update the store to include a WarpRouteChainAddressMap, this is a
custom type that is of `ChainMap<Record<Address, TokenArgs>>;`, the last
string would be the token symbol
- Create `parseWarpRouteDetails()` function to decode the information
about the warp transfer, a few checks are done to identify if the
message is an actual warp route transfer
- Upgrade `hyperlane` packages


![image](https://github.com/user-attachments/assets/694019d8-8425-4540-a092-c1a23c8846e1)

![image](https://github.com/user-attachments/assets/24db2578-64d7-41b7-85a5-e216f395c6dd)
  • Loading branch information
Xaroz authored Feb 5, 2025
1 parent 1f49cb8 commit 28880e8
Show file tree
Hide file tree
Showing 15 changed files with 245 additions and 67 deletions.
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

0 comments on commit 28880e8

Please sign in to comment.