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

web3 updates

This commit is contained in:
Matthias Kretschmann 2024-03-13 01:20:29 +00:00
parent 592498fb9a
commit feee0c678b
Signed by: m
GPG Key ID: 606EEEF3C479A91F
22 changed files with 8841 additions and 2018 deletions

10463
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,8 @@
"@nanostores/query": "^0.2.10", "@nanostores/query": "^0.2.10",
"@nanostores/react": "^0.7.2", "@nanostores/react": "^0.7.2",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@rainbow-me/rainbowkit": "^1.3.0", "@rainbow-me/rainbowkit": "^2.0.2",
"@tanstack/react-query": "^5.27.5",
"astro": "4.5.2", "astro": "4.5.2",
"astro-expressive-code": "^0.33.4", "astro-expressive-code": "^0.33.4",
"astro-redirect-from": "^1.0.6", "astro-redirect-from": "^1.0.6",
@ -76,8 +77,8 @@
"sharp": "^0.33.2", "sharp": "^0.33.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"swr": "^2.2.5", "swr": "^2.2.5",
"viem": "^1.19.13", "viem": "^2.8.5",
"wagmi": "^1.4.12" "wagmi": "^2.5.7"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",

View File

@ -1,42 +1,22 @@
import { formatEther, formatUnits } from 'viem' import { useAccount, useEnsName } from 'wagmi'
import { useAccount, useEnsName, useNetwork } from 'wagmi'
import type {
SendTransactionArgs,
WriteContractPreparedArgs
} from 'wagmi/actions'
import styles from './Data.module.css' import styles from './Data.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken } from '@features/Web3/stores' import { $amount, $selectedToken } from '@features/Web3/stores'
import { truncateAddress } from '@features/Web3/lib/truncateAddress' import { truncateAddress } from '@features/Web3/lib/truncateAddress'
export function Data({ export function Data({
to, to,
ensResolved, ensResolved,
txConfig,
isDisabled isDisabled
}: { }: {
to: `0x${string}` | null | undefined to: `0x${string}` | null | undefined
ensResolved: string | null | undefined ensResolved: string | null | undefined
txConfig: SendTransactionArgs | WriteContractPreparedArgs | undefined
isDisabled: boolean isDisabled: boolean
}) { }) {
const { chain } = useNetwork() const { address: from, chain } = useAccount()
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)
// Derive display values in preview from actual tx config
// instead from our form stores
const value =
(txConfig as SendTransactionArgs)?.value ||
(txConfig as WriteContractPreparedArgs)?.request?.args?.[1] ||
'0'
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}>
@ -66,7 +46,7 @@ export function Data({
/> />
</div> </div>
<span className={styles.amount}> <span className={styles.amount}>
{displayAmountFromConfig} {selectedToken?.symbol} {amount} {selectedToken?.symbol}
</span> </span>
</td> </td>
</tr> </tr>

View File

@ -1,5 +1,4 @@
import { Loader } from '@components/Loader' import { Loader } from '@components/Loader'
import { usePrepareSend } from '@features/Web3/hooks/usePrepareSend'
import { useSend } from '@features/Web3/hooks/useSend' import { useSend } from '@features/Web3/hooks/useSend'
import { $isInitSend } from '@features/Web3/stores' import { $isInitSend } from '@features/Web3/stores'
import { useEnsAddress, useEnsName } from 'wagmi' import { useEnsAddress, useEnsName } from 'wagmi'
@ -17,12 +16,7 @@ export function Preview() {
chainId: 1 chainId: 1
}) })
const { const { handleSend, isLoading, error } = useSend()
data: txConfig,
error: prepareError,
isError: isPrepareError
} = usePrepareSend({ to })
const { handleSend, isLoading, error } = useSend({ txConfig })
// TODO: Cancel flow if chain changes in preview as this can mess with token selection // TODO: Cancel flow if chain changes in preview as this can mess with token selection
// useEffect(() => { // useEffect(() => {
@ -32,16 +26,9 @@ export function Preview() {
return ( return (
<> <>
<Data <Data to={to} ensResolved={ensResolved} isDisabled={isLoading} />
to={to}
ensResolved={ensResolved}
txConfig={txConfig}
isDisabled={isLoading}
/>
{error || prepareError ? ( {error ? <div className={styles.alert}>{error}</div> : null}
<div className={styles.alert}>{error || prepareError}</div>
) : null}
<footer className={styles.actions}> <footer className={styles.actions}>
<button <button
@ -50,7 +37,7 @@ export function Preview() {
await handleSend() await handleSend()
}} }}
className="btn btn-primary" className="btn btn-primary"
disabled={isLoading || !txConfig || isPrepareError} disabled={isLoading}
> >
{isLoading ? <Loader /> : 'Make it rain'} {isLoading ? <Loader /> : 'Make it rain'}
</button> </button>

