Skip to content

Commit 5158a95

Browse files
authored
Merge pull request #45 from smartcontractkit/chore/ethers-nextjs-page
Add separate Ethers adapter demo page
2 parents 2b52598 + 51e50a2 commit 5158a95

File tree

7 files changed

+278
-1435
lines changed

7 files changed

+278
-1435
lines changed

examples/nextjs/README.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,8 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update
2020

2121
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
2222

23-
## Learn More
23+
## Example Implementation
24+
This NextJS example app
25+
- shows the use of both the `ccip-react-components` package as well as the `ccip-js` package.
26+
- shows how to use the `ccipClient` exposed by `ccip-js` using viem (default, preferred) and also with an ethers provider (via an adapter).
2427

25-
To learn more about Next.js, take a look at the following resources:
26-
27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29-
30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31-
32-
## Deploy on Vercel
33-
34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35-
36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"use client";
2+
3+
import { useMemo, useState } from "react";
4+
import { Providers } from "../ccip-js/providers";
5+
import { ethers } from "ethers";
6+
import { createClient } from "@chainlink/ccip-js";
7+
import { createPublicClient, custom } from "viem";
8+
import { useAccount, useSwitchChain } from "wagmi";
9+
import { ethersProviderToPublicClient, ethersSignerToWalletClient } from "@chainlink/ccip-js";
10+
11+
export default function CCIPEthersDemoPage() {
12+
return (
13+
<Providers>
14+
<EthersDemo />
15+
</Providers>
16+
);
17+
}
18+
19+
function EthersDemo() {
20+
const ccipClient = useMemo(() => createClient(), []);
21+
const [publicReady, setPublicReady] = useState(false);
22+
const [walletReady, setWalletReady] = useState(false);
23+
const [publicError, setPublicError] = useState<string | null>(null);
24+
const [walletError, setWalletError] = useState<string | null>(null);
25+
const { chain, address } = useAccount();
26+
const { chains, switchChain, isError: isSwitchError, error: switchError } = useSwitchChain();
27+
const [selectedChainId, setSelectedChainId] = useState<string>("");
28+
29+
const [routerAddress, setRouterAddress] = useState<string>("");
30+
const [destinationChainSelector, setDestinationChainSelector] = useState<string>("");
31+
const [onRamp, setOnRamp] = useState<string>("");
32+
const [onRampError, setOnRampError] = useState<string | null>(null);
33+
34+
const [publicClient, setPublicClient] = useState<any>(null);
35+
36+
async function connectEthers() {
37+
try {
38+
setPublicError(null);
39+
setWalletError(null);
40+
const browserProvider = new ethers.BrowserProvider((window as any).ethereum);
41+
const signer = await browserProvider.getSigner();
42+
43+
const net = await browserProvider.getNetwork();
44+
const viemChain = {
45+
id: Number(net.chainId),
46+
name: net.name || "unknown",
47+
nativeCurrency: { name: "", symbol: "", decimals: 18 },
48+
rpcUrls: { default: { http: [] }, public: { http: [] } },
49+
} as any;
50+
51+
const viemPublic =
52+
typeof ethersProviderToPublicClient === "function"
53+
? ethersProviderToPublicClient(browserProvider, viemChain)
54+
: createPublicClient({ chain: viemChain, transport: custom(browserProvider as any) });
55+
56+
if (typeof ethersSignerToWalletClient === "function") {
57+
await ethersSignerToWalletClient(signer, viemChain);
58+
}
59+
60+
setPublicClient(viemPublic);
61+
setPublicReady(true);
62+
setWalletReady(true);
63+
} catch (e: any) {
64+
const msg = e?.message ?? String(e);
65+
if (!publicReady) setPublicError(msg);
66+
if (!walletReady) setWalletError(msg);
67+
}
68+
}
69+
70+
return (
71+
<div className="m-2 p-2 w-full grid gap-2">
72+
<div className="space-y-2 border rounded-md p-4 bg-white">
73+
<h2 className="font-bold">Connect (ethers → viem via adapters)</h2>
74+
<button
75+
className="rounded-md p-2 bg-black text-white hover:bg-slate-600 transition-colors"
76+
onClick={connectEthers}
77+
>
78+
Connect
79+
</button>
80+
{!publicReady && publicError && <p className="text-red-500">{publicError}</p>}
81+
{!walletReady && walletError && <p className="text-red-500">{walletError}</p>}
82+
{publicReady && walletReady && <p>Adapters ready.</p>}
83+
</div>
84+
85+
<div className="space-y-2 border rounded-md p-4 bg-white">
86+
<h2 className="font-bold">Network</h2>
87+
{address && <p>{`Address: ${address}`}</p>}
88+
{chain && <p>{`Connected to ${chain.name} (chainId: ${chain.id})`}</p>}
89+
<div className="flex flex-col">
90+
<label htmlFor="chainId">Switch to chain</label>
91+
<select
92+
className="border border-slate-300 rounded-md p-1"
93+
name="chainId"
94+
value={selectedChainId}
95+
onChange={e => setSelectedChainId(e.target.value)}
96+
>
97+
<option value="" disabled>
98+
Select chain
99+
</option>
100+
{chains.map(c => (
101+
<option key={c.id} value={c.id}>
102+
{c.name}
103+
</option>
104+
))}
105+
</select>
106+
</div>
107+
<button
108+
className="rounded-md p-2 bg-black text-white hover:bg-slate-600 transition-colors"
109+
onClick={async () => {
110+
if (selectedChainId) {
111+
try {
112+
await switchChain({ chainId: Number(selectedChainId) });
113+
await connectEthers();
114+
} catch (e) {
115+
// ignore, error shown below
116+
}
117+
}
118+
}}
119+
>
120+
Switch
121+
</button>
122+
{isSwitchError && <p className="text-red-500">{switchError?.message}</p>}
123+
</div>
124+
125+
<div className="space-y-2 border rounded-md p-4 bg-white">
126+
<h2 className="font-bold">Get On-ramp address (ethers-adapted public client)</h2>
127+
<div className="flex flex-col">
128+
<label htmlFor="routerAddress">Router Address*</label>
129+
<input
130+
className="border border-slate-300 rounded-md p-1"
131+
name="routerAddress"
132+
placeholder="0x..."
133+
onChange={({ target }) => setRouterAddress(target.value)}
134+
/>
135+
</div>
136+
<div className="flex flex-col w-full">
137+
<label htmlFor="destinationChainSelector">Destination Chain Selector*</label>
138+
<input
139+
className="border border-slate-300 rounded-md p-1"
140+
name="destinationChainSelector"
141+
placeholder="1234..."
142+
onChange={({ target }) => setDestinationChainSelector(target.value)}
143+
/>
144+
</div>
145+
<button
146+
className="rounded-md p-2 bg-black text-white hover:bg-slate-600 transition-colors"
147+
onClick={async () => {
148+
setOnRamp("");
149+
setOnRampError(null);
150+
if (publicClient && routerAddress && destinationChainSelector) {
151+
try {
152+
const result = await ccipClient.getOnRampAddress({
153+
client: publicClient,
154+
routerAddress: routerAddress as any,
155+
destinationChainSelector,
156+
});
157+
setOnRamp(result as string);
158+
} catch (e: any) {
159+
setOnRampError(e?.message ?? String(e));
160+
}
161+
}
162+
}}
163+
>
164+
Get On-ramp
165+
</button>
166+
{onRampError && <p className="text-red-500">{onRampError}</p>}
167+
{onRamp && (
168+
<div className="flex flex-col w-full">
169+
<label>On-ramp contract address:</label>
170+
<code className="w-full whitespace-pre-wrap break-all">{onRamp}</code>
171+
</div>
172+
)}
173+
</div>
174+
</div>
175+
);
176+
}
177+
178+

examples/nextjs/app/layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ export default function RootLayout({
3636
className="border border-slate-300 rounded-md p-2 hover:bg-slate-300 transition-colors"
3737
href="/ccip-js"
3838
>
39-
CCIP-JS
39+
CCIP-JS (viem)
40+
</Link>
41+
<Link
42+
className="border border-slate-300 rounded-md p-2 hover:bg-slate-300 transition-colors"
43+
href="/ccip-ethers"
44+
>
45+
CCIP-JS (ethers)
4046
</Link>
4147
</nav>
4248
<main className="flex flex-col items-center justify-center bg-slate-100 grow">

examples/nextjs/components/ccip.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,30 @@ function ConnectWallet() {
5656
const { chains, switchChain, error: switchError, isError: isSwitchError } = useSwitchChain();
5757

5858
const [chainId, setChainId] = useState<string>(`${chain?.id}`);
59+
const [localConnectError, setLocalConnectError] = useState<string | null>(null);
5960

6061
return (
6162
<div className="space-y-2 border rounded-md p-4 bg-white">
62-
<h2 className="font-bold">Connect Wallet:</h2>
63-
<div className="space-x-2">
64-
{connectors.map(connector => (
65-
<button
66-
className="rounded-md p-2 bg-black text-white hover:bg-slate-600 transition-colors"
67-
key={connector.uid}
68-
onClick={() => connect({ connector })}
69-
>
70-
{connector.name}
71-
</button>
72-
))}
73-
</div>
63+
<h2 className="font-bold">Connect Wallet</h2>
64+
<button
65+
className="rounded-md p-2 bg-black text-white hover:bg-slate-600 transition-colors"
66+
onClick={() => {
67+
setLocalConnectError(null);
68+
const preferredConnector =
69+
connectors.find(c => c.id === "metaMask" || c.name.toLowerCase() === "metamask") ||
70+
connectors.find(c => c.id === "injected") ||
71+
connectors[0];
72+
if (!preferredConnector) {
73+
setLocalConnectError("No wallet connector available");
74+
return;
75+
}
76+
connect({ connector: preferredConnector });
77+
}}
78+
>
79+
Connect Wallet
80+
</button>
7481
{isConnectError && <p className="text-red-500">{connectError.message}</p>}
82+
{localConnectError && <p className="text-red-500">{localConnectError}</p>}
7583
{address && <p>{`Address: ${address}`}</p>}
7684
{chain && (
7785
<>

examples/nextjs/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"private": true,
66
"scripts": {
7-
"bdev": "pnpm run build-components && next dev",
7+
"bdev": "pnpm run build-ccip-js && pnpm run build-components && next dev",
88
"dev": "next dev",
99
"build-ccip-js": "pnpm --filter ccip-js run build",
1010
"build-components": "pnpm --filter ccip-react-components run build",
@@ -13,9 +13,10 @@
1313
"lint": "next lint"
1414
},
1515
"dependencies": {
16-
"@chainlink/ccip-js": "^0.2.1",
17-
"@chainlink/ccip-react-components": "^0.3.0",
16+
"@chainlink/ccip-js": "workspace:^",
17+
"@chainlink/ccip-react-components": "workspace:^",
1818
"@tanstack/react-query": "^5.37.1",
19+
"ethers": "^6",
1920
"next": "14.2.3",
2021
"react": "18",
2122
"react-dom": "18",

examples/nextjs/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
}
1919
],
2020
"paths": {
21-
"@/*": ["./*"]
21+
"@/*": ["./*"],
22+
"@chainlink/ccip-js": ["../../packages/ccip-js/dist/api.d.ts"],
23+
"@chainlink/ccip-js/*": ["../../packages/ccip-js/dist/*"]
2224
}
2325
},
2426
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

0 commit comments

Comments
 (0)