1
0
mirror of https://github.com/kremalicious/blog.git synced 2025-02-14 21:10:25 +01:00
This commit is contained in:
Matthias Kretschmann 2023-11-03 22:43:06 +00:00
parent a6f01ed2aa
commit afb2b16e69
Signed by: m
GPG Key ID: 606EEEF3C479A91F
21 changed files with 161 additions and 184 deletions

View File

@ -0,0 +1,24 @@
.web3 {
margin-top: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 4);
}
.form {
max-width: 100%;
width: 100%;
text-align: center;
min-height: 165px;
}
.disclaimer {
color: var(--text-color-light);
font-size: var(--font-size-small);
margin-top: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 6);
}
.disclaimer code {
background: none;
color: var(--text-color);
padding-left: 2px;
}

View File

@ -1,6 +1,6 @@
import { test, expect } from 'vitest' import { test, expect } from 'vitest'
import { render, fireEvent, screen } from '@testing-library/react' import { render, fireEvent, screen } from '@testing-library/react'
import Web3Form from '.' import { Web3Form } from './Form'
test('Web3Donation component', async () => { test('Web3Donation component', async () => {
render(<Web3Form />) render(<Web3Form />)

View File

@ -0,0 +1,48 @@
import { type ReactElement, useEffect } from 'react'
import { useAccount } from 'wagmi'
import { InputGroup } from '../Input'
import styles from './Form.module.css'
import { useStore } from '@nanostores/react'
import { $selectedToken, $isInitSend, $amount } from '@features/Web3/stores'
import siteConfig from '@config/blog.config'
import { Send } from '../Send'
import { RainbowKit } from '../RainbowKit/RainbowKit'
export function Web3Form(): ReactElement {
const { address: account } = useAccount()
const selectedToken = useStore($selectedToken)
const isInitSend = useStore($isInitSend)
const amount = useStore($amount)
const isDisabled = !account
// reset amount whenever token changes
useEffect(() => {
if (!selectedToken) return
$amount.set('')
}, [selectedToken])
return (
<div className={styles.web3}>
{isInitSend ? (
<Send />
) : (
<form
className={styles.form}
onSubmit={(e) => {
e.preventDefault()
if (amount === '' || amount === '0') return
$isInitSend.set(true)
}}
>
<RainbowKit />
<InputGroup isDisabled={isDisabled} />
<div className={styles.disclaimer}>
Sends tokens to my account{' '}
<code>{siteConfig.author.ether.ens}</code>
</div>
</form>
)}
</div>
)
}

View File

@ -1,45 +1 @@
import { type ReactElement, useEffect } from 'react' export * from './Form'
import { useAccount } from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import { InputGroup } from '../Input'
import styles from './index.module.css'
import { useStore } from '@nanostores/react'
import { $selectedToken, $isInitSend, $amount } from '@features/Web3/stores'
import siteConfig from '@config/blog.config'
import { Send } from '../Send'
export default function Web3Form(): ReactElement {
const { address: account } = useAccount()
const selectedToken = useStore($selectedToken)
const isInitSend = useStore($isInitSend)
const amount = useStore($amount)
const isDisabled = !account
// reset amount whenever token changes
useEffect(() => {
if (!selectedToken) return
$amount.set('')
}, [selectedToken])
return isInitSend ? (
<Send />
) : (
<form
className={styles.web3}
onSubmit={(e) => {
e.preventDefault()
if (amount === '' || amount === '0') return
$isInitSend.set(true)
}}
>
<div className={styles.rainbowkit}>
<ConnectButton chainStatus="full" showBalance={false} />
</div>
<InputGroup isDisabled={isDisabled} />
<div className={styles.disclaimer}>
Sends tokens to my account <code>{siteConfig.author.ether.ens}</code>
</div>
</form>
)
}

View File

@ -1,9 +1,8 @@
.inputGroup { .inputGroup {
--height: 60px; --height: 50px;
margin: auto; margin: auto;
position: relative; position: relative;
animation: fadeIn 0.8s ease-out backwards;
margin-top: calc(var(--spacer) / 3); margin-top: calc(var(--spacer) / 3);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -51,10 +50,6 @@
} }
} }
.inputInput::-webkit-inner-spin-button {
margin-left: -1rem;
}
:global([data-theme='dark']) .inputInput { :global([data-theme='dark']) .inputInput {
border-color: var(--border-color); border-color: var(--border-color);
} }
@ -85,13 +80,3 @@
color: var(--text-color); color: var(--text-color);
border-color: var(--text-color-light); border-color: var(--text-color-light);
} */ } */
@keyframes fadeIn {
from {
opacity: 0.01;
}
to {
opacity: 1;
}
}

