1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-12-22 09:13:35 +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/react": "^0.7.2",
"@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-expressive-code": "^0.33.4",
"astro-redirect-from": "^1.0.6",
@ -76,8 +77,8 @@
"sharp": "^0.33.2",
"slugify": "^1.6.6",
"swr": "^2.2.5",
"viem": "^1.19.13",
"wagmi": "^1.4.12"
"viem": "^2.8.5",
"wagmi": "^2.5.7"
},
"devDependencies": {
"@playwright/test": "^1.42.1",

View File

@ -1,42 +1,22 @@
import { formatEther, formatUnits } from 'viem'
import { useAccount, useEnsName, useNetwork } from 'wagmi'
import type {
SendTransactionArgs,
WriteContractPreparedArgs
} from 'wagmi/actions'
import { useAccount, useEnsName } from 'wagmi'
import styles from './Data.module.css'
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'
export function Data({
to,
ensResolved,
txConfig,
isDisabled
}: {
to: `0x${string}` | null | undefined
ensResolved: string | null | undefined
txConfig: SendTransactionArgs | WriteContractPreparedArgs | undefined
isDisabled: boolean
}) {
const { chain } = useNetwork()
const { address: from } = useAccount()
const { address: from, chain } = useAccount()
const { data: ensFrom } = useEnsName({ address: from, chainId: 1 })
const selectedToken = useStore($selectedToken)
// 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'
const amount = useStore($amount)
return (
<table className={styles.table} aria-disabled={isDisabled}>
@ -66,7 +46,7 @@ export function Data({
/>
</div>
<span className={styles.amount}>
{displayAmountFromConfig} {selectedToken?.symbol}
{amount} {selectedToken?.symbol}
</span>
</td>
</tr>

View File

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

View File

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

View File

@ -1,14 +1,20 @@
import '../lib/polyfills'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiConfig } from 'wagmi'
import { wagmiConfig, chains, theme } from '../lib/rainbowkit'
import { WagmiProvider } from 'wagmi'
import { wagmiConfig, theme } from '../lib/rainbowkit'
import { Web3Form } from './Form'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export function Web3() {
return (
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider chains={chains} theme={theme}>
<Web3Form />
</RainbowKitProvider>
</WagmiConfig>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider theme={theme}>
<Web3Form />
</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 () => {
vi.mock('wagmi', () => ({
useNetwork: () => ({ chain: undefined }),
useChainId: () => undefined,
useAccount: () => ({ address: undefined })
}))

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import useSWR, { type SWRResponse } from 'swr'
import { useNetwork, useAccount } from 'wagmi'
import { useChainId, useAccount } from 'wagmi'
import type { GetToken } from './types'
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.
//
export function useFetchTokens(): SWRResponse<GetToken[] | undefined, Error> {
const { chain } = useNetwork()
const chainId = useChainId()
const { address } = useAccount()
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,
// preventing initial fetch.
useEffect(() => {
if (!address || !chain?.id) {
if (!address || !chainId) {
setUrl(undefined)
return
}
const url = `${apiUrl}/balance?address=${address}&chainId=${chain?.id}`
const url = `${apiUrl}/balance?address=${address}&chainId=${chainId}`
setUrl(url)
}, [address, chain?.id])
}, [address, chainId])
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'
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()
})
@ -11,10 +17,10 @@ test('with isNative true uses correct method', async () => {
const selectedToken = {
address: '0x0',
decimals: 18
}
} as any
const spy = vi.spyOn(wagmiActionsMock, 'sendTransaction')
await send(selectedToken as any, {} as any)
await send({} as any, selectedToken, '1', '0xabcdef1234567890', 1)
expect(spy).toHaveBeenCalledOnce()
})
@ -22,9 +28,9 @@ test('with isNative false uses correct method', async () => {
const selectedToken = {
address: '0xabcdef1234567890',
decimals: 18
}
} as any
const spy = vi.spyOn(wagmiActionsMock, 'writeContract')
await send(selectedToken as any, {} as any)
await send({} as any, selectedToken, '1', '0xabcdef1234567890', 1)
expect(spy).toHaveBeenCalledOnce()
})

View File

@ -1,21 +1,31 @@
import {
sendTransaction as sendNative,
writeContract,
type SendTransactionArgs,
type WriteContractPreparedArgs
} from 'wagmi/actions'
import { sendTransaction, writeContract } from 'wagmi/actions'
import type { GetToken } from '../useFetchTokens'
import { parseEther, parseUnits } from 'viem'
import { abiErc20Transfer } from './abiErc20Transfer'
import type { UseConfigReturnType } from 'wagmi'
export async function send(
config: UseConfigReturnType,
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 =
selectedToken?.address === '0x0'
? await sendNative(config as SendTransactionArgs)
: await writeContract(config as WriteContractPreparedArgs)
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 result = isNative
? await sendTransaction(config, requestNative)
: await writeContract(config, requestErc20)
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 { useState } from 'react'
import type {
SendTransactionArgs,
WriteContractPreparedArgs
} from 'wagmi/actions'
import { send } from './send'
import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
import { useAccount, useConfig } from 'wagmi'
export function useSend({
txConfig
}: {
txConfig: SendTransactionArgs | WriteContractPreparedArgs | undefined
}) {
export function useSend() {
const selectedToken = useStore($selectedToken)
const amount = useStore($amount)
const config = useConfig()
const { address, chainId } = useAccount()
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
@ -24,8 +20,8 @@ export function useSend({
setIsError(false)
setError(undefined)
setIsLoading(true)
const result = await send(selectedToken, txConfig)
$txHash.set(result?.hash)
const hash = await send(config, selectedToken, amount, address, chainId)
if (hash) $txHash.set(hash)
} catch (error: unknown) {
const errorMessage = (error as Error).message
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 { configureChains, createConfig } from 'wagmi'
import { type Theme, getDefaultConfig } from '@rainbow-me/rainbowkit'
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 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')
}
export const { chains, publicClient } = configureChains(
[mainnet, polygon, base, optimism],
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
)
export const { connectors } = getDefaultWallets({
export const wagmiConfig = getDefaultConfig({
appName: 'kremalicious.com',
projectId: PUBLIC_WALLETCONNECT_ID,
chains
})
export const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient
chains: [mainnet, polygon, base, optimism]
})
export const theme: Theme = {

View File

@ -109,7 +109,7 @@ import CodeCopy from '@components/CopyCode.astro'
<section class="section highlight" id="web3">
<h4 class="titleCoin"><Wallet /> Web3 Wallet</h4>
<Web3 client:load />
<Web3 client:only="react" />
</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() {
return 'Connect Wallet'
}

View File

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

View File

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