1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-22 09:56:51 +01:00

automatic network switching

This commit is contained in:
Matthias Kretschmann 2024-03-14 13:18:21 +00:00
parent d1315e7912
commit cbbb81e391
Signed by: m
GPG Key ID: 606EEEF3C479A91F
12 changed files with 84 additions and 48 deletions

View File

@ -32,8 +32,15 @@ table[aria-disabled='true'] {
align-items: center; align-items: center;
} }
.table :global(.TokenLogo), .table :global(.TokenLogo) {
.table :global(.TokenLogo) img {
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 50%;
border: 1px solid var(--border-color);
}
.table :global(.TokenLogo) img {
width: 100%;
height: 100%;
border-radius: 50%;
} }

View File

@ -1,4 +1,4 @@
import { useAccount, useEnsName } from 'wagmi' import { useAccount, useChains, useEnsName } from 'wagmi'
import styles from './Data.module.css' import styles from './Data.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $amount, $selectedToken } from '@features/Web3/stores' import { $amount, $selectedToken } from '@features/Web3/stores'
@ -13,11 +13,17 @@ export function Data({
ensResolved: string | null | undefined ensResolved: string | null | undefined
isDisabled: boolean isDisabled: boolean
}) { }) {
const { address: from, chain } = useAccount() const chains = useChains()
const { address: from } = useAccount()
const { data: ensFrom } = useEnsName({ address: from, chainId: 1 }) const { data: ensFrom } = useEnsName({ address: from, chainId: 1 })
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const amount = useStore($amount) const amount = useStore($amount)
const networkName = chains.filter(
(chain) => chain.id === selectedToken?.chainId
)[0].name
return ( return (
<table className={styles.table} aria-disabled={isDisabled}> <table className={styles.table} aria-disabled={isDisabled}>
<tbody> <tbody>
@ -54,7 +60,10 @@ export function Data({
<tr> <tr>
<td className={styles.label}>on</td> <td className={styles.label}>on</td>
<td> <td>
<span className={styles.network}>{chain?.name}</span> <div className="TokenLogo">
<img src={selectedToken?.chainLogo || ''} />
</div>
<span className={styles.network}>{networkName}</span>
</td> </td>
</tr> </tr>

View File

@ -18,12 +18,6 @@ export function Preview() {
const { handleSend, isLoading, error } = useSend() const { handleSend, isLoading, error } = useSend()
// TODO: Cancel flow if chain changes in preview as this can mess with token selection
// useEffect(() => {
// if (!chain?.id || $isInitSend.get() === false) return
// $isInitSend.set(false)
// }, [chain?.id])
return ( return (
<> <>
<Data to={to} ensResolved={ensResolved} isDisabled={isLoading} /> <Data to={to} ensResolved={ensResolved} isDisabled={isLoading} />

View File

@ -3,18 +3,12 @@
} }
.rainbowkit > div:first-child { .rainbowkit > div:first-child {
display: flex; /* display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
justify-content: space-between; justify-content: space-between; */
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
/* hide the account icon, and hope nothing else */
/* .rainbowkit button [aria-hidden] {
display: none;
} */
.rainbowkit [aria-label='Chain Selector'], .rainbowkit [aria-label='Chain Selector'],
.rainbowkit [data-testid='rk-account-button'] div { .rainbowkit [data-testid='rk-account-button'] div {
font-weight: var(--font-weight-base); font-weight: var(--font-weight-base);

View File

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

View File

@ -21,8 +21,8 @@
} }
.TokenLogo { .TokenLogo {
width: 28px; width: 32px;
height: 28px; height: 32px;
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);
@ -30,15 +30,24 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
position: relative;
} }
.TokenLogo img { .TokenLogoImage {
margin: 0; margin: 0;
width: 26px;
height: 26px;
border-radius: 50%; border-radius: 50%;
} }
.TokenChainLogo {
width: 16px;
height: 16px;
position: absolute;
bottom: -5px;
right: -5px;
border-radius: 50%;
border: 2px solid var(--border-color);
}
.TokenName, .TokenName,
.TokenBalance { .TokenBalance {
margin: 0; margin: 0;

View File

@ -41,10 +41,21 @@ export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
<Select.ItemText> <Select.ItemText>
<span className="TokenLogo"> <span className="TokenLogo">
{token?.logo ? ( {token?.logo ? (
<img src={token.logo} width="32" height="32" /> <img
src={token.logo}
width="32"
height="32"
className="TokenLogoImage"
/>
) : ( ) : (
token?.symbol?.substring(0, 3) token?.symbol?.substring(0, 3)
)} )}
<img
src={token?.chainLogo}
width="20"
height="20"
className="TokenChainLogo"
/>
</span> </span>
</Select.ItemText> </Select.ItemText>
<div> <div>

View File

@ -17,7 +17,7 @@ export function TokenSelect() {
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const items = tokens?.map((token) => ( const items = tokens?.map((token) => (
<Token key={token.address} token={token} /> <Token key={`${token.address}-${token.chainId}`} token={token} />
)) ))
function handleValueChange(value: `0x${string}`) { function handleValueChange(value: `0x${string}`) {
@ -32,12 +32,12 @@ export function TokenSelect() {
useEffect(() => { useEffect(() => {
if (selectedToken?.address || !tokens || !tokens?.length) return if (selectedToken?.address || !tokens || !tokens?.length) return
handleValueChange('0x0') // select ETH mainnet token
handleValueChange('0x0-1')
}, [tokens, selectedToken]) }, [tokens, selectedToken])
return tokens && address ? ( return tokens && address ? (
<Select.Root <Select.Root
// defaultValue={selectedToken?.address || tokens[0].address}
value={selectedToken?.address} value={selectedToken?.address}
onValueChange={(value: `0x${string}`) => handleValueChange(value)} onValueChange={(value: `0x${string}`) => handleValueChange(value)}
disabled={isLoading} disabled={isLoading}

View File

@ -2,12 +2,13 @@ export type GetToken = {
address: `0x${string}` address: `0x${string}`
balance: number | undefined balance: number | undefined
chainId: number chainId: number
name: string | null chainLogo: string | undefined
symbol: string | null name: string | undefined
decimals: number | null symbol: string | undefined
logo: string | null decimals: number | undefined
logo: string | undefined
price: { price: {
usd: number | null usd: number | undefined
eur: number | null eur: number | undefined
} }
} }

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import useSWR, { type SWRResponse } from 'swr' import useSWR, { type SWRResponse } from 'swr'
import { useChainId, useAccount } from 'wagmi' import { useAccount, useChains } from 'wagmi'
import type { GetToken } from './types' import type { GetToken } from './types'
const fetcher = (url: string) => fetch(url).then((res) => res.json()) const fetcher = (url: string) => fetch(url).then((res) => res.json())
@ -10,26 +10,23 @@ const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL
// Wrapper for fetching user tokens with swr. // Wrapper for fetching user tokens with swr.
// //
export function useFetchTokens(): SWRResponse<GetToken[] | undefined, Error> { export function useFetchTokens(): SWRResponse<GetToken[] | undefined, Error> {
const chainId = useChainId()
const { address } = useAccount() const { address } = useAccount()
// const { chains } = useConfig() const chains = useChains()
const [url, setUrl] = useState<string | undefined>() const [url, setUrl] = useState<string | undefined>()
const fetchResults = useSWR<GetToken[] | undefined>(url, fetcher) const fetchResults = useSWR<GetToken[] | undefined>(url, fetcher)
// Set url only after we have all data loaded on client, // Set url only after we have all data loaded on client,
// preventing initial fetch. // preventing initial fetch.
useEffect(() => { useEffect(() => {
if (!address || !chainId) { if (!address || !chains) {
setUrl(undefined) setUrl(undefined)
return return
} }
// const chainIds = chains.map((chain) => chain.id).join(',') const chainIds = chains.map((chain) => chain.id).join(',')
const url = `${apiUrl}/balance?address=${address}&chainIds=${chainId}` const url = `${apiUrl}/balance?address=${address}&chainIds=${chainIds}`
setUrl(url) setUrl(url)
}, [address, chainId]) }, [address, chains])
return fetchResults return fetchResults
} }

View File

@ -13,7 +13,7 @@ export async function send(
) { ) {
if (!selectedToken?.decimals || !amount || !to) return if (!selectedToken?.decimals || !amount || !to) return
const isNative = selectedToken.address === '0x0' const isNative = selectedToken.address.startsWith('0x0')
const requestNative = { chainId, to, value: parseEther(amount) } const requestNative = { chainId, to, value: parseEther(amount) }
const requestErc20 = { const requestErc20 = {
chainId, chainId,

View File

@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react'
import { useState } from 'react' import { useState } from 'react'
import { send } from './send' import { send } from './send'
import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage' import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
import { useAccount, useConfig, useEnsAddress } from 'wagmi' import { useAccount, useConfig, useEnsAddress, useSwitchChain } from 'wagmi'
import siteConfig from '@config/blog.config' import siteConfig from '@config/blog.config'
export function useSend() { export function useSend() {
@ -13,17 +13,31 @@ export function useSend() {
const { chainId } = useAccount() const { chainId } = useAccount()
const { ens } = siteConfig.author.ether const { ens } = siteConfig.author.ether
const { data: to } = useEnsAddress({ name: ens, chainId: 1 }) const { data: to } = useEnsAddress({ name: ens, chainId: 1 })
const { switchChain } = useSwitchChain()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
async function handleSend() { async function handleSend() {
if (!selectedToken || !amount || !to) return
// switch chains first
if (chainId !== selectedToken.chainId) {
switchChain({ chainId: selectedToken.chainId })
}
try { try {
setIsError(false) setIsError(false)
setError(undefined) setError(undefined)
setIsLoading(true) setIsLoading(true)
const hash = await send(config, selectedToken, amount, to, chainId) const hash = await send(
config,
selectedToken,
amount,
to,
selectedToken.chainId
)
if (hash) $txHash.set(hash) if (hash) $txHash.set(hash)
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = (error as Error).message const errorMessage = (error as Error).message