Skip to content

Commit a9e7399

Browse files
committed
decode nested ICA multicalls
1 parent abff71b commit a9e7399

3 files changed

Lines changed: 494 additions & 58 deletions

File tree

src/features/messages/cards/IcaDetailsCard.tsx

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ import { tryGetBlockExplorerAddressUrl } from '../../../utils/url';
1414
import { MessageDebugResult } from '../../debugger/types';
1515
import {
1616
decodeIcaBody,
17+
decodeIcaCallData,
18+
decodeMulticallIcaCalls,
1719
IcaMessageType,
1820
useCcipReadIsmUrls,
1921
useIcaAddress,
2022
useRelatedIcaMessage,
2123
useRevealCalls,
2224
} from '../ica';
25+
import { CollapsibleLabelAndCodeBlock } from './CodeBlock';
2326
import { KeyValueRow } from './KeyValueRow';
2427

28+
interface DisplayIcaCall extends IcaCall {
29+
nestedCalls?: IcaCall[];
30+
}
31+
2532
/**
2633
* Check if a bytes32 salt contains an address (first 12 bytes are zeros, last 20 bytes are non-zero).
2734
* This is surfaced as User only for ICA user-salt mode, where the router encodes the EOA in bytes12(0) + address.
@@ -138,6 +145,10 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
138145
const originChainName = chainMetadataResolver.tryGetChainName(originDomainId) || undefined;
139146
const destinationChainName =
140147
chainMetadataResolver.tryGetChainName(destinationDomainId) || undefined;
148+
const tryGetChainName = useCallback(
149+
(domainId: number) => chainMetadataResolver.tryGetChainName(domainId) || undefined,
150+
[chainMetadataResolver],
151+
);
141152
const destinationNativeDecimals =
142153
chainMetadataResolver.tryGetChainMetadata(destinationDomainId)?.nativeToken?.decimals ?? 18;
143154

@@ -207,14 +218,20 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
207218

208219
// Combine calls from message body (CALLS type) or from reveal metadata (REVEAL type)
209220
const displayCalls = useMemo(() => {
221+
const attachNestedCalls = (calls: IcaCall[]): DisplayIcaCall[] =>
222+
calls.map((call) => ({
223+
...call,
224+
nestedCalls: decodeMulticallIcaCalls(call, destinationChainName) ?? undefined,
225+
}));
226+
210227
if (decodeResult?.messageType === IcaMessageType.CALLS) {
211-
return decodeResult.calls;
228+
return attachNestedCalls(decodeResult.calls);
212229
}
213230
if (decodeResult?.messageType === IcaMessageType.REVEAL && revealCalls) {
214-
return revealCalls;
231+
return attachNestedCalls(revealCalls);
215232
}
216233
return [];
217-
}, [decodeResult, revealCalls]);
234+
}, [decodeResult, revealCalls, destinationChainName]);
218235

219236
// Get the failed call index from debug result (-1 if no failure or not available)
220237
const failedCallIndex = debugResult?.icaDetails?.failedCallIndex ?? -1;
@@ -237,6 +254,19 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
237254
),
238255
] as const,
239256
),
257+
...displayCalls.flatMap((call, i) =>
258+
(call.nestedCalls ?? []).map(
259+
async (nestedCall, nestedIndex) =>
260+
[
261+
`call-${i}-${nestedIndex}`,
262+
await tryGetBlockExplorerAddressUrl(
263+
chainMetadataResolver,
264+
destinationDomainId,
265+
nestedCall.to,
266+
),
267+
] as const,
268+
),
269+
),
240270
displayOwner
241271
? ([
242272
'owner',
@@ -617,8 +647,10 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
617647
index={i}
618648
total={displayCalls.length}
619649
explorerUrl={explorerUrls[`call-${i}`]}
650+
nestedExplorerUrls={explorerUrls}
620651
blur={blur}
621652
failedCallIndex={failedCallIndex}
653+
tryGetChainName={tryGetChainName}
622654
nativeDecimals={destinationNativeDecimals}
623655
/>
624656
))}
@@ -649,18 +681,26 @@ function IcaCallDetails({
649681
index,
650682
total,
651683
explorerUrl,
684+
nestedExplorerUrls,
652685
blur,
653686
failedCallIndex,
687+
tryGetChainName,
654688
nativeDecimals,
655689
}: {
656-
call: IcaCall;
690+
call: DisplayIcaCall;
657691
index: number;
658692
total: number;
659693
explorerUrl: string | null | undefined;
694+
nestedExplorerUrls: Record<string, string | null>;
660695
blur: boolean;
661696
failedCallIndex: number;
697+
tryGetChainName: (domainId: number) => string | undefined;
662698
nativeDecimals: number;
663699
}) {
700+
const decodedCallData = useMemo(
701+
() => decodeIcaCallData(call.data, tryGetChainName),
702+
[call.data, tryGetChainName],
703+
);
664704
const { hasValue, formattedValue } = useMemo(
665705
() => getFormattedCallValue(call.value, nativeDecimals),
666706
[call.value, nativeDecimals],
@@ -671,14 +711,17 @@ function IcaCallDetails({
671711
return (
672712
<div
673713
className={clsx(
674-
'rounded border p-3',
675-
isFailed ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-gray-50',
714+
'rounded border border-gray-200 bg-gray-50 p-3',
715+
isFailed && 'border-red-200 bg-red-50',
676716
)}
677717
>
678-
<label className={clsx('text-xs font-medium', isFailed ? 'text-red-600' : 'text-gray-600')}>
679-
{`Call ${index + 1} of ${total}${isFailed ? ' - Failed' : ''}`}
680-
</label>
718+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
719+
<label className={clsx('text-xs font-medium', isFailed ? 'text-red-600' : 'text-gray-700')}>
720+
{`Call ${index + 1} of ${total}${isFailed ? ' - Failed' : ''}`}
721+
</label>
722+
</div>
681723
<div className="mt-2 space-y-2">
724+
{decodedCallData && <DecodedCallBanner decodedCallData={decodedCallData} blur={blur} />}
682725
<KeyValueRow
683726
label="Target:"
684727
labelWidth="w-16 sm:w-20"
@@ -698,15 +741,139 @@ function IcaCallDetails({
698741
blurValue={blur}
699742
/>
700743
)}
744+
{decodedCallData ? (
745+
<CollapsibleLabelAndCodeBlock label="Raw data:" value={call.data} />
746+
) : (
747+
<KeyValueRow
748+
label="Data:"
749+
labelWidth="w-16 sm:w-20"
750+
display={call.data}
751+
displayWidth="w-52 sm:w-80 lg:w-96"
752+
showCopy={true}
753+
blurValue={blur}
754+
/>
755+
)}
756+
{!!call.nestedCalls?.length && (
757+
<div className="mt-3 space-y-2 border-l-2 border-gray-200 pl-3">
758+
<div className="text-xs font-medium text-gray-600">Decoded multicall:</div>
759+
{call.nestedCalls.map((nestedCall, nestedIndex) => (
760+
<NestedIcaCallDetails
761+
key={`nested-ica-call-${index}-${nestedIndex}`}
762+
call={nestedCall}
763+
index={nestedIndex}
764+
total={call.nestedCalls?.length ?? 0}
765+
explorerUrl={nestedExplorerUrls[`call-${index}-${nestedIndex}`]}
766+
blur={blur}
767+
tryGetChainName={tryGetChainName}
768+
nativeDecimals={nativeDecimals}
769+
/>
770+
))}
771+
</div>
772+
)}
773+
</div>
774+
</div>
775+
);
776+
}
777+
778+
function NestedIcaCallDetails({
779+
call,
780+
index,
781+
total,
782+
explorerUrl,
783+
blur,
784+
tryGetChainName,
785+
nativeDecimals,
786+
}: {
787+
call: IcaCall;
788+
index: number;
789+
total: number;
790+
explorerUrl: string | null | undefined;
791+
blur: boolean;
792+
tryGetChainName: (domainId: number) => string | undefined;
793+
nativeDecimals: number;
794+
}) {
795+
const decodedCallData = useMemo(
796+
() => decodeIcaCallData(call.data, tryGetChainName),
797+
[call.data, tryGetChainName],
798+
);
799+
const { hasValue, formattedValue } = useMemo(
800+
() => getFormattedCallValue(call.value, nativeDecimals),
801+
[call.value, nativeDecimals],
802+
);
803+
804+
return (
805+
<div className="rounded border border-gray-200 bg-white p-3">
806+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
807+
<label className="text-xs font-medium text-gray-600">
808+
{`Nested call ${index + 1} of ${total}`}
809+
</label>
810+
</div>
811+
<div className="mt-2 space-y-2">
812+
{decodedCallData && <DecodedCallBanner decodedCallData={decodedCallData} blur={blur} />}
701813
<KeyValueRow
702-
label="Data:"
814+
label="Target:"
703815
labelWidth="w-16 sm:w-20"
704-
display={call.data}
705-
displayWidth="w-52 sm:w-80 lg:w-96"
816+
display={call.to}
817+
displayWidth="w-52 sm:w-72"
818+
link={explorerUrl || undefined}
706819
showCopy={true}
707820
blurValue={blur}
708821
/>
822+
{hasValue && (
823+
<KeyValueRow
824+
label="Value:"
825+
labelWidth="w-16 sm:w-20"
826+
display={`${formattedValue} (native)`}
827+
displayWidth="w-52 sm:w-72"
828+
showCopy={false}
829+
blurValue={blur}
830+
/>
831+
)}
832+
{decodedCallData ? (
833+
<CollapsibleLabelAndCodeBlock label="Raw data:" value={call.data} />
834+
) : (
835+
<KeyValueRow
836+
label="Data:"
837+
labelWidth="w-16 sm:w-20"
838+
display={call.data}
839+
displayWidth="w-52 sm:w-80 lg:w-96"
840+
showCopy={true}
841+
blurValue={blur}
842+
/>
843+
)}
709844
</div>
710845
</div>
711846
);
712847
}
848+
849+
function DecodedCallBanner({
850+
decodedCallData,
851+
blur,
852+
}: {
853+
decodedCallData: NonNullable<ReturnType<typeof decodeIcaCallData>>;
854+
blur: boolean;
855+
}) {
856+
return (
857+
<div className="rounded bg-primary-50 px-3 py-2 text-xs text-primary-700">
858+
<div>
859+
<span className="font-medium">{decodedCallData.functionName}():</span>{' '}
860+
{decodedCallData.summary}
861+
</div>
862+
{!!decodedCallData.details?.length && (
863+
<div className="mt-1.5 grid grid-cols-1 gap-y-1 sm:grid-cols-2 sm:gap-x-3">
864+
{decodedCallData.details.map((detail) => (
865+
<KeyValueRow
866+
key={`${detail.label}-${detail.value}`}
867+
label={detail.label}
868+
labelWidth="w-24 sm:w-28"
869+
display={detail.value}
870+
displayWidth="w-52 sm:w-72"
871+
showCopy={detail.value.startsWith('0x')}
872+
blurValue={blur}
873+
/>
874+
))}
875+
</div>
876+
)}
877+
</div>
878+
);
879+
}

0 commit comments

Comments
 (0)