View File

@ -1,18 +1,18 @@
import { $txHash, $isInitSend } from '@features/Web3/stores' import { $txHash, $isInitSend } from '@features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import styles from './Success.module.css' import styles from './Success.module.css'
import { useNetwork } from 'wagmi' import { useAccount } from 'wagmi'
import { ExplorerLink } from './ExplorerLink' import { ExplorerLink } from './ExplorerLink'
const title = `You're amazing, thanks!` const title = `You're amazing, thanks!`
const description = `Your transaction is on its way. You can check the status on` const description = `Your transaction is on its way. You can check the status on`
export function Success() { export function Success() {
const { chain } = useNetwork() const account = useAccount()
const txHash = useStore($txHash) const txHash = useStore($txHash)
const explorerName = chain?.blockExplorers?.default.name const explorerName = account?.chain?.blockExplorers?.default.name
const explorerUrl = chain?.blockExplorers?.default.url const explorerUrl = account?.chain?.blockExplorers?.default.url
return ( return (
<div className={styles.success}> <div className={styles.success}>

View File

@ -1,14 +1,20 @@
import '../lib/polyfills'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit' import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiConfig } from 'wagmi' import { WagmiProvider } from 'wagmi'
import { wagmiConfig, chains, theme } from '../lib/rainbowkit' import { wagmiConfig, theme } from '../lib/rainbowkit'
import { Web3Form } from './Form' import { Web3Form } from './Form'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export function Web3() { export function Web3() {
return ( return (
<WagmiConfig config={wagmiConfig}> <WagmiProvider config={wagmiConfig}>
<RainbowKitProvider chains={chains} theme={theme}> <QueryClientProvider client={queryClient}>
<Web3Form /> <RainbowKitProvider theme={theme}>
</RainbowKitProvider> <Web3Form />
</WagmiConfig> </RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
) )
} }

View File

@ -4,7 +4,7 @@ import { useFetchTokens } from './useFetchTokens'
test('useFetchTokens does not fetch anything when no chain or address are present', async () => { test('useFetchTokens does not fetch anything when no chain or address are present', async () => {
vi.mock('wagmi', () => ({ vi.mock('wagmi', () => ({
useNetwork: () => ({ chain: undefined }), useChainId: () => undefined,
useAccount: () => ({ address: undefined }) useAccount: () => ({ address: 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 { useNetwork, useAccount } from 'wagmi' import { useChainId, useAccount } 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,7 +10,7 @@ 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 { chain } = useNetwork() const chainId = useChainId()
const { address } = useAccount() const { address } = useAccount()
const [url, setUrl] = useState<string | undefined>() const [url, setUrl] = useState<string | undefined>()
@ -20,14 +20,14 @@ export function useFetchTokens(): SWRResponse<GetToken[] | undefined, Error> {
// 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 || !chain?.id) { if (!address || !chainId) {
setUrl(undefined) setUrl(undefined)
return return
} }
const url = `${apiUrl}/balance?address=${address}&chainId=${chain?.id}` const url = `${apiUrl}/balance?address=${address}&chainId=${chainId}`
setUrl(url) setUrl(url)
}, [address, chain?.id]) }, [address, chainId])
return fetchResults return fetchResults
} }

View File

@ -1 +0,0 @@
export * from './usePrepareSend'

View File

@ -1,43 +0,0 @@
import { test, expect, vi } from 'vitest'
import { prepare } from './prepare'
import * as wagmiActionsMock from '../../../../../test/__mocks__/wagmi/actions'
test('prepare with undefined params', async () => {
try {
await prepare(undefined, undefined, undefined, undefined)
expect(true).toBe(false)
} catch (e) {
expect(true).toBe(true)
}
})
test('prepare with isNative true uses correct method', async () => {
const selectedToken = {
address: '0x0',
decimals: 18
// Add other required properties here
}
const amount = '1'
const to = '0xabcdef1234567890'
const chainId = 1
const spy = vi.spyOn(wagmiActionsMock, 'prepareSendTransaction')
await prepare(selectedToken as any, amount, to, chainId)
expect(spy).toHaveBeenCalledOnce()
})
test('prepare with isNative false uses correct method', async () => {
const selectedToken = {
address: '0xabcdef1234567890',
decimals: 18
}
const amount = '1'
const to = '0xabcdef1234567890'
const chainId = 1
const spy = vi.spyOn(wagmiActionsMock, 'prepareWriteContract')
await prepare(selectedToken as any, amount, to, chainId)
expect(spy).toHaveBeenCalledOnce()
})

View File

@ -1,42 +0,0 @@
import { parseEther, parseUnits } from 'viem'
import {
prepareSendTransaction,
prepareWriteContract,
type SendTransactionArgs,
type WriteContractPreparedArgs
} from 'wagmi/actions'
import { abiErc20Transfer } from './abiErc20Transfer'
import type { GetToken } from '../useFetchTokens'
export async function prepare(
selectedToken: GetToken | undefined,
amount: string | undefined,
to: `0x${string}` | null | undefined,
chainId: number | undefined
) {
if (
!chainId ||
!to ||
!amount ||
!selectedToken ||
!selectedToken?.address ||
!selectedToken?.decimals
)
return
const isNative = selectedToken.address === '0x0'
const requestNative = { chainId, to, value: parseEther(amount) }
const requestErc20 = {
chainId,
address: selectedToken.address,
abi: abiErc20Transfer,
functionName: 'transfer',
args: [to, parseUnits(amount, selectedToken.decimals)]
}
const config = isNative
? ((await prepareSendTransaction(requestNative)) as SendTransactionArgs)
: ((await prepareWriteContract(requestErc20)) as WriteContractPreparedArgs)
return config
}

View File

@ -1,59 +0,0 @@
import { useState, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { useNetwork } from 'wagmi'
import type {
SendTransactionArgs,
WriteContractPreparedArgs
} from 'wagmi/actions'
import { $amount, $selectedToken } from '@features/Web3/stores'
import { prepare } from './prepare'
export function usePrepareSend({
to
}: {
to: `0x${string}` | null | undefined
}) {
const selectedToken = useStore($selectedToken)
const amount = useStore($amount)
const { chain } = useNetwork()
const [txConfig, setTxConfig] = useState<
SendTransactionArgs | WriteContractPreparedArgs
>()
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const [error, setError] = useState<string | undefined>()
useEffect(() => {
async function init() {
if (!selectedToken || !amount || !to || !chain?.id) return
setError(undefined)
setIsError(false)
setIsLoading(true)
try {
const config = await prepare(selectedToken, amount, to, chain.id)
setTxConfig(config)
} catch (error: unknown) {
console.error((error as Error).message)
setIsError(true)
// only expose useful errors in UI
if (
(error as Error).message.includes(
'this transaction exceeds the balance of the account.'
)
) {
setError(undefined)
}
} finally {
setIsLoading(false)
}
}
init()
}, [selectedToken || amount || to || chain?.id])
return { data: txConfig, isLoading, isError, error }
}

View File

@ -3,7 +3,13 @@ import { send } from './send'
import * as wagmiActionsMock from '../../../../../test/__mocks__/wagmi/actions' import * as wagmiActionsMock from '../../../../../test/__mocks__/wagmi/actions'
test('with undefined params', async () => { test('with undefined params', async () => {
const result = await send(undefined, undefined) const result = await send(
undefined as any,
undefined as any,
undefined as any,
undefined as any,
undefined as any
)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
@ -11,10 +17,10 @@ test('with isNative true uses correct method', async () => {
const selectedToken = { const selectedToken = {
address: '0x0', address: '0x0',
decimals: 18 decimals: 18
} } as any
const spy = vi.spyOn(wagmiActionsMock, 'sendTransaction') const spy = vi.spyOn(wagmiActionsMock, 'sendTransaction')
await send(selectedToken as any, {} as any) await send({} as any, selectedToken, '1', '0xabcdef1234567890', 1)
expect(spy).toHaveBeenCalledOnce() expect(spy).toHaveBeenCalledOnce()
}) })
@ -22,9 +28,9 @@ test('with isNative false uses correct method', async () => {
const selectedToken = { const selectedToken = {
address: '0xabcdef1234567890', address: '0xabcdef1234567890',
decimals: 18 decimals: 18
} } as any
const spy = vi.spyOn(wagmiActionsMock, 'writeContract') const spy = vi.spyOn(wagmiActionsMock, 'writeContract')
await send(selectedToken as any, {} as any) await send({} as any, selectedToken, '1', '0xabcdef1234567890', 1)
expect(spy).toHaveBeenCalledOnce() expect(spy).toHaveBeenCalledOnce()
}) })

View File

@ -1,21 +1,31 @@
import { import { sendTransaction, writeContract } from 'wagmi/actions'
sendTransaction as sendNative,
writeContract,
type SendTransactionArgs,
type WriteContractPreparedArgs
} from 'wagmi/actions'
import type { GetToken } from '../useFetchTokens' import type { GetToken } from '../useFetchTokens'
import { parseEther, parseUnits } from 'viem'
import { abiErc20Transfer } from './abiErc20Transfer'
import type { UseConfigReturnType } from 'wagmi'
export async function send( export async function send(
config: UseConfigReturnType,
selectedToken: GetToken | undefined, selectedToken: GetToken | undefined,
config: SendTransactionArgs | WriteContractPreparedArgs | undefined amount: string | undefined,
to: `0x${string}` | undefined,
chainId: number | undefined
) { ) {
if (!config || !selectedToken) return if (!selectedToken?.decimals || !amount || !to) return
const result = const isNative = selectedToken.address === '0x0'
selectedToken?.address === '0x0' const requestNative = { chainId, to, value: parseEther(amount) }
? await sendNative(config as SendTransactionArgs) const requestErc20 = {
: await writeContract(config as WriteContractPreparedArgs) chainId,
address: selectedToken.address,
abi: abiErc20Transfer,
functionName: 'transfer',
args: [to, parseUnits(amount, selectedToken.decimals)]
}
const result = isNative
? await sendTransaction(config, requestNative)
: await writeContract(config, requestErc20)
return result return result
} }

View File

@ -1,19 +1,15 @@
import { $txHash, $selectedToken } from '@features/Web3/stores' import { $txHash, $selectedToken, $amount } from '@features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { useState } from 'react' import { useState } from 'react'
import type {
SendTransactionArgs,
WriteContractPreparedArgs
} from 'wagmi/actions'
import { send } from './send' import { send } from './send'
import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage' import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
import { useAccount, useConfig } from 'wagmi'
export function useSend({ export function useSend() {
txConfig
}: {
txConfig: SendTransactionArgs | WriteContractPreparedArgs | undefined
}) {
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const amount = useStore($amount)
const config = useConfig()
const { address, chainId } = useAccount()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
@ -24,8 +20,8 @@ export function useSend({
setIsError(false) setIsError(false)
setError(undefined) setError(undefined)
setIsLoading(true) setIsLoading(true)
const result = await send(selectedToken, txConfig) const hash = await send(config, selectedToken, amount, address, chainId)
$txHash.set(result?.hash) if (hash) $txHash.set(hash)
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = (error as Error).message const errorMessage = (error as Error).message
console.error(errorMessage) console.error(errorMessage)

View File

@ -0,0 +1,7 @@
import { Buffer } from 'buffer'
window.global = window.global ?? window
window.Buffer = window.Buffer ?? Buffer
window.process = window.process ?? { env: {} } // Minimal process polyfill
export {}

View File

@ -1,32 +1,17 @@
import { type Theme, getDefaultWallets } from '@rainbow-me/rainbowkit' import { type Theme, getDefaultConfig } from '@rainbow-me/rainbowkit'
import { configureChains, createConfig } from 'wagmi'
import { mainnet, polygon, base, optimism } from 'wagmi/chains' import { mainnet, polygon, base, optimism } from 'wagmi/chains'
import { infuraProvider } from 'wagmi/providers/infura'
import { publicProvider } from 'wagmi/providers/public'
const PUBLIC_INFURA_ID = import.meta.env.PUBLIC_INFURA_ID
const PUBLIC_WALLETCONNECT_ID = import.meta.env.PUBLIC_WALLETCONNECT_ID const PUBLIC_WALLETCONNECT_ID = import.meta.env.PUBLIC_WALLETCONNECT_ID
const isProduction = import.meta.env.PROD const isProduction = import.meta.env.PROD
if (isProduction && (!PUBLIC_INFURA_ID || !PUBLIC_WALLETCONNECT_ID)) { if (isProduction && !PUBLIC_WALLETCONNECT_ID) {
throw new Error('Missing web3-related environment variables') throw new Error('Missing web3-related environment variables')
} }
export const { chains, publicClient } = configureChains( export const wagmiConfig = getDefaultConfig({
[mainnet, polygon, base, optimism],
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
)
export const { connectors } = getDefaultWallets({
appName: 'kremalicious.com', appName: 'kremalicious.com',
projectId: PUBLIC_WALLETCONNECT_ID, projectId: PUBLIC_WALLETCONNECT_ID,
chains chains: [mainnet, polygon, base, optimism]
})
export const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient
}) })
export const theme: Theme = { export const theme: Theme = {

View File

@ -109,7 +109,7 @@ import CodeCopy from '@components/CopyCode.astro'
<section class="section highlight" id="web3"> <section class="section highlight" id="web3">
<h4 class="titleCoin"><Wallet /> Web3 Wallet</h4> <h4 class="titleCoin"><Wallet /> Web3 Wallet</h4>
<Web3 client:load /> <Web3 client:only="react" />
</section> </section>
<section class="section"> <section class="section">

View File

@ -1,19 +1,3 @@
import { vi } from 'vitest'
export function configureChains() {
return { chains: [{}], provider: {} }
}
export const apiProvider = {
infura: vi.fn(),
alchemy: vi.fn(),
fallback: vi.fn()
}
export function getDefaultWallets() {
return { connectors: [{}] }
}
export function ConnectButton() { export function ConnectButton() {
return 'Connect Wallet' return 'Connect Wallet'
} }

View File

@ -1,11 +1,3 @@
export const prepareSendTransaction = async () => {
return {}
}
export const prepareWriteContract = async () => {
return {}
}
export const sendTransaction = async () => { export const sendTransaction = async () => {
return {} return {}
} }

View File

@ -31,29 +31,14 @@ const mainnet = {
serializers: undefined serializers: undefined
} }
export function useNetwork() { export function useChainId() {
return { return 1
chain: mainnet
}
} }
export function useAccount() { export function useAccount() {
return { return {
address: '0x0000000000000000000000000000000000000000' address: '0x0000000000000000000000000000000000000000',
} chain: mainnet
}
export function usePrepareSendTransaction() {
return {
data: {
address: '0x0000000000000000000000000000000000000000'
}
}
}
export function usePrepareContractWrite() {
return {
config: {}
} }
} }
@ -114,7 +99,9 @@ export function useProvider() {
return {} return {}
} }
export const chain = mainnet export function useConfig() {
return {}
}
export function createClient() { export function createClient() {
return { return {