Skip to content

Commit 18f4e6e

Browse files
authored
Merge pull request #7 from smartcontractkit/feat/sdk-inspector-polish
Add SDK Inspector panel with real-time call visualization
2 parents c03c8f4 + 44abb9d commit 18f4e6e

49 files changed

Lines changed: 2615 additions & 244 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/03-multichain-bridge-dapp/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,49 @@ Optional: copy `.env.example` to `.env`. Set `RPC_<NETWORK_ID>` (e.g. `RPC_ETHER
5353
- **Validation:** `isValidAddress(receiver, networkInfo(destNetworkId).family)` (shared-utils) handles all families.
5454
- **Pools:** Not every token is enabled on every lane. PoolInfo shows support and rate limits; unsupported lanes show "Lane Not Supported."
5555

56+
## SDK Inspector
57+
58+
The app includes an SDK Inspector panel (toggle via the `</>` button) that visualizes every CCIP SDK call in real time, grouped into four phases: **Setup**, **Fee Estimation**, **Transfer**, and **Tracking**. Each entry shows the method name, arguments, result, latency, and an educational annotation explaining _what_ the call does and _why_ it happens at that point in the flow.
59+
60+
The inspector is **optional instrumentation** layered on top of the SDK calls -- it does not change the SDK's behavior or API surface. If you are reading the source code to learn how to build your own frontend, here is how to navigate it:
61+
62+
### Reading through the inspector code
63+
64+
SDK calls in hooks like `useTransfer.ts` are wrapped in `logSDKCall()`:
65+
66+
```ts
67+
// The wrapper adds inspector instrumentation around the SDK call.
68+
// The actual SDK usage is always the second argument (the lambda).
69+
const tokenInfo = await logSDKCall(
70+
{ method: "chain.getTokenInfo", phase: "estimation", ... },
71+
() => chain.getTokenInfo(tokenAddress) // <-- this is the SDK call
72+
);
73+
```
74+
75+
To extract the SDK pattern, read the lambda. The config object above it (`method`, `phase`, `displayArgs`, `annotation`) is purely for the inspector UI.
76+
77+
### What you can ignore
78+
79+
| File / directory | Purpose | Needed for your app? |
80+
| ---------------------------------------- | ---------------------------------------- | -------------------- |
81+
| `src/inspector/` | Inspector store, annotations, re-exports | No |
82+
| `logSDKCall` / `logSDKCallSync` wrappers | Record calls to the inspector | No |
83+
| `getAnnotation(...)` | Educational text for each method | No |
84+
| `displayArgs` in hook calls | Badge labels shown in the inspector | No |
85+
86+
### What to focus on
87+
88+
| File | What it teaches |
89+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
90+
| `hooks/useTransfer.ts` | End-to-end transfer flow: `networkInfo``getTokenInfo``getFee``getLaneLatency``generateUnsignedSendMessage` |
91+
| `hooks/useEVMTransfer.ts` | EVM-specific signing: approval txs, simulation, `sendTransaction`, `getMessagesInTx` |
92+
| `hooks/useSolanaTransfer.ts` | Solana-specific signing: `VersionedTransaction`, wallet adapter `sendTransaction` |
93+
| `hooks/useAptosTransfer.ts` | Aptos-specific signing: `signAndSubmitTransaction`, transaction polling |
94+
| `hooks/useTokenPoolInfo.ts` | Token pool discovery: registry → pool config → remote token + rate limits |
95+
| `hooks/ChainContext.tsx` | Lazy chain instantiation: `EVMChain.fromUrl` / `SolanaChain.fromUrl` / `AptosChain.fromUrl` |
96+
97+
Every SDK call in these files follows the same pattern: strip the `logSDKCall` wrapper and you have production-ready code.
98+
5699
## Concepts
57100

58101
- **Network IDs:** SDK format only (e.g. `ethereum-testnet-sepolia`, `solana-devnet`, `aptos-testnet`).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.appBody {
2+
display: flex;
3+
flex: 1;
4+
position: relative;
5+
}

examples/03-multichain-bridge-dapp/src/App.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Order: ErrorBoundary → QueryClient → Wagmi → RainbowKit → Solana → Aptos → ChainContext → TransactionHistory → App.
44
*/
55

6-
import { useMemo, useCallback, useContext } from "react";
6+
import { useMemo, useCallback, useContext, lazy, Suspense } from "react";
77
import { QueryClientProvider } from "@tanstack/react-query";
88
import { WagmiProvider } from "wagmi";
99
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
@@ -26,6 +26,8 @@ import { NETWORKS, type FeeTokenOptionItem } from "@ccip-examples/shared-config"
2626
import { networkInfo, NetworkType } from "@chainlink/ccip-sdk";
2727
import { getWalletAddress, type WalletAddresses } from "@ccip-examples/shared-utils";
2828
import { ErrorBoundary, Header } from "@ccip-examples/shared-components";
29+
import { SDKInspectorToggle } from "@ccip-examples/shared-components/inspector";
30+
import { useSDKInspector } from "@ccip-examples/shared-utils/inspector";
2931
import { ChainContextProvider } from "./hooks/ChainContext.jsx";
3032
import { TransactionHistoryContext } from "./hooks/transactionHistoryTypes.js";
3133
import { TransactionHistoryProvider } from "./hooks/TransactionHistoryContext.jsx";
@@ -36,8 +38,16 @@ import { TransactionHistory } from "./components/transaction/TransactionHistory.
3638
import { useTransfer } from "./hooks/useTransfer.js";
3739
import "@ccip-examples/shared-components/styles/globals.css";
3840
import styles from "@ccip-examples/shared-components/layout/AppLayout.module.css";
41+
import appStyles from "./App.module.css";
3942
import "@rainbow-me/rainbowkit/styles.css";
4043

44+
// Lazy load the inspector panel — zero cost when inspector is disabled
45+
const SDKInspectorPanel = lazy(() =>
46+
import("@ccip-examples/shared-components/inspector").then((m) => ({
47+
default: m.SDKInspectorPanel,
48+
}))
49+
);
50+
4151
const queryClient = createDefaultQueryClient();
4252

4353
/** RPC endpoints from shared-config (no wrapper files needed). */
@@ -69,6 +79,7 @@ function AppContent() {
6979

7080
const transfer = useTransfer();
7181
const addTransaction = useContext(TransactionHistoryContext).addTransaction;
82+
const { enabled: inspectorEnabled } = useSDKInspector();
7283

7384
const isConnected = Boolean(evmAddress ?? solanaAddress ?? aptosAddress);
7485
const isLoading = ["estimating", "sending"].includes(transfer.status);
@@ -94,9 +105,18 @@ function AppContent() {
94105
token: string,
95106
amount: string,
96107
receiver: string,
97-
feeToken: FeeTokenOptionItem | null
108+
feeToken: FeeTokenOptionItem | null,
109+
remoteToken: string | null
98110
) => {
99-
const result = await transfer.transfer(source, dest, token, amount, receiver, feeToken);
111+
const result = await transfer.transfer(
112+
source,
113+
dest,
114+
token,
115+
amount,
116+
receiver,
117+
feeToken,
118+
remoteToken
119+
);
100120
if (result?.messageId == null) return;
101121
const sender = getWalletAddress(source, walletAddresses);
102122
if (sender)
@@ -124,42 +144,52 @@ function AppContent() {
124144
return (
125145
<div className={styles.app}>
126146
<Header title="Multichain Family Bridge" subtitle="EVM, Solana, and Aptos token transfers">
147+
<SDKInspectorToggle />
127148
<HistoryButton />
128149
</Header>
129-
<main className={`${styles.main} ${styles.mainWide}`}>
130-
<div className={styles.section}>
131-
<WalletConnect />
132-
</div>
133-
134-
{isConnected && (
135-
<>
136-
<BridgeForm
137-
walletAddresses={walletAddresses}
138-
currentChainId={chainId ?? null}
139-
fee={transfer.fee}
140-
feeFormatted={transfer.feeFormatted}
141-
estimatedTime={transfer.estimatedTime}
142-
isLoading={isLoading}
143-
onEstimateFee={handleEstimateFee}
144-
onTransfer={handleTransfer}
145-
onSwitchChain={handleSwitchChain}
146-
onClearEstimate={transfer.clearEstimate}
147-
onReset={transfer.reset}
148-
/>
149-
150-
<TransactionStatusView
151-
status={transfer.status}
152-
error={transfer.error}
153-
txHash={transfer.txHash}
154-
messageId={transfer.messageId}
155-
estimatedTime={transfer.estimatedTime}
156-
onReset={transfer.reset}
157-
lastTransferContext={transfer.lastTransferContext}
158-
categorizedError={transfer.categorizedError}
159-
/>
160-
</>
150+
151+
<div className={appStyles.appBody}>
152+
{inspectorEnabled && (
153+
<Suspense fallback={null}>
154+
<SDKInspectorPanel />
155+
</Suspense>
161156
)}
162-
</main>
157+
158+
<main className={`${styles.main} ${styles.mainWide}`}>
159+
<div className={styles.section}>
160+
<WalletConnect />
161+
</div>
162+
163+
{isConnected && (
164+
<>
165+
<BridgeForm
166+
walletAddresses={walletAddresses}
167+
currentChainId={chainId ?? null}
168+
fee={transfer.fee}
169+
feeFormatted={transfer.feeFormatted}
170+
estimatedTime={transfer.estimatedTime}
171+
isLoading={isLoading}
172+
onEstimateFee={handleEstimateFee}
173+
onTransfer={handleTransfer}
174+
onSwitchChain={handleSwitchChain}
175+
onClearEstimate={transfer.clearEstimate}
176+
onReset={transfer.reset}
177+
/>
178+
179+
<TransactionStatusView
180+
status={transfer.status}
181+
error={transfer.error}
182+
txHash={transfer.txHash}
183+
messageId={transfer.messageId}
184+
estimatedTime={transfer.estimatedTime}
185+
onReset={transfer.reset}
186+
lastTransferContext={transfer.lastTransferContext}
187+
categorizedError={transfer.categorizedError}
188+
/>
189+
</>
190+
)}
191+
</main>
192+
</div>
163193

164194
<TransactionHistory />
165195
</div>

examples/03-multichain-bridge-dapp/src/components/bridge/BridgeForm.tsx

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import {
3434
type BalanceItem,
3535
} from "@ccip-examples/shared-components";
3636
import { useWalletBalances, useFeeTokens } from "@ccip-examples/shared-utils/hooks";
37+
import { inspectorStore } from "../../inspector/index.js";
38+
import { getAnnotation } from "../../inspector/annotations.js";
39+
import { serializeForDisplay } from "@ccip-examples/shared-utils/inspector";
3740
import { PoolInfo } from "./PoolInfo.js";
3841
import { useChains } from "../../hooks/useChains.js";
3942
import { NETWORK_TO_CHAIN_ID } from "@ccip-examples/shared-config/wagmi";
@@ -62,7 +65,8 @@ interface BridgeFormProps {
6265
token: string,
6366
amount: string,
6467
receiver: string,
65-
feeToken: FeeTokenOptionItem | null
68+
feeToken: FeeTokenOptionItem | null,
69+
remoteToken: string | null
6670
) => Promise<void>;
6771
onSwitchChain: (chainId: number) => void;
6872
onClearEstimate: () => void;
@@ -88,15 +92,17 @@ export function BridgeForm({
8892
const [receiver, setReceiver] = useState("");
8993
const [useSelfAsReceiver, setUseSelfAsReceiver] = useState(true);
9094
const [copied, setCopied] = useState(false);
95+
const [poolRemoteToken, setPoolRemoteToken] = useState<string | null>(null);
9196

9297
const { isEVM, getChain } = useChains();
9398

94-
/** Clear stale transfer result + fee when the user changes networks */
99+
/** Clear stale transfer result + fee + inspector when the user changes source network */
95100
const handleSourceChange = useCallback(
96101
(id: string) => {
97102
setSourceNetworkId(id);
98103
onReset();
99104
onClearEstimate();
105+
inspectorStore.clearCalls();
100106
},
101107
[onReset, onClearEstimate]
102108
);
@@ -127,13 +133,40 @@ export function BridgeForm({
127133
? (getTokenAddress(TOKEN_SYMBOL, sourceNetworkId) ?? null)
128134
: null;
129135

136+
/** Stable callback for PoolInfo to report remoteToken changes */
137+
const handleRemoteTokenResolved = useCallback((rt: string | null) => {
138+
setPoolRemoteToken(rt);
139+
}, []);
140+
141+
const recordSDKCall = useCallback(
142+
(method: string, args: Record<string, string>, result?: unknown, durationMs?: number) => {
143+
if (!inspectorStore.getSnapshot().enabled) return;
144+
const ann = getAnnotation(method);
145+
inspectorStore.addCall({
146+
id: crypto.randomUUID(),
147+
timestamp: Date.now(),
148+
phase: "setup",
149+
method,
150+
displayArgs: args,
151+
codeSnippet: ann.codeSnippet,
152+
annotation: ann.annotation,
153+
status: "success",
154+
result: serializeForDisplay(result),
155+
durationMs,
156+
});
157+
},
158+
[]
159+
);
160+
130161
const {
131162
feeTokens,
132163
selectedToken: feeToken,
133164
setSelectedToken: setFeeToken,
134165
isLoading: feeTokensLoading,
135166
error: feeTokensError,
136-
} = useFeeTokens(sourceNetworkId || null, routerAddress, walletAddress, getChain);
167+
} = useFeeTokens(sourceNetworkId || null, routerAddress, walletAddress, getChain, {
168+
onSDKCall: recordSDKCall,
169+
});
137170

138171
const {
139172
token,
@@ -144,7 +177,8 @@ export function BridgeForm({
144177
tokenAddress,
145178
walletAddress ?? null,
146179
getChain,
147-
TOKEN_SYMBOL
180+
TOKEN_SYMBOL,
181+
{ onSDKCall: recordSDKCall, skipNative: true, skipLink: true }
148182
);
149183

150184
/** Wallet address on the destination chain (for "send to myself") */
@@ -195,7 +229,15 @@ export function BridgeForm({
195229

196230
const handleTransfer = () => {
197231
if (canTransfer && token)
198-
void onTransfer(sourceNetworkId, destNetworkId, TOKEN_SYMBOL, amount, receiver, feeToken);
232+
void onTransfer(
233+
sourceNetworkId,
234+
destNetworkId,
235+
TOKEN_SYMBOL,
236+
amount,
237+
receiver,
238+
feeToken,
239+
poolRemoteToken
240+
);
199241
};
200242

201243
const handleSwitchChain = () => {
@@ -258,6 +300,7 @@ export function BridgeForm({
258300
tokenAddress={tokenAddress ?? undefined}
259301
tokenDecimals={token?.decimals}
260302
tokenSymbol={TOKEN_SYMBOL}
303+
onRemoteTokenResolved={handleRemoteTokenResolved}
261304
/>
262305

263306
<div className={styles.tokenRow}>

examples/03-multichain-bridge-dapp/src/components/bridge/PoolInfo.module.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@
7171

7272
.rateLimits {
7373
margin-top: var(--spacing-4);
74+
padding-top: var(--spacing-3);
75+
border-top: 1px solid var(--color-border-light);
76+
}
77+
78+
.rateLimitsLabel {
79+
display: block;
80+
font-size: var(--font-size-xs);
81+
font-weight: 600;
82+
color: var(--color-text-secondary);
83+
text-transform: uppercase;
84+
letter-spacing: 0.05em;
85+
margin-bottom: var(--spacing-3);
7486
}
7587

7688
.skeleton {

0 commit comments

Comments
 (0)