1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-22 09:56:51 +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 { render, fireEvent, screen } from '@testing-library/react'
import Web3Form from '.'
import { Web3Form } from './Form'
test('Web3Donation component', async () => {
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'
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>
)
}
export * from './Form'

View File

@ -1,9 +1,8 @@
.inputGroup {
--height: 60px;
--height: 50px;
margin: auto;
position: relative;
animation: fadeIn 0.8s ease-out backwards;
margin-top: calc(var(--spacer) / 3);
display: flex;
flex-direction: column;
@ -51,10 +50,6 @@
}
}
.inputInput::-webkit-inner-spin-button {
margin-left: -1rem;
}
:global([data-theme='dark']) .inputInput {
border-color: var(--border-color);
}
@ -85,13 +80,3 @@
color: var(--text-color);
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 styles from './InputGroup.module.css'
import { TokenSelect } from '../TokenSelect'
import { $amount, $isInitSend } from '@features/Web3/stores'
import { $amount, $isInitSend, $selectedToken } from '@features/Web3/stores'
import { useStore } from '@nanostores/react'
export function InputGroup({
@ -12,6 +12,7 @@ export function InputGroup({
isDisabled: boolean
}): ReactElement {
const amount = useStore($amount)
const selectedToken = useStore($selectedToken)
function handleChange(newAmount: string) {
$amount.set(newAmount)
@ -37,7 +38,7 @@ export function InputGroup({
<button
className={`${styles.submit} btn btn-primary`}
disabled={isDisabled || !amount}
disabled={isDisabled || !amount || !selectedToken}
onClick={() => $isInitSend.set(true)}
>
Preview

View File

@ -1,4 +1,4 @@
import { formatEther } from 'viem'
import { formatEther, formatUnits } from 'viem'
import { useAccount, useEnsName, useNetwork } from 'wagmi'
import type {
SendTransactionArgs,
@ -30,7 +30,12 @@ export function Data({
(txConfig as SendTransactionArgs)?.value ||
(txConfig as WriteContractPreparedArgs)?.request?.args?.[1] ||
'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 (
<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 {
padding-left: 0 !important;
}
@ -52,50 +44,3 @@
border: 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 { $txHash } from '@features/Web3/stores'
import styles from './Send.module.css'
import { Success } from '../Success'
import { Preview } from '../Preview'
export function Send() {
const txHash = useStore($txHash)
return <div className={styles.send}>{txHash ? <Success /> : <Preview />}</div>
return txHash ? <Success /> : <Preview />
}

View File

@ -21,8 +21,8 @@
}
.TokenLogo {
width: 34px;
height: 34px;
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: calc(var(--spacer) / 4);
border: 1px solid var(--border-color);
@ -34,8 +34,8 @@
.TokenLogo img {
margin: 0;
width: 32px;
height: 32px;
width: 26px;
height: 26px;
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 { useFetchTokens } from '@features/Web3/hooks/useFetchTokens'
import { useStore } from '@nanostores/react'
import { $setTokens, $tokens } from '@features/Web3/stores'
import {
$selectedToken,
$setSelectedToken
} from '@features/Web3/stores/selectedToken'
import { $tokens, $selectedToken } from '@features/Web3/stores'
import { Loader } from '@components/Loader'
import { useAccount } from 'wagmi'
import { useEffect } from 'react'
@ -28,19 +24,20 @@ export function TokenSelect() {
function handleValueChange(value: `0x${string}`) {
const token = tokens?.find((token) => token.address === value)
if (!token) return
$setSelectedToken(token)
$selectedToken.set(token)
}
// reset when no account connected
// TODO: reset when no account connected
useEffect(() => {
if (!address && tokens?.length) {
$setTokens(undefined)
if (!address && tokens?.length && selectedToken) {
$tokens.set(undefined)
$selectedToken.set(undefined)
}
}, [address])
return tokens && selectedToken ? (
return tokens ? (
<Select.Root
defaultValue={selectedToken?.address}
defaultValue={selectedToken?.address || tokens[0].address}
onValueChange={(value: `0x${string}`) => handleValueChange(value)}
disabled={isLoading}
>

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,13 @@ export function useSend({
$txHash.set(result?.hash)
} catch (error: unknown) {
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)
} finally {
setIsLoading(false)

View File

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