Skip to content

Commit

Permalink
feat: swap controls for the example app (#148)
Browse files Browse the repository at this point in the history
* restructure code a little

* add invariant

* add inputs for both eth and weth

* add controls

* remove console log
  • Loading branch information
frontendphil authored Oct 2, 2024
1 parent dbb71a8 commit bb84614
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 66 deletions.
36 changes: 36 additions & 0 deletions example-app/app/Connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ConnectKitButton } from 'connectkit'
import { PropsWithChildren } from 'react'
import { ClientOnly } from 'remix-utils/client-only'
import { useAccount, useDisconnect } from 'wagmi'

export const Connect = () => {
const account = useAccount()
const { disconnect } = useDisconnect()

if (account.isDisconnected || account.isConnecting) {
return <ConnectKitButton />
}

return (
<ClientOnly>
{() => (
<button
className="rounded bg-red-500 px-4 py-2 text-white outline-none transition-colors hover:bg-red-600"
onClick={() => disconnect()}
>
Disconnect wallet
</button>
)}
</ClientOnly>
)
}

export const Connected = ({ children }: PropsWithChildren) => {
const account = useAccount()

if (account.isDisconnected || account.isConnecting) {
return null
}

return <ClientOnly>{() => children}</ClientOnly>
}
25 changes: 25 additions & 0 deletions example-app/app/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AllHTMLAttributes, useId } from 'react'

type InputProps = AllHTMLAttributes<HTMLInputElement> & {
label: string
}

export const Input = ({ label, children, ...props }: InputProps) => {
const id = useId()

return (
<div className="flex flex-col gap-2">
<label htmlFor={id} className="ml-4 font-semibold">
{label}
</label>

<input
{...props}
id={id}
className="w-full rounded border border-gray-200 bg-gray-100 px-4 py-2 outline-none ring-2 ring-transparent focus:border-blue-600 focus:ring-blue-300"
/>

{children && <div className="ml-4">{children}</div>}
</div>
)
}
1 change: 1 addition & 0 deletions example-app/app/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Input } from './Input'
29 changes: 22 additions & 7 deletions example-app/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { Transfer } from '@/transfer'
import { json, Links, Meta, Scripts, useLoaderData } from '@remix-run/react'
import { invariantResponse } from '@epic-web/invariant'
import { json } from '@remix-run/node'
import { Links, Meta, Scripts, useLoaderData } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ConnectKitProvider } from 'connectkit'
import { ClientOnly } from 'remix-utils/client-only'
import { WagmiProvider } from 'wagmi'
import { config } from './config'
import { Connect, Connected } from './Connect'
import './tailwind.css'
import { Transfer } from './transfer'

export const loader = () => json({ projectId: process.env.PROJECT_ID })
const getProjectId = () => {
const { PROJECT_ID } = process.env

invariantResponse(PROJECT_ID, '"PROJECT_ID" environment variable not found')

return PROJECT_ID
}

export const loader = () => json({ projectId: getProjectId() })

export const queryClient = new QueryClient()