View File

@ -3,7 +3,7 @@ import Input from '@components/Input'
import { Conversion } from '../Conversion' import { Conversion } from '../Conversion'
import styles from './InputGroup.module.css' import styles from './InputGroup.module.css'
import { TokenSelect } from '../TokenSelect' import { TokenSelect } from '../TokenSelect'
import { $amount, $isInitSend } from '@features/Web3/stores' import { $amount, $isInitSend, $selectedToken } from '@features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
export function InputGroup({ export function InputGroup({
@ -12,6 +12,7 @@ export function InputGroup({
isDisabled: boolean isDisabled: boolean
}): ReactElement { }): ReactElement {
const amount = useStore($amount) const amount = useStore($amount)
const selectedToken = useStore($selectedToken)
function handleChange(newAmount: string) { function handleChange(newAmount: string) {
$amount.set(newAmount) $amount.set(newAmount)
@ -37,7 +38,7 @@ export function InputGroup({
<button <button
className={`${styles.submit} btn btn-primary`} className={`${styles.submit} btn btn-primary`}
disabled={isDisabled || !amount} disabled={isDisabled || !amount || !selectedToken}
onClick={() => $isInitSend.set(true)} onClick={() => $isInitSend.set(true)}
> >
Preview Preview

View File

@ -1,4 +1,4 @@
import { formatEther } from 'viem' import { formatEther, formatUnits } from 'viem'
import { useAccount, useEnsName, useNetwork } from 'wagmi' import { useAccount, useEnsName, useNetwork } from 'wagmi'
import type { import type {
SendTransactionArgs, SendTransactionArgs,
@ -30,7 +30,12 @@ export function Data({
(txConfig as SendTransactionArgs)?.value || (txConfig as SendTransactionArgs)?.value ||
(txConfig as WriteContractPreparedArgs)?.request?.args?.[1] || (txConfig as WriteContractPreparedArgs)?.request?.args?.[1] ||
'0' '0'
const displayAmountFromConfig = formatEther(value as bigint) const displayAmountFromConfig =
selectedToken?.decimals === 18
? formatEther(value as bigint)
: selectedToken?.decimals
? formatUnits(value as bigint, selectedToken.decimals)
: '0'
return ( return (
<table className={styles.table} aria-disabled={isDisabled}> <table className={styles.table} aria-disabled={isDisabled}>

View File

@ -1,11 +1,3 @@
.web3 {
margin: calc(var(--spacer) / 2) auto calc(var(--spacer) / 4) auto;
max-width: 100%;
width: 100%;
text-align: center;
min-height: 165px;
}
.rainbowkit button > div { .rainbowkit button > div {
padding-left: 0 !important; padding-left: 0 !important;
} }
@ -52,50 +44,3 @@
border: none; border: none;
transform: none; transform: none;
} }
.disclaimer {
color: var(--text-color-light);
font-size: var(--font-size-small);
margin-top: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 6);
}
.disclaimer code {
background: none;
color: var(--text-color);
padding-left: 2px;
}
.message {
font-size: var(--font-size-small);
position: relative;
}
.message::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
/* ascii code for the ellipsis character */
content: '\2026';
width: 0;
position: absolute;
left: 100%;
bottom: 0;
}
.success {
composes: message;
color: green;
}
.success::after {
display: none;
}
@keyframes ellipsis {
to {
width: 0.75rem;
}
}

View File

@ -0,0 +1,10 @@
import { ConnectButton } from '@rainbow-me/rainbowkit'
import styles from './RainbowKit.module.css'
export function RainbowKit() {
return (
<div className={styles.rainbowkit}>
<ConnectButton chainStatus="full" showBalance={false} />
</div>
)
}

View File

@ -1,3 +0,0 @@
.send {
margin-top: calc(var(--spacer) / 2);
}

View File

@ -1,11 +1,10 @@
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $txHash } from '@features/Web3/stores' import { $txHash } from '@features/Web3/stores'
import styles from './Send.module.css'
import { Success } from '../Success' import { Success } from '../Success'
import { Preview } from '../Preview' import { Preview } from '../Preview'
export function Send() { export function Send() {
const txHash = useStore($txHash) const txHash = useStore($txHash)
return <div className={styles.send}>{txHash ? <Success /> : <Preview />}</div> return txHash ? <Success /> : <Preview />
} }

View File

@ -21,8 +21,8 @@
} }
.TokenLogo { .TokenLogo {
width: 34px; width: 28px;
height: 34px; height: 28px;
border-radius: 50%; border-radius: 50%;
margin-right: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 4);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
@ -34,8 +34,8 @@
.TokenLogo img { .TokenLogo img {
margin: 0; margin: 0;
width: 32px; width: 26px;
height: 32px; height: 26px;
border-radius: 50%; border-radius: 50%;
} }

View File

@ -6,11 +6,7 @@ import { Icon as ChevronsDown } from '@images/components/react/ChevronsDown'
import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp' import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp'
import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens' import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $setTokens, $tokens } from '@features/Web3/stores' import { $tokens, $selectedToken } from '@features/Web3/stores'
import {
$selectedToken,
$setSelectedToken
} from '@features/Web3/stores/selectedToken'
import { Loader } from '@components/Loader' import { Loader } from '@components/Loader'
import { useAccount } from 'wagmi' import { useAccount } from 'wagmi'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -28,19 +24,20 @@ export function TokenSelect() {
function handleValueChange(value: `0x${string}`) { function handleValueChange(value: `0x${string}`) {
const token = tokens?.find((token) => token.address === value) const token = tokens?.find((token) => token.address === value)
if (!token) return if (!token) return
$setSelectedToken(token) $selectedToken.set(token)
} }
// reset when no account connected // TODO: reset when no account connected
useEffect(() => { useEffect(() => {
if (!address && tokens?.length) { if (!address && tokens?.length && selectedToken) {
$setTokens(undefined) $tokens.set(undefined)
$selectedToken.set(undefined)
} }
}, [address]) }, [address])
return tokens && selectedToken ? ( return tokens ? (
<Select.Root <Select.Root
defaultValue={selectedToken?.address} defaultValue={selectedToken?.address || tokens[0].address}
onValueChange={(value: `0x${string}`) => handleValueChange(value)} onValueChange={(value: `0x${string}`) => handleValueChange(value)}
disabled={isLoading} disabled={isLoading}
> >

View File

@ -1,7 +1,7 @@
import { RainbowKitProvider } from '@rainbow-me/rainbowkit' import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiConfig } from 'wagmi' import { WagmiConfig } from 'wagmi'
import { wagmiConfig, chains, theme } from '../lib/rainbowkit' import { wagmiConfig, chains, theme } from '../lib/rainbowkit'
import Web3Form from './Form' import { Web3Form } from './Form'
export function Web3() { export function Web3() {
return ( return (

View File

@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import type { GetToken } from '@features/Web3/stores/tokens' import type { GetToken } from '@features/Web3/stores/tokens'
import { useNetwork, useAccount } from 'wagmi' import { useNetwork, useAccount } from 'wagmi'
import { $setTokens } from '@features/Web3/stores/tokens' import { $tokens } from '@features/Web3/stores'
import { $setSelectedToken } from '@features/Web3/stores/selectedToken' import { useStore } from '@nanostores/react'
const fetcher = (url: string) => fetch(url).then((res) => res.json()) const fetcher = (url: string) => fetch(url).then((res) => res.json())
const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL
@ -14,6 +14,7 @@ const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL
export function useFetchTokens() { export function useFetchTokens() {
const { chain } = useNetwork() const { chain } = useNetwork()
const { address } = useAccount() const { address } = useAccount()
const tokens = useStore($tokens)
const [url, setUrl] = useState<string | undefined>() const [url, setUrl] = useState<string | undefined>()
@ -32,15 +33,11 @@ export function useFetchTokens() {
// Sync with $tokens store // Sync with $tokens store
useEffect(() => { useEffect(() => {
if (!data) return if (!data) return
$setTokens(data)
}, [data])
// Set default token data to first item, if (data !== tokens) {
// which most of time is native token $tokens.set(data)
useEffect(() => { }
if (!data?.[0]?.chainId) return }, [data])
$setSelectedToken(data?.[0])
}, [data?.[0]?.chainId])
return fetchResults return fetchResults
} }

View File

@ -14,7 +14,14 @@ export async function prepare(
to: `0x${string}` | null | undefined, to: `0x${string}` | null | undefined,
chainId: number | undefined chainId: number | undefined
) { ) {
if (!chainId || !to || !amount || !selectedToken || !selectedToken?.address) if (
!chainId ||
!to ||
!amount ||
!selectedToken ||
!selectedToken?.address ||
!selectedToken?.decimals
)
return return
const isNative = selectedToken.address === '0x0' const isNative = selectedToken.address === '0x0'
@ -24,7 +31,7 @@ export async function prepare(
address: selectedToken.address, address: selectedToken.address,
abi: abiErc20Transfer, abi: abiErc20Transfer,
functionName: 'transfer', functionName: 'transfer',
args: [to, parseUnits(amount, selectedToken.decimals || 18)] args: [to, parseUnits(amount, selectedToken.decimals)]
} }
const config = isNative const config = isNative

View File

@ -46,8 +46,6 @@ export function usePrepareSend({
'this transaction exceeds the balance of the account.' 'this transaction exceeds the balance of the account.'
) )
) { ) {
setError('Insufficient funds')
} else {
setError(undefined) setError(undefined)
} }
} finally { } finally {

View File

@ -27,7 +27,13 @@ export function useSend({
$txHash.set(result?.hash) $txHash.set(result?.hash)
} catch (error: unknown) { } catch (error: unknown) {
console.error((error as Error).message) console.error((error as Error).message)
setError((error as Error).message)
// only expose useful errors in UI
if ((error as Error).message.includes('User rejected the request.')) {
setError(undefined)
} else {
setError((error as Error).message)
}
setIsError(true) setIsError(true)
} finally { } finally {
setIsLoading(false) setIsLoading(false)

View File

@ -1,21 +1,23 @@
import { action } from 'nanostores' import { atom } from 'nanostores'
import { persistentAtom } from '@nanostores/persistent' // import { persistentAtom } from '@nanostores/persistent'
import type { GetToken } from './tokens' import type { GetToken } from './tokens'
export const $selectedToken = persistentAtom<GetToken | undefined>( export const $selectedToken = atom<GetToken | undefined>()
'@kremalicious/selectedToken',
undefined,
{
encode: JSON.stringify,
decode: JSON.parse
}
)
export const $setSelectedToken = action( // export const $selectedToken = persistentAtom<GetToken | undefined>(
$selectedToken, // '@kremalicious/selectedToken',
'setSelectedToken', // undefined,
(store, token: GetToken) => { // {
store.set(token) // encode: JSON.stringify,
return store.get() // decode: JSON.parse
} // }
) // )
// export const $selectedToken.set = action(
// $selectedToken,
// 'setSelectedToken',
// (store, token: GetToken) => {
// store.set(token)
// return store.get()
// }
// )

View File

@ -1,13 +1,13 @@
import { action, atom } from 'nanostores' import { atom } from 'nanostores'
import type { GetToken } from './types' import type { GetToken } from './types'
export const $tokens = atom<GetToken[] | undefined>() export const $tokens = atom<GetToken[] | undefined>()
export const $setTokens = action( // export const $setTokens = action(
$tokens, // $tokens,
'setTokens', // 'setTokens',
(store, tokens: GetToken[] | undefined) => { // (store, tokens: GetToken[] | undefined) => {
store.set(tokens) // store.set(tokens)
return store.get() // return store.get()
} // }
) // )