1
0
Fork 0

handle sending

This commit is contained in:
Matthias Kretschmann 2023-10-28 22:34:58 +01:00
parent 15130e8d92
commit d8da825649
Signed by: m
GPG Key ID: 606EEEF3C479A91F
14 changed files with 215 additions and 123 deletions

View File

@ -1,5 +1,4 @@
import Web3Donation from './Web3Donation'
import config from '@config/blog.config'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiConfig } from 'wagmi'
import { wagmiConfig, chains, theme } from './Web3Donation/lib/rainbowkit'
@ -9,7 +8,7 @@ export default function Web3(): ReactElement {
return (
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider chains={chains} theme={theme}>
<Web3Donation address={config.author.ether} />
<Web3Donation />
</RainbowKitProvider>
</WagmiConfig>
)

View File

@ -1,18 +0,0 @@
export async function getFiat({
amount,
tokenId = 'ethereum'
}: {
amount: number
tokenId?: string
}): Promise<{ [key: string]: string }> {
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=eur%2Cusd`
const response = await fetch(url)
const json = await response.json()
if (!json) console.error(response.statusText)
const { usd, eur } = json[tokenId]
const dollar = (amount * usd).toFixed(2)
const euro = (amount * eur).toFixed(2)
return { dollar, euro }
}

View File

@ -16,6 +16,8 @@ export async function getTokens(
address: `0x${string}`,
chainId: number
): Promise<GetToken[]> {
if (!address || !chainId) return []
// const url = `http://localhost:3000/api/balance?address=${address}&chainId=${chainId}`
const url = `https://web3-api-kremalicious.vercel.app/api/balance?address=${address}&chainId=${chainId}`
const response = await fetch(url)

View File

@ -1,36 +1,20 @@
import { type ReactElement, useEffect, useState } from 'react'
import { type ReactElement } from 'react'
import styles from './Conversion.module.css'
import { getFiat } from '../../api/getFiat'
import type { GetToken } from '../../api/getTokens'
export function Conversion({
amount,
symbol
token
}: {
amount: string
symbol: string
token: GetToken | undefined
}): ReactElement {
const [conversion, setConversion] = useState({
euro: '0.00',
dollar: '0.00'
})
const { dollar, euro } = conversion
useEffect(() => {
async function getFiatResponse() {
try {
const tokenId = symbol === 'MATIC' ? 'matic-network' : 'ethereum'
const { dollar, euro } = await getFiat({
amount: Number(amount),
tokenId
})
setConversion({ euro, dollar })
} catch (error) {
console.error((error as Error).message)
}
}
getFiatResponse()
}, [amount, symbol])
const dollar = token?.price?.usd
? (Number(amount) * token?.price?.usd).toFixed(2)
: '0.00'
const euro = token?.price?.eur
? (Number(amount) * token?.price?.eur).toFixed(2)
: '0.00'
return (
<div className={styles.conversion}>

View File

@ -74,6 +74,10 @@
color: var(--text-color-light);
}
.disclaimer code {
color: var(--text-color);
}
@keyframes fadeIn {
from {
opacity: 0.01;

View File

@ -4,25 +4,26 @@ import { Conversion } from '../Conversion'
import styles from './InputGroup.module.css'
import { TokenSelect } from '../Tokens'
import config from '@config/blog.config'
import type { GetToken } from '../../api/getTokens'
export function InputGroup({
amount,
token,
isDisabled,
symbol,
setAmount,
setToken
setTokenSelected
}: {
amount: string
token: GetToken | undefined
isDisabled: boolean
symbol: string
setAmount(amount: string): void
setToken(token: string): void
setAmount: React.Dispatch<React.SetStateAction<string>>
setTokenSelected: React.Dispatch<React.SetStateAction<GetToken>>
}): ReactElement {
return (
<>
<div className={styles.inputGroup}>
<div className={styles.token}>
<TokenSelect setToken={setToken} />
<TokenSelect setTokenSelected={setTokenSelected} />
</div>
<Input
type="text"
@ -41,7 +42,7 @@ export function InputGroup({
Make it rain
</button>
</div>
<Conversion amount={amount} symbol={symbol} />
<Conversion amount={amount} token={token} />
<div className={styles.disclaimer}>
This form sends tokens to my account <code>{config.author.ether}</code>
</div>

View File

@ -0,0 +1,44 @@
import { parseEther } from 'viem'
import { useContractWrite, usePrepareContractWrite } from 'wagmi'
import siteConfig from '@config/blog.config'
import { abi } from './abi'
import { useEffect } from 'react'
export function SendErc20({
amount,
tokenAddress,
setSendFormData
}: {
amount: string
tokenAddress: `0x${string}` | undefined
setSendFormData: any
}) {
const { config } = usePrepareContractWrite({
address: tokenAddress,
abi,
functionName: 'transfer',
args: [siteConfig.author.ether, parseEther(amount)]
})
const {
data,
writeAsync: send,
isError,
isSuccess,
isLoading,
error
} = useContractWrite(config)
useEffect(() => {
setSendFormData({
data,
send,
isError,
isSuccess,
isLoading,
error
})
}, [data, send, isError, isSuccess, isLoading, error])
return <></>
}

View File

@ -0,0 +1,44 @@
import { parseEther } from 'viem'
import {
useNetwork,
usePrepareSendTransaction,
useSendTransaction
} from 'wagmi'
import siteConfig from '@config/blog.config'
import { useEffect } from 'react'
export function SendNative({
amount,
setSendFormData
}: {
amount: string
setSendFormData: any
}) {
const { chain } = useNetwork()
const { config } = usePrepareSendTransaction({
chainId: chain?.id,
to: siteConfig.author.ether,
value: parseEther(amount)
})
const {
data,
sendTransactionAsync: send,
isError,
isSuccess,
isLoading,
error
} = useSendTransaction(config)
useEffect(() => {
setSendFormData({
data,
send,
isError,
isSuccess,
isLoading,
error
})
}, [data, send, isError, isSuccess, isLoading, error])
return <></>
}

View File

@ -0,0 +1,13 @@
export const abi = [
{
constant: false,
inputs: [
{ name: '_to', type: 'address' },
{ name: '_value', type: 'uint256' }
],
name: 'transfer',
outputs: [{ name: 'success', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function'
}
]

View File

@ -0,0 +1,2 @@
export * from './SendErc20'
export * from './SendNative'

View File

@ -6,13 +6,13 @@ import { Check } from '@images/components/react'
import type { GetToken } from '../../api/getTokens'
interface SelectItemProps extends HTMLAttributes<HTMLDivElement> {
token: GetToken
token: GetToken | undefined
}
export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
({ className, token, ...props }, forwardedRef) => {
const balance =
token.balance && token.symbol
token?.balance && token?.symbol
? formatCurrency(token.balance, token.symbol, 'en', false, {
decimalPlaces: 3,
significantFigures: 3
@ -20,28 +20,30 @@ export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
: 0
const valueInUsd =
token.balance && token.price?.usd ? token.balance * token.price.usd : 0
token?.balance && token?.price?.usd
? token?.balance * token?.price.usd
: 0
const valueInUsdFormatted = formatCurrency(valueInUsd, 'USD', 'en')
return balance && parseInt(balance) !== 0 && valueInUsd >= 1 ? (
<Select.Item
className={`${className ? className : ''} Token`}
{...props}
value={token.address}
title={token.address}
value={token?.address || ''}
title={token?.address}
ref={forwardedRef}
>
<Select.ItemText>
<span className="TokenLogo">
{token.logo ? (
{token?.logo ? (
<img src={token.logo} width="32" height="32" />
) : (
token.symbol?.substring(0, 3)
token?.symbol?.substring(0, 3)
)}
</span>
</Select.ItemText>
<div>
<h3 className="TokenName">{token.name}</h3>
<h3 className="TokenName">{token?.name}</h3>
<p className="TokenBalance">{balance}</p>
</div>
<div className="TokenValue">{valueInUsdFormatted}</div>

View File

@ -4,11 +4,13 @@ import { Token } from './Token'
import { ChevronDown, ChevronsDown, ChevronsUp } from '@images/components/react'
import { useTokens } from '../../hooks/useTokens'
import { TokenLoading } from './TokenLoading'
import type { GetToken } from '../../api/getTokens'
import { useEffect } from 'react'
export function TokenSelect({
setToken
setTokenSelected
}: {
setToken: (token: string) => void
setTokenSelected: React.Dispatch<React.SetStateAction<GetToken>>
}) {
const { data: tokens, isLoading } = useTokens()
@ -16,10 +18,19 @@ export function TokenSelect({
<Token key={token.address} token={token} />
))
function handleValueChange(value: `0x${string}`) {
const token = tokens?.find((token) => token.address === value)
if (!token) return
setTokenSelected(token)
}
// set default token data
useEffect(() => handleValueChange('0x0'), [])
return tokens ? (
<Select.Root
defaultValue={tokens?.[0].address}
onValueChange={(value) => setToken(value)}
onValueChange={(value: `0x${string}`) => handleValueChange(value)}
disabled={!tokens || isLoading}
>
<Select.Trigger

View File

@ -1,98 +1,102 @@
import { type ReactElement, useState } from 'react'
import { type ReactElement, useState, useEffect } from 'react'
import { useDebounce } from 'use-debounce'
import { parseEther } from 'viem'
import {
useAccount,
useNetwork,
usePrepareSendTransaction,
useSendTransaction
} from 'wagmi'
import { useAccount } from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import Alert, { getTransactionMessage } from './components/Alert/Alert'
import { InputGroup } from './components/Input'
import styles from './index.module.css'
import { SendNative, SendErc20 } from './components/Send'
import type { GetToken } from './api/getTokens'
export default function Web3Donation({
address
}: {
address: string
}): ReactElement {
export default function Web3Donation(): ReactElement {
const { address: account } = useAccount()
const { chain } = useNetwork()
const [amount, setAmount] = useState('')
const [debouncedAmount] = useDebounce(amount, 500)
const [token, setToken] = useState<string>()
const [tokenSelected, setTokenSelected] = useState<GetToken>({
address: '0x0'
} as any)
const [message, setMessage] = useState<{ status: string; text: string }>()
const [transactionHash, setTransactionHash] = useState<string>()
const [sendFormData, setSendFormData] = useState<{
data: { hash: `0x${string}` }
send: () => Promise<void>
isLoading: boolean
isSuccess: boolean
isError: boolean
error: Error | null
}>()
// dummy
if (token) {
console.log(token)
}
const { data, send, isLoading, isSuccess, isError, error } =
sendFormData || {}
const { config } = usePrepareSendTransaction({
chainId: chain?.id,
to: address,
value: parseEther(debouncedAmount)
})
const { sendTransactionAsync, isError, isSuccess } =
useSendTransaction(config)
useEffect(() => {
if (!isError || !error) return
setMessage(
error.message.includes('User rejected the request.')
? undefined
: {
status: 'error',
text: error?.message as string
}
)
}, [isError])
useEffect(() => {
if (!isLoading) return
async function handleSendTransaction() {
setMessage({
status: 'loading',
text: getTransactionMessage().waitingForUser
text: getTransactionMessage().waitingConfirmation
})
}, [isLoading])
try {
const result = sendTransactionAsync && (await sendTransactionAsync())
useEffect(() => {
if (!isSuccess) return
if (isError) {
throw new Error(undefined)
}
setTransactionHash(result?.hash)
setMessage({
status: 'loading',
text: getTransactionMessage().waitingConfirmation
})
if (isSuccess) {
setMessage({
status: 'success',
text: getTransactionMessage().success
})
}
} catch (error) {
setMessage(undefined)
}
}
setMessage({
status: 'success',
text: getTransactionMessage().success
})
}, [isSuccess])
const isDisabled = !account
return (
<form
className={styles.web3}
onSubmit={(e) => {
onSubmit={async (e) => {
e.preventDefault()
handleSendTransaction()
if (!send || amount === '' || amount === '0') return
await send()
}}
>
<ConnectButton chainStatus="icon" showBalance={false} />
{message ? (
<Alert message={message} transactionHash={transactionHash} />
<Alert message={message} transactionHash={data?.hash} />
) : (
<InputGroup
amount={amount}
symbol={chain?.nativeCurrency?.symbol || 'ETH'}
token={tokenSelected}
setAmount={setAmount}
setToken={setToken}
setTokenSelected={setTokenSelected}
isDisabled={isDisabled}
/>
)}
{tokenSelected?.address === '0x0' ? (
<SendNative
amount={debouncedAmount}
setSendFormData={setSendFormData}
/>
) : (
<SendErc20
amount={debouncedAmount}
tokenAddress={tokenSelected?.address}
setSendFormData={setSendFormData}
/>
)}
</form>
)
}

View File

@ -37,12 +37,12 @@ const coins = Object.entries(config.author).filter(
<BackButton />
<div class="content">
<h3 class="subTitle">Send from your browser wallet.</h3>
<h3 class="subTitle">Send from your browser wallet</h3>
<Web3 client:only="react" />
</div>
<div class="content">
<h3 class="subTitle">Send from any wallet.</h3>
<h3 class="subTitle">Send from any wallet</h3>
{
coins.map(([key, value]: [key: string, value: string]) => (