export default function App() {
const { projectId } = useLoaderData<typeof loader>()
const wagmiConfig = config(projectId)

return (
<html className="h-full w-full">
Expand All @@ -21,15 +32,19 @@ export default function App() {
<Meta />
<Links />
</head>
<body className="flex h-full w-full flex-col items-center justify-center gap-8">
<WagmiProvider config={config(projectId)}>
<body className="flex h-full w-full flex-col items-center justify-center gap-8 text-sm">
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<ConnectKitProvider>
<h1 className="text-2xl font-bold">
zodiac pilot <span className="font-normal">example app</span>
</h1>

<ClientOnly>{() => <Transfer />}</ClientOnly>
<Connect />

<Connected>
<Transfer />
</Connected>
</ConnectKitProvider>
</QueryClientProvider>
</WagmiProvider>
Expand Down
108 changes: 95 additions & 13 deletions example-app/app/transfer/Balance.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,106 @@
import { formatEther } from 'viem'
import { useBalance } from 'wagmi'
import { PropsWithChildren } from 'react'
import { Abi, Address, erc20Abi, formatEther, formatUnits } from 'viem'
import { useBalance, useReadContract } from 'wagmi'

type BalanceProps = {
contract: `0x${string}`
address: Address
contract?: Address
}

export const Balance = ({ contract }: BalanceProps) => {
const { data, isPending } = useBalance({ address: contract })
export const Balance = ({ address, contract }: BalanceProps) => {
return (
<div className="flex items-center gap-2 text-xs leading-none text-gray-500">
<span className="font-semibold uppercase">Balance</span>
{contract ? (
<ER20Balance address={address} contract={contract} />
) : (
<EthBalance address={address} />
)}
</div>
)
}

if (isPending) {
return null
const useEthBalance = (address: Address): BalanceValue | [null, null] => {
const { data, isFetched } = useBalance({ address })

if (isFetched) {
return [formatEther(data.value), data.symbol] as const
}

return [null, null]
}

const Symbol = ({ children }: PropsWithChildren) => (
<span className="rounded bg-blue-100 px-1 text-xs font-semibold uppercase tabular-nums text-blue-500">
{children}
</span>
)

type BalanceValue = [balance: string, symbol: string]

type EthBalanceProps = {
address: Address
}

const EthBalance = ({ address }: EthBalanceProps) => {
const [balance, symbol] = useEthBalance(address)

return (
<div className="flex items-center gap-2">
{formatEther(data.value)}
<>
{balance}

<span className="rounded bg-blue-100 px-1 text-xs font-semibold uppercase tabular-nums text-blue-500">
{data.symbol}
</span>
</div>
<Symbol>{symbol}</Symbol>
</>
)
}

type UseER20BalanceOptions<T extends Abi> = {
address: Address
contract: Address
}

const useERC20Balance = <T extends Abi>({
address,
contract,
}: UseER20BalanceOptions<T>): BalanceValue | [null, null] => {
const balanceOf = useReadContract({
abi: erc20Abi,
functionName: 'balanceOf',
address: contract,
args: [address],
})

const decimals = useReadContract({
abi: erc20Abi,
functionName: 'decimals',
address: contract,
})

const symbol = useReadContract({
abi: erc20Abi,
functionName: 'symbol',
address: contract,
})

if (balanceOf.isFetched && decimals.isFetched && symbol.isFetched) {
return [formatUnits(balanceOf.data, decimals.data), symbol.data] as const
}

return [null, null]
}

type ER20BalanceProps = {
address: Address
contract: Address
}

const ER20Balance = ({ address, contract }: ER20BalanceProps) => {
const [balance, symbol] = useERC20Balance({ address, contract })

return (
<>
{balance}
<Symbol>{symbol}</Symbol>
</>
)
}
99 changes: 75 additions & 24 deletions example-app/app/transfer/Transfer.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,89 @@
import { ConnectKitButton } from 'connectkit'
import { Input } from '@/components'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useState } from 'react'
import { Fragment } from 'react/jsx-runtime'
import { useAccount, useDisconnect } from 'wagmi'
import { useAccount } from 'wagmi'
import { Balance } from './Balance'

type Target = 'ETH' | 'WETH'

export const Transfer = () => {
const account = useAccount()
const { disconnect } = useDisconnect()

if (account.isDisconnected || account.isConnecting) {
return <ConnectKitButton />
}
const [target, setTarget] = useState<Target>('WETH')

return (
<dl>
<form
className="flex w-1/3 flex-col gap-8"
onSubmit={(event) => {
event.preventDefault()
}}
>
{account.addresses.map((address) => (
<Fragment key={address}>
<dt className="font-semibold">Account</dt>
<dd className="flex items-center gap-2">
{account.address}

<button
className="rounded bg-red-50 px-2 text-red-500 outline-none transition-colors hover:bg-red-100"
onClick={() => disconnect()}
>
Disconnect
</button>
</dd>

<dt className="font-semibold">Balance</dt>
<dd>
<Balance contract={account.address} />
</dd>
<Input disabled defaultValue={address} label="Account" />

<div className="grid grid-cols-5 gap-8">
<div className="col-span-2">
<Input
disabled={target === 'ETH'}
required={target === 'WETH'}
step="0.000000000000000001"
label="ETH"
name="eth"
placeholder="0"
type="number"
>
<Balance address={address} />
</Input>
</div>

<div className="flex items-center justify-center">
<button
type="button"
className="rounded p-2 hover:bg-gray-100"
onClick={() => setTarget(target === 'ETH' ? 'WETH' : 'ETH')}
>
{target === 'WETH' ? (
<>
<span className="sr-only">Swap WETH to ETH</span>
<ChevronRight />
</>
) : (
<>
<span className="sr-only">Swap ETH to WETH</span>
<ChevronLeft />
</>
)}
</button>
</div>

<div className="col-span-2">
<Input
disabled={target === 'WETH'}
required={target === 'ETH'}
step="0.000000000000000001"
label="WETH"
name="weth"
placeholder="0"
type="number"
>
<Balance
address={address}
contract="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
/>
</Input>
</div>
</div>

<button
type="submit"
className="rounded border border-transparent bg-gray-900 px-4 py-2 font-semibold text-white outline-none ring-2 ring-transparent hover:bg-gray-800 focus:border-purple-700 focus:ring-purple-400"
>
Transfer
</button>
</Fragment>
))}
</dl>
</form>
)
}
Loading

0 comments on commit bb84614

Please sign in to comment.