Skip to content

Commit 6d43004

Browse files
authored
use orchestration service for Chain Abstraction (#759)
* use orchestration service to asset bridginh * added final step of doing initial transfer request * chores: clean up & refactor * improved UI for CA demo Added input filed for amount and recipient address * added error handling on failure * use orchestrator server-side nonce and gas estimate values
1 parent 4b6f227 commit 6d43004

File tree

14 files changed

+1191
-757
lines changed

14 files changed

+1191
-757
lines changed

advanced/dapps/chain-abstraction-demo/app/layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ export default function RootLayout({
2424
}: Readonly<{
2525
children: React.ReactNode;
2626
}>) {
27-
const initialState = cookieToInitialState(config, headers().get("cookie"));
28-
27+
const cookies = headers().get('cookie')
2928
return (
3029
<html lang="en" suppressHydrationWarning>
3130
<head />
@@ -35,7 +34,7 @@ export default function RootLayout({
3534
fontSans.variable
3635
)}
3736
>
38-
<AppKitProvider initialState={initialState}>
37+
<AppKitProvider cookies={cookies}>
3938
<ThemeProvider
4039
attribute="class"
4140
defaultTheme="dark"

advanced/dapps/chain-abstraction-demo/app/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
'use client'
12
import { Card, CardContent, CardHeader } from "@/components/ui/card";
23
import Transfer from "./transfer";
34

45
export default function Home() {
56
return (
67
<main>
78
<div>
8-
<Card>
9+
<Card >
910
<CardHeader>
1011
<w3m-button />
1112
</CardHeader>
1213
<CardContent>
13-
<Transfer></Transfer>
14+
<Transfer/>
1415
</CardContent>
1516
</Card>
1617
</div>
Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,88 @@
11
"use client";
22
import { Button } from "@/components/ui/button";
3+
import { Input } from "@/components/ui/input";
34
import useSendUsdc from "./hooks/useSendUsdc";
4-
import { useAccount } from "wagmi";
5+
import { useAccount, useReadContract } from "wagmi";
56
import { useState } from "react";
67
import { useToast } from "@/hooks/use-toast";
78
import { Loader2 } from "lucide-react";
9+
import { isAddress } from "viem";
10+
import { tokenAddresses } from "@/consts/tokens";
811

9-
const sendToAddress = "0x81D8C68Be5EcDC5f927eF020Da834AA57cc3Bd24";
10-
const sendAmount = 6000000;
12+
interface BalanceDisplayProps {
13+
address: `0x${string}`;
14+
chainId: number;
15+
}
16+
17+
const BalanceDisplay: React.FC<BalanceDisplayProps> = ({ address, chainId }) => {
18+
const {
19+
data: usdcBalance,
20+
isLoading: usdcBalanceLoading,
21+
} = useReadContract({
22+
abi: [
23+
{
24+
inputs: [{ internalType: "address", name: "owner", type: "address" }],
25+
name: "balanceOf",
26+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
27+
stateMutability: "view",
28+
type: "function",
29+
},
30+
],
31+
address: tokenAddresses[chainId],
32+
functionName: 'balanceOf',
33+
args: [address],
34+
});
35+
36+
// Convert the balance from the smallest unit to USDC
37+
const formattedBalance = usdcBalanceLoading
38+
? 'Loading...'
39+
: usdcBalance
40+
? (parseFloat(usdcBalance.toString()) / 1e6).toFixed(2) // Convert to USDC and format to 2 decimal places
41+
: '0.00';
42+
43+
return (
44+
<p className="text-gray-800 text-lg font-semibold">USDC Balance: {formattedBalance} USDC</p>
45+
);
46+
};
1147

1248
export default function Transfer() {
1349
const { sendUsdcAsync } = useSendUsdc();
14-
const { isConnected, chain } = useAccount();
50+
const { isConnected, chain, address } = useAccount();
1551
const [isLoading, setIsLoading] = useState(false);
1652
const { toast } = useToast();
53+
const [sendToAddress, setSendToAddress] = useState<string>("");
54+
const [sendAmount, setSendAmount] = useState<number | string>("");
55+
56+
if (!chain || !address) {
57+
return null;
58+
}
1759

1860
const onButtonClick = async () => {
1961
try {
62+
if (!isAddress(sendToAddress)) {
63+
toast({
64+
variant: "destructive",
65+
title: "Invalid Address",
66+
description: "Please enter a valid Ethereum address.",
67+
});
68+
return;
69+
}
70+
71+
const amount = Number(sendAmount);
72+
if (amount <= 0) {
73+
toast({
74+
variant: "destructive",
75+
title: "Invalid Amount",
76+
description: "Please enter an amount greater than zero.",
77+
});
78+
return;
79+
}
80+
81+
// Convert the amount to smallest denomination (e.g., 1 USDC = 1,000,000 in smallest unit)
82+
const amountInSmallestDenomination = amount * 1e6;
83+
2084
setIsLoading(true);
21-
const res = await sendUsdcAsync(sendToAddress, sendAmount);
85+
const res = await sendUsdcAsync(sendToAddress, amountInSmallestDenomination);
2286
console.log("Transaction completed", res);
2387
toast({
2488
title: "Transaction completed",
@@ -39,16 +103,61 @@ export default function Transfer() {
39103
return (
40104
<>
41105
{isConnected ? (
42-
<Button onClick={onButtonClick} disabled={isLoading}>
43-
{isLoading ? (
44-
<>
45-
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Sending...
46-
</>
47-
) : (
48-
<>Perform action with USDC on {chain?.name}</>
49-
)}
50-
</Button>
51-
) : null}
106+
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md space-y-4">
107+
<BalanceDisplay address={address} chainId={chain.id} />
108+
<form
109+
onSubmit={(e) => {
110+
e.preventDefault();
111+
onButtonClick();
112+
}}
113+
className="space-y-4"
114+
>
115+
<div>
116+
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
117+
Recipient Address
118+
</label>
119+
<Input
120+
type="text"
121+
id="address"
122+
value={sendToAddress}
123+
onChange={(e) => setSendToAddress(e.target.value)}
124+
className="mt-1 w-[340px] block rounded-md text-black border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2"
125+
required
126+
placeholder="Enter recipient address"
127+
/>
128+
</div>
129+
<div>
130+
<label htmlFor="amount" className="block text-sm font-medium text-gray-700">
131+
Amount (in USDC)
132+
</label>
133+
<Input
134+
type="number"
135+
id="amount"
136+
value={sendAmount}
137+
onChange={(e) => setSendAmount(e.target.value)}
138+
className="mt-1 block w-[340px] text-black rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2"
139+
required
140+
placeholder="Enter amount in USDC (e.g., 2 for 2 USDC)"
141+
/>
142+
</div>
143+
<Button
144+
type="submit"
145+
disabled={isLoading || !sendToAddress || !sendAmount}
146+
className={`w-full ${isLoading ? 'bg-gray-400' : 'bg-blue-600 hover:bg-blue-700'} transition duration-200`}
147+
>
148+
{isLoading ? (
149+
<>
150+
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Sending...
151+
</>
152+
) : (
153+
<>Send USDC on {chain?.name}</>
154+
)}
155+
</Button>
156+
</form>
157+
</div>
158+
) : (
159+
<p className="text-center text-gray-500">Please connect your account to proceed.</p>
160+
)}
52161
</>
53162
);
54-
}
163+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
export interface InputProps
6+
extends React.InputHTMLAttributes<HTMLInputElement> {}
7+
8+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
9+
({ className, type, ...props }, ref) => {
10+
return (
11+
<input
12+
type={type}
13+
className={cn(
14+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
15+
className
16+
)}
17+
ref={ref}
18+
{...props}
19+
/>
20+
)
21+
}
22+
)
23+
Input.displayName = "Input"
24+
25+
export { Input }
Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
import { defaultWagmiConfig } from "@web3modal/wagmi/react/config";
1+
import { cookieStorage, createStorage, http } from '@wagmi/core'
2+
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
3+
import { arbitrum, optimism, base } from '@reown/appkit/networks'
24

3-
import { cookieStorage, createStorage } from "wagmi";
4-
import { arbitrum, base, optimism } from "wagmi/chains";
5+
export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
56

6-
// Get projectId from https://cloud.walletconnect.com
7-
export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
7+
if (!projectId) {
8+
throw new Error('Project ID is not defined')
9+
}
10+
11+
export const networks = [base, optimism, arbitrum]
12+
13+
export const wagmiAdapter = new WagmiAdapter({
14+
storage: createStorage({
15+
storage: cookieStorage
16+
}),
17+
ssr: true,
18+
projectId,
19+
networks
20+
})
821

9-
if (!projectId) throw new Error("Project ID is not defined");
1022

1123
export const metadata = {
12-
name: "AppKit",
13-
description: "AppKit Example",
24+
name: "Chain Abstraction Demo",
25+
description: "A demo of Chain Abstraction",
1426
url: "https://web3modal.com", // origin must match your domain & subdomain
1527
icons: ["https://avatars.githubusercontent.com/u/37784886"],
1628
};
1729

18-
// Create wagmiConfig
19-
const chains = [base, optimism, arbitrum] as const;
20-
export const config = defaultWagmiConfig({
21-
chains,
22-
projectId,
23-
metadata,
24-
ssr: true,
25-
storage: createStorage({
26-
storage: cookieStorage,
27-
}),
28-
});
30+
31+
export const config = wagmiAdapter.wagmiConfig
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Hex } from "viem";
22

33
export const tokenAddresses: Record<number, Hex> = {
4-
42161: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // Arbitrum
4+
42161: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // Arbitrum
5+
10: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // Optimism
6+
8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base
57
}
Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
1-
"use client";
1+
'use client'
22

3-
import React, { ReactNode } from "react";
4-
import { config, projectId, metadata } from "@/config";
3+
import { wagmiAdapter, projectId, metadata } from '@/config'
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5+
import { createAppKit } from '@reown/appkit/react'
6+
import { arbitrum, base, optimism } from '@reown/appkit/networks'
7+
import React, { type ReactNode } from 'react'
8+
import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi'
59

6-
import { createWeb3Modal } from "@web3modal/wagmi/react";
7-
8-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9-
10-
import { State, WagmiProvider } from "wagmi";
11-
12-
// Setup queryClient
1310
const queryClient = new QueryClient();
1411

1512
if (!projectId) throw new Error("Project ID is not defined");
1613

17-
// Create modal
18-
createWeb3Modal({
19-
metadata,
20-
wagmiConfig: config,
14+
const modal = createAppKit({
15+
adapters: [wagmiAdapter],
2116
projectId,
22-
enableAnalytics: true, // Optional - defaults to your Cloud configuration
23-
});
17+
networks: [base, optimism, arbitrum],
18+
defaultNetwork: base,
19+
metadata: metadata,
20+
features: {
21+
analytics: true
22+
}
23+
})
24+
25+
function AppKitProvider({ children, cookies }: { children: ReactNode; cookies: string | null }) {
26+
const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig as Config, cookies)
2427

25-
export default function AppKitProvider({
26-
children,
27-
initialState,
28-
}: {
29-
children: ReactNode;
30-
initialState?: State;
31-
}) {
3228
return (
33-
<WagmiProvider config={config} initialState={initialState}>
29+
<WagmiProvider config={wagmiAdapter.wagmiConfig } initialState={initialState}>
3430
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
3531
</WagmiProvider>
36-
);
32+
)
3733
}
34+
35+
export default AppKitProvider

0 commit comments

Comments
 (0)