Skip to content

Ethereum demo #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/ethereum/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# next.js
/.next/
/out/

# production
/build

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env*

# typescript
*.tsbuildinfo
next-env.d.ts
94 changes: 94 additions & 0 deletions examples/ethereum/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
25 changes: 25 additions & 0 deletions examples/ethereum/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Metadata } from 'next'
import "@interchain-ui/react/styles";
// import '../styles/globals.css'
import Provider from './provider'

export const metadata: Metadata = {
title: 'Ethereum Demo - InterchainJS',
description: 'Created with InterchainJS, Interchain-Kit',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body>
<Provider>
{children}
</Provider>
</body>
</html>
)
}
225 changes: 225 additions & 0 deletions examples/ethereum/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
"use client"

import { Box, Button, TextField, NumberField, FieldLabel, Callout } from "@interchain-ui/react"
import React, { useState, useEffect } from "react"
import { Wallet, ArrowRight, RefreshCw, AlertCircle } from "lucide-react"
import { SignerFromBrowser } from "@interchainjs/ethereum/signers/SignerFromBrowser"
import { MetaMaskInpageProvider } from "@metamask/providers";
import BigNumber from "bignumber.js";
import { useChain } from '@interchain-kit/react'
import { WalletState } from "@interchain-kit/core"

type EthereumProvider = MetaMaskInpageProvider

// Alias Card components
const Card = Box
const CardHeader = Box
const CardContent = Box
const CardFooter = Box
const CardTitle = Box
const CardDescription = Box

export default function WalletPage() {
const [balance, setBalance] = useState<string>("0")
const [isLoading, setIsLoading] = useState(false)
const [recipient, setRecipient] = useState("")
const [amount, setAmount] = useState<number>(0)
const [error, setError] = useState("")
const [ethereum, setEthereum] = useState<EthereumProvider>()

const { wallet, status, connect, address: account, disconnect } = useChain('ethereum')

useEffect(() => {
console.log('status from useChain:', status)
if (status === WalletState.Connected) {
const setEthProviderFromWallet = async () => {
await new Promise(resolve => setTimeout(resolve, 500))
const ethProviderFromWallet = await wallet.getProvider('1') as EthereumProvider
console.log("Ethereum provider:", ethProviderFromWallet)
setEthereum(ethProviderFromWallet)
}
setEthProviderFromWallet()
}
setIsLoading(status === WalletState.Connecting)
}, [status])

// Connect wallet
const connectWallet = async () => {
connect()
}

// Disconnect wallet
const disconnectWallet = () => {
disconnect()
setBalance("0")
setError("")
}

// Get balance
const getBalance = async () => {
if (!ethereum) return
try {
console.log('ethereum in getBalance:', ethereum)
const wallet = new SignerFromBrowser(
ethereum!
// window.ethereum as EthereumProvider
)
console.log('wallet in getBalance:', wallet)
const balance = await wallet.getBalance()
console.log('balance in getBalance:', balance)
setBalance(new BigNumber(balance.toString()).div(10 ** 18).toString())
} catch (err: any) {
console.error("Failed to get balance:", err)
setError(err.message || "Failed to get balance")
}
}

// Refresh balance
const refreshBalance = async () => {
console.log('account in refreshBalance:', account)
if (account) {
await getBalance()
}
}

// Send transaction
const sendTransaction = async () => {
setIsLoading(true)
setError("")

try {
if (!recipient || amount <= 0) {
throw new Error("Please enter recipient address and amount")
}

if (!/^0x[a-fA-F0-9]{40}$/.test(recipient)) {
throw new Error("Invalid Ethereum address")
}

const signer = new SignerFromBrowser(ethereum!)

// Create transaction
const tx = {
to: recipient,
value: BigInt(new BigNumber(amount).shiftedBy(18).integerValue(BigNumber.ROUND_DOWN).toString())
}

// Send transaction
const transaction = await signer.send(tx)

// Wait for confirmation
await transaction.wait()

// Update balance
await getBalance()

// Clear form
setRecipient("")
setAmount(0)
} catch (err: any) {
setError(err.message || "Transaction failed")
} finally {
setIsLoading(false)
}
}

// Listen for account changes
useEffect(() => {
if (account) {
getBalance()
return
}
setBalance("0")
}, [account, ethereum])

return (
<main className="container mx-auto py-10 px-4">
<h1 className="text-3xl font-bold text-center mb-8">Ethereum Wallet</h1>

<Box className={`grid gap-6 ${status === WalletState.Connected ? "md:grid-cols-2" : ""}`}>
<Card className='border border-1 p-5 rounded-md'>
<CardHeader className='mb-4'>
<CardTitle className='font-bold text-2xl'>Wallet Connection</CardTitle>
<CardDescription className='text-gray-500'>Connect your Ethereum wallet to view balance</CardDescription>
</CardHeader>
<CardContent>
{status !== WalletState.Connected ? (
<Button onClick={connectWallet} disabled={isLoading} className="w-full">
{isLoading ? "Connecting..." : "Connect Wallet"}
<Wallet className="ml-2 h-4 w-4" />
</Button>
) : (
<Box className="space-y-4">
<Box className="flex flex-col space-y-1">
<FieldLabel htmlFor="account" label='Wallet Address'>Wallet Address</FieldLabel>
<Box id="account" className="p-2 border rounded-md bg-muted font-mono text-sm break-all">{account}</Box>
</Box>

<Box className="flex flex-col space-y-1">
<Box className="flex justify-between items-center">
<FieldLabel htmlFor="balance" label="ETH Balance">ETH Balance</FieldLabel>
<Button onClick={refreshBalance} disabled={isLoading} size="sm">
<RefreshCw className="h-4 w-4" />
</Button>
</Box>
<Box id="balance" className="p-2 border rounded-md bg-muted font-mono text-xl">{balance} ETH</Box>
</Box>

<Button onClick={disconnectWallet} className="w-full">
Disconnect Wallet
</Button>
</Box>
)}
</CardContent>
</Card>

{status === WalletState.Connected && (
<Card className='border border-1 p-5 rounded-md'>
<CardHeader className='mb-4'>
<CardTitle className='font-bold text-2xl'>Send Ethereum</CardTitle>
<CardDescription className='text-gray-500'>Transfer ETH to another address</CardDescription>
</CardHeader>
<CardContent>
<Box className="space-y-4">
<Box className="space-y-2">
<FieldLabel htmlFor="recipient" label="Recipient Address">Recipient Address</FieldLabel>
<TextField
id="recipient"
placeholder="0x..."
value={recipient}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setRecipient(e.target.value)}
disabled={isLoading}
/>
</Box>

<Box className="space-y-2">
<FieldLabel htmlFor="amount" label='Amount (ETH)'>Amount (ETH)</FieldLabel>
<NumberField
id="amount"
placeholder="0.01"
value={amount}
onChange={(value: number) => setAmount(value)}
isDisabled={isLoading}
/>
</Box>
</Box>
</CardContent>
<CardFooter className="mt-4">
<Button className="w-full" onClick={sendTransaction} disabled={isLoading || !recipient || amount <= 0}>
{isLoading ? "Processing..." : "Send Transaction"}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
)}
</Box>

{error && (
<Callout title="Error" className="mt-6" intent="error">
<Box as="span" className="h-4 w-4 inline-block mr-2"><AlertCircle /></Box>
{error}
</Callout>
)}
</main>
)
}
Loading
Loading