feat: integrate li.fi widget to swap token on ETH network

* feat: add wagmi, viem and @lifi/widget dependencies

* feat: add ethereum contract call and signing API functions

* feat: add sendContractCall ethereum wallet function

* feat: add signer context for wallet password and transaction signing

* feat: add custom wagmi status connector

* feat: add wagmi provider setup

* feat: add exchange drawer UI component

* feat: integrate LiFi widget for token swaps

* feat: update root route with new providers

* feat: add exchange button to token component

* chore: update CSP and improve password modal accessibility

* feat(wallet): enhance exchange drawer UI

* chore(wallet): update lifi, wagmi & viem versions

* chore(changeset): revert useless quotes change

* chore(changeset): remove useless formatting change

* chore(wallet): remove useless async prefix

* chore(wallet/status-connector): turn code handling provider requests into a function for better readability

* chore: add changeset

* fix(wallet): properly type status-connector connect method
This commit is contained in:
Jules
2026-01-15 13:16:51 +09:00
committed by GitHub
parent 07e85e1d9e
commit 4614fcc1a5
16 changed files with 7418 additions and 407 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@status-im/wallet': patch
'wallet': patch
---
feat: integrate li.fi widget to swap tokens on ETH network
+3
View File
@@ -31,6 +31,7 @@
"@cardano-sdk/crypto": "^0.2.3",
"@cardano-sdk/key-management": "^0.27.5",
"@hookform/resolvers": "^3.1.1",
"@lifi/widget": "^3.40.1",
"@radix-ui/react-dialog": "^1.1.1",
"@status-im/colors": "workspace:*",
"@status-im/components": "workspace:*",
@@ -63,7 +64,9 @@
"trpc-chrome": "^1.0.0",
"ts-pattern": "^5.7.1",
"unified": "^11.0.5",
"viem": "^2.44.1",
"vite-plugin-node-polyfills": "^0.23.0",
"wagmi": "^3.3.2",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -0,0 +1,121 @@
'use client'
import { useCallback, useMemo, useState } from 'react'
import { LiFiWidget, type WidgetConfig } from '@lifi/widget'
import {
type Account,
ExchangeDrawer as ExchangeDrawerUI,
PasswordModal,
} from '@status-im/wallet/components'
import { useAccount } from 'wagmi'
const NATIVE_ETH_ADDRESS = '0x0000000000000000000000000000000000000000'
const ETHEREUM_MAINNET_CHAIN_ID = 1
export type ExchangeDrawerProps = {
children: React.ReactElement
account?: Account
fromChain?: number
fromToken?: string
isUnlocked?: boolean
onUnlock?: (password: string) => Promise<boolean>
}
export const ExchangeDrawer = (props: ExchangeDrawerProps) => {
const {
children,
account,
fromChain = ETHEREUM_MAINNET_CHAIN_ID,
fromToken,
isUnlocked = false,
onUnlock,
} = props
const [open, setOpen] = useState(false)
const [showPasswordModal, setShowPasswordModal] = useState(false)
const { isConnected, address: connectedAddress } = useAccount()
const handleOpenChange = useCallback(
(isOpen: boolean) => {
if (isOpen && !isUnlocked) {
setShowPasswordModal(true)
return
}
setOpen(isOpen)
},
[isUnlocked],
)
const handlePasswordConfirm = useCallback(
async (password: string) => {
if (!onUnlock) return
const success = await onUnlock(password)
if (success) {
setShowPasswordModal(false)
setOpen(true)
} else {
throw new Error('Invalid password')
}
},
[onUnlock],
)
const widgetConfig: WidgetConfig = useMemo(
() => ({
integrator: 'StatusWallet',
variant: 'compact',
fromChain,
fromToken: fromToken || NATIVE_ETH_ADDRESS,
fromAddress: isConnected ? connectedAddress : account?.address,
chains: {
allow: [1],
},
hiddenUI: ['appearance'],
theme: {
container: {
display: 'flex',
height: '100%',
borderRadius: '16px',
},
},
}),
[isConnected, connectedAddress, account?.address, fromChain, fromToken],
)
if (!account) {
return null
}
const widgetContent =
isUnlocked && open ? (
<LiFiWidget
key={connectedAddress}
integrator={widgetConfig.integrator}
config={widgetConfig}
/>
) : null
return (
<>
<ExchangeDrawerUI
open={open}
onOpenChange={handleOpenChange}
trigger={children}
>
{widgetContent}
</ExchangeDrawerUI>
{onUnlock && (
<PasswordModal
open={showPasswordModal}
onOpenChange={setShowPasswordModal}
onConfirm={handlePasswordConfirm}
buttonLabel="Unlock & Exchange"
/>
)}
</>
)
}
+140
View File
@@ -5,6 +5,10 @@ import { createTRPCProxyClient } from '@trpc/client'
import { initTRPC } from '@trpc/server'
import superjson from 'superjson'
import { createChromeHandler } from 'trpc-chrome/adapter'
import {
signMessage as viemSignMessage,
signTypedData as viemSignTypedData,
} from 'viem/accounts'
import { z } from 'zod'
import * as bitcoin from './bitcoin/bitcoin'
@@ -373,6 +377,142 @@ const apiRouter = router({
id,
}
}),
sendContractCall: t.procedure
.input(
z.object({
walletId: z.string(),
password: z.string(),
fromAddress: z.string(),
toAddress: z.string(),
gasLimit: z.string(),
maxFeePerGas: z.string(),
maxInclusionFeePerGas: z.string(),
data: z.string(),
value: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const { keyStore, walletCore } = ctx
const wallet = await keyStore.load(input.walletId)
const account = wallet.activeAccounts.find(
account => account.address === input.fromAddress,
)
if (!account) {
throw new Error('From address not found')
}
const privateKey = await keyStore.getKey(
wallet.id,
input.password,
account,
)
const id = await ethereum.sendContractCall({
walletCore,
walletPrivateKey: privateKey,
chainID: '01',
toAddress: input.toAddress,
fromAddress: input.fromAddress,
gasLimit: input.gasLimit,
maxFeePerGas: input.maxFeePerGas,
maxInclusionFeePerGas: input.maxInclusionFeePerGas,
data: input.data,
value: input.value,
})
return {
id,
}
}),
signMessage: t.procedure
.input(
z.object({
walletId: z.string(),
password: z.string(),
fromAddress: z.string(),
message: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const { keyStore, walletCore } = ctx
const wallet = await keyStore.load(input.walletId)
const account = wallet.activeAccounts.find(
acc => acc.address === input.fromAddress,
)
if (!account) {
throw new Error('From address not found')
}
const privateKey = await keyStore.getKey(
wallet.id,
input.password,
account,
)
const privateKeyHex = walletCore.HexCoding.encode(privateKey.data())
const message = input.message.startsWith('0x')
? { raw: input.message as `0x${string}` }
: input.message
const signature = await viemSignMessage({
message,
privateKey: privateKeyHex as `0x${string}`,
})
return { signature }
}),
signTypedData: t.procedure
.input(
z.object({
walletId: z.string(),
password: z.string(),
fromAddress: z.string(),
domain: z.record(z.unknown()),
types: z.record(
z.array(z.object({ name: z.string(), type: z.string() })),
),
primaryType: z.string(),
message: z.record(z.unknown()),
}),
)
.mutation(async ({ input, ctx }) => {
const { keyStore, walletCore } = ctx
const wallet = await keyStore.load(input.walletId)
const account = wallet.activeAccounts.find(
acc => acc.address === input.fromAddress,
)
if (!account) {
throw new Error('From address not found')
}
const privateKey = await keyStore.getKey(
wallet.id,
input.password,
account,
)
const privateKeyHex = walletCore.HexCoding.encode(privateKey.data())
const signature = await viemSignTypedData({
domain: input.domain,
types: input.types,
primaryType: input.primaryType,
message: input.message,
privateKey: privateKeyHex as `0x${string}`,
})
return { signature }
}),
}),
bitcoin: router({
+75
View File
@@ -92,6 +92,81 @@ export async function send({
}
}
export async function sendContractCall({
walletCore,
walletPrivateKey,
chainID,
toAddress,
fromAddress,
network = 'ethereum',
gasLimit,
maxFeePerGas,
maxInclusionFeePerGas,
data,
value = '0',
}: {
walletCore: WalletCore
walletPrivateKey: InstanceType<WalletCore['PrivateKey']>
chainID: string
toAddress: string
fromAddress: string
network?: string
gasLimit: string
maxFeePerGas: string
maxInclusionFeePerGas: string
data: string
value?: string
}) {
const nonceHex = await getNonceHex(fromAddress, network)
const chainIdHex = getChainIdHex(chainID)
const cleanData = data.replace(/^0x/, '')
const cleanValue = padHex(value)
const txInput = encoder.Ethereum.Proto.SigningInput.create({
chainId: Uint8Array.from(Buffer.from(chainIdHex, 'hex')),
nonce: Uint8Array.from(Buffer.from(nonceHex, 'hex')),
gasLimit: Uint8Array.from(Buffer.from(padHex(gasLimit), 'hex')),
maxFeePerGas: Uint8Array.from(Buffer.from(padHex(maxFeePerGas), 'hex')),
maxInclusionFeePerGas: Uint8Array.from(
Buffer.from(padHex(maxInclusionFeePerGas), 'hex'),
),
toAddress,
transaction: {
contractGeneric: {
amount: Uint8Array.from(Buffer.from(cleanValue, 'hex')),
data: Uint8Array.from(Buffer.from(cleanData, 'hex')),
},
},
privateKey: walletPrivateKey.data(),
txMode: encoder.Ethereum.Proto.TransactionMode.Enveloped,
})
const inputEncoded =
encoder.Ethereum.Proto.SigningInput.encode(txInput).finish()
const outputData = walletCore.AnySigner.sign(
inputEncoded,
walletCore.CoinType.ethereum,
)
const output = encoder.Ethereum.Proto.SigningOutput.decode(outputData)
const rawTx = walletCore.HexCoding.encode(output.encoded)
const response = await fetch(BROADCAST_TRANSACTION_URL.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json: { txHex: rawTx, network } }),
cache: 'no-store',
})
if (!response.ok) {
throw new Error('Failed to broadcast transaction')
}
const body = await response.json()
const txid = body.result.data.json
return { txid }
}
export async function sendErc20({
walletCore,
walletPrivateKey,
+247
View File
@@ -0,0 +1,247 @@
import {
type Address,
createPublicClient,
type Hex,
http,
numberToHex,
} from 'viem'
import { createConnector } from 'wagmi'
import { mainnet } from 'wagmi/chains'
export type StatusConnectorOptions = {
getAddress: () => Address | undefined
signAndSendTransaction: (tx: {
to: Address
value: bigint
data?: Hex
gas?: bigint
maxFeePerGas?: bigint
maxPriorityFeePerGas?: bigint
}) => Promise<Hex>
signMessage: (message: Hex) => Promise<Hex>
signTypedData: (typedData: string) => Promise<Hex>
}
function handleProviderRequest(
method: string,
params: unknown[] | undefined,
options: {
getAddress: () => Address | undefined
signAndSendTransaction: StatusConnectorOptions['signAndSendTransaction']
signMessage: StatusConnectorOptions['signMessage']
signTypedData: StatusConnectorOptions['signTypedData']
emitter: { emit: (event: string, ...args: unknown[]) => void }
},
): Promise<unknown> {
const {
getAddress,
signAndSendTransaction,
signMessage,
signTypedData,
emitter,
} = options
const address = getAddress()
switch (method) {
case 'eth_requestAccounts':
case 'eth_accounts': {
const accounts = address ? [address] : []
if (method === 'eth_requestAccounts' && accounts.length > 0) {
emitter.emit('connect', {
accounts,
chainId: mainnet.id,
})
}
return Promise.resolve(accounts)
}
case 'eth_chainId':
return Promise.resolve(numberToHex(mainnet.id))
case 'eth_sendTransaction': {
const txParams = (params as Record<string, unknown>[])?.[0]
if (!txParams) throw new Error('No transaction params')
return signAndSendTransaction({
to: txParams.to as Address,
value: txParams.value ? BigInt(txParams.value as string) : 0n,
data: txParams.data as Hex | undefined,
gas: txParams.gas ? BigInt(txParams.gas as string) : undefined,
maxFeePerGas: txParams.maxFeePerGas
? BigInt(txParams.maxFeePerGas as string)
: undefined,
maxPriorityFeePerGas: txParams.maxPriorityFeePerGas
? BigInt(txParams.maxPriorityFeePerGas as string)
: undefined,
})
}
case 'wallet_switchEthereumChain':
return Promise.resolve(null)
case 'wallet_getCapabilities': {
const [requestedAddress, chainIds] = (params || []) as [
Address | undefined,
string[] | undefined,
]
const address = requestedAddress || getAddress()
if (!address) {
return Promise.resolve({})
}
const requestedChains = chainIds || [numberToHex(mainnet.id)]
const capabilities: Record<string, Record<string, unknown>> = {}
for (const chainId of requestedChains) {
capabilities[chainId] = {
atomicBatch: {
supported: false,
},
}
}
return Promise.resolve(capabilities)
}
case 'personal_sign': {
const [message] = params as [Hex]
return signMessage(message)
}
case 'eth_signTypedData_v4': {
const [, typedData] = params as [Address, string]
return signTypedData(typedData)
}
default: {
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
return client.request({
method: method as never,
params: params as never,
})
}
}
}
export function statusConnector(options: StatusConnectorOptions) {
const { getAddress, signAndSendTransaction, signMessage, signTypedData } =
options
return createConnector(config => {
const createProvider = () => {
const provider = {
isStatus: true,
isMetaMask: true,
get connected() {
return !!getAddress()
},
chainId: numberToHex(mainnet.id),
get selectedAddress() {
return getAddress()
},
request: async ({
method,
params,
}: {
method: string
params?: unknown[]
}) => {
return handleProviderRequest(method, params, {
getAddress,
signAndSendTransaction,
signMessage,
signTypedData,
emitter: config.emitter as unknown as {
emit: (event: string, ...args: unknown[]) => void
},
})
},
on(event: string, handler: (...args: unknown[]) => void) {
config.emitter.on(event as never, handler)
},
removeListener(event: string, handler: (...args: unknown[]) => void) {
config.emitter.off(event as never, handler)
},
}
return provider
}
const provider = createProvider()
if (typeof window !== 'undefined') {
;(window as { ethereum?: unknown }).ethereum = provider
}
return {
id: 'status-wallet',
name: 'Status Wallet',
type: 'injected' as const,
async connect<withCapabilities extends boolean = false>() {
const address = getAddress()
if (!address) {
throw new Error('No wallet connected')
}
return {
accounts: [address] as readonly Address[],
chainId: mainnet.id,
} as {
accounts: withCapabilities extends true
? readonly {
address: Address
capabilities: Record<string, unknown>
}[]
: readonly Address[]
chainId: number
}
},
// required by wagmi's connector interface
// empty because we delegate connection state to wallet-context through getAddress()
async disconnect() {},
async getAccounts() {
const address = getAddress()
return address ? [address] : []
},
async getChainId() {
return mainnet.id
},
async isAuthorized() {
return !!getAddress()
},
async switchChain({ chainId }) {
const chain = config.chains.find(c => c.id === chainId)
if (!chain) throw new Error('Chain not found')
config.emitter.emit('change', { chainId })
return chain
},
onAccountsChanged(accounts) {
if (accounts.length === 0) {
config.emitter.emit('disconnect')
} else {
config.emitter.emit('change', { accounts: accounts as Address[] })
}
},
onChainChanged(chainId) {
config.emitter.emit('change', {
chainId: Number(chainId),
})
},
onDisconnect() {
config.emitter.emit('disconnect')
},
async getProvider() {
return provider
},
}
})
}
@@ -0,0 +1,316 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
import { type Address, createPublicClient, type Hex, http } from 'viem'
import { mainnet } from 'viem/chains'
import { apiClient } from './api-client'
import { useWallet } from './wallet-context'
type SignerContextValue = {
address: Address | undefined
isUnlocked: boolean
unlock: (password: string) => Promise<boolean>
lock: () => void
signAndSendTransaction: (tx: {
to: Address
value: bigint
data?: Hex
gas?: bigint
maxFeePerGas?: bigint
maxPriorityFeePerGas?: bigint
}) => Promise<Hex>
signMessage: (message: Hex) => Promise<Hex>
signTypedData: (typedData: string) => Promise<Hex>
requestUnlock: () => Promise<string | null>
setUnlockHandler: (handler: () => Promise<string | null>) => void
}
const SignerContext = createContext<SignerContextValue | undefined>(undefined)
export function useWalletSigner() {
const context = useContext(SignerContext)
if (!context) {
throw new Error('useWalletSigner must be used within SignerProvider')
}
return context
}
export function SignerProvider({ children }: { children: React.ReactNode }) {
const { currentWallet } = useWallet()
const [sessionPassword, setSessionPassword] = useState<string | null>(null)
const [unlockHandler, setUnlockHandler] = useState<
(() => Promise<string | null>) | null
>(null)
const address = useMemo(() => {
return currentWallet?.activeAccounts[0]?.address as Address | undefined
}, [currentWallet])
const unlock = useCallback(
async (password: string): Promise<boolean> => {
if (!currentWallet?.id) return false
try {
await apiClient.wallet.get.query({
walletId: currentWallet.id,
password,
})
setSessionPassword(password)
return true
} catch {
return false
}
},
[currentWallet?.id],
)
const lock = useCallback(() => {
setSessionPassword(null)
}, [])
const requestUnlock = useCallback(async (): Promise<string | null> => {
if (sessionPassword) return sessionPassword
if (unlockHandler) {
const password = await unlockHandler()
if (password) {
setSessionPassword(password)
}
return password
}
return null
}, [sessionPassword, unlockHandler])
const ensurePassword = useCallback(async (): Promise<string> => {
let password = sessionPassword
if (!password) {
password = await requestUnlock()
if (!password) {
throw new Error('Wallet not unlocked')
}
}
return password
}, [sessionPassword, requestUnlock])
const signAndSendTransaction = useCallback(
async (tx: {
to: Address
value: bigint
data?: Hex
gas?: bigint
maxFeePerGas?: bigint
maxPriorityFeePerGas?: bigint
}): Promise<Hex> => {
if (!currentWallet?.id || !address) {
throw new Error('No wallet connected')
}
const password = await ensurePassword()
let maxFeePerGas = tx.maxFeePerGas?.toString(16)
let maxPriorityFeePerGas = tx.maxPriorityFeePerGas?.toString(16)
let gasLimit = tx.gas?.toString(16)
if (!maxFeePerGas || !maxPriorityFeePerGas || !gasLimit) {
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
})
if (!maxFeePerGas || !maxPriorityFeePerGas) {
const [block, priorityFee] = await Promise.all([
publicClient.getBlock({ blockTag: 'latest' }),
publicClient.request({
method: 'eth_maxPriorityFeePerGas',
}),
])
const baseFee = block.baseFeePerGas || 0n
const priorityFeeBigInt = BigInt(priorityFee as string)
maxPriorityFeePerGas = priorityFeeBigInt.toString(16)
// maxFeePerGas calculation - https://www.blocknative.com/blog/eip-1559-fees
maxFeePerGas = (baseFee * 2n + priorityFeeBigInt).toString(16)
}
if (!gasLimit) {
const estimatedGas = await publicClient.estimateGas({
account: address,
to: tx.to,
value: tx.value,
data: tx.data,
})
gasLimit = (estimatedGas + estimatedGas / 10n).toString(16)
}
}
const extractTxHash = (id: unknown): string | undefined => {
if (typeof id === 'string') {
return id
}
if (id && typeof id === 'object') {
const obj = id as Record<string, unknown>
if ('result' in obj && typeof obj.result === 'string') {
return obj.result
}
if ('txid' in obj) {
return obj.txid as string
}
}
return undefined
}
if (tx.data) {
const ERC20_TRANSFER_SIGNATURE = '0xa9059cbb'
const isErc20Transfer = tx.data
.toLowerCase()
.startsWith(ERC20_TRANSFER_SIGNATURE)
if (isErc20Transfer) {
const result =
await apiClient.wallet.account.ethereum.sendErc20.mutate({
walletId: currentWallet.id,
password,
fromAddress: address,
toAddress: tx.to,
gasLimit,
maxFeePerGas,
maxInclusionFeePerGas: maxPriorityFeePerGas,
data: tx.data,
})
const txHash = extractTxHash(result.id.txid)
if (!txHash) throw new Error('Transaction failed')
return txHash as Hex
}
const valueHex = tx.value.toString(16)
const result =
await apiClient.wallet.account.ethereum.sendContractCall.mutate({
walletId: currentWallet.id,
password,
fromAddress: address,
toAddress: tx.to,
gasLimit,
maxFeePerGas,
maxInclusionFeePerGas: maxPriorityFeePerGas,
data: tx.data,
value: valueHex,
})
if (result.id.txid?.error) {
console.error('Contract call error:', result.id.txid.error)
throw new Error(result.id.txid.error)
}
const txHash = extractTxHash(result.id.txid)
if (!txHash) throw new Error('Transaction failed')
return txHash as Hex
}
const amountHex = tx.value.toString(16)
const result = await apiClient.wallet.account.ethereum.send.mutate({
walletId: currentWallet.id,
password,
fromAddress: address,
toAddress: tx.to,
amount: amountHex,
gasLimit,
maxFeePerGas,
maxInclusionFeePerGas: maxPriorityFeePerGas,
})
const txHash = extractTxHash(result.id.txid)
if (!txHash) throw new Error('Transaction failed')
return txHash as Hex
},
[currentWallet?.id, address, ensurePassword],
)
const signMessage = useCallback(
async (message: Hex): Promise<Hex> => {
if (!currentWallet?.id || !address) {
throw new Error('No wallet connected')
}
const password = await ensurePassword()
const result = await apiClient.wallet.account.ethereum.signMessage.mutate(
{
walletId: currentWallet.id,
password,
fromAddress: address,
message,
},
)
return result.signature as Hex
},
[currentWallet?.id, address, ensurePassword],
)
const signTypedData = useCallback(
async (typedData: string): Promise<Hex> => {
if (!currentWallet?.id || !address) {
throw new Error('No wallet connected')
}
const password = await ensurePassword()
const parsed = JSON.parse(typedData)
const result =
await apiClient.wallet.account.ethereum.signTypedData.mutate({
walletId: currentWallet.id,
password,
fromAddress: address,
domain: parsed.domain,
types: parsed.types,
primaryType: parsed.primaryType,
message: parsed.message,
})
return result.signature as Hex
},
[currentWallet?.id, address, ensurePassword],
)
const handleSetUnlockHandler = useCallback(
(handler: () => Promise<string | null>) => {
setUnlockHandler(() => handler)
},
[],
)
const value: SignerContextValue = useMemo(
() => ({
address,
isUnlocked: !!sessionPassword,
unlock,
lock,
signAndSendTransaction,
signMessage,
signTypedData,
requestUnlock,
setUnlockHandler: handleSetUnlockHandler,
}),
[
address,
sessionPassword,
unlock,
lock,
signAndSendTransaction,
signMessage,
signTypedData,
requestUnlock,
handleSetUnlockHandler,
],
)
return (
<SignerContext.Provider value={value}>{children}</SignerContext.Provider>
)
}
@@ -0,0 +1,62 @@
'use client'
import { useMemo, useRef } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createConfig, http, WagmiProvider } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { statusConnector } from '../lib/status-connector'
import { useWalletSigner } from './signer-context'
const queryClient = new QueryClient()
type Props = {
children: React.ReactNode
}
function WagmiConfigProvider({ children }: Props) {
const { address, signAndSendTransaction, signMessage, signTypedData } =
useWalletSigner()
const addressRef = useRef(address)
const signAndSendTransactionRef = useRef(signAndSendTransaction)
const signMessageRef = useRef(signMessage)
const signTypedDataRef = useRef(signTypedData)
addressRef.current = address
signAndSendTransactionRef.current = signAndSendTransaction
signMessageRef.current = signMessage
signTypedDataRef.current = signTypedData
const config = useMemo(() => {
return createConfig({
chains: [mainnet],
connectors: [
statusConnector({
getAddress: () => addressRef.current,
signAndSendTransaction: async tx => {
return signAndSendTransactionRef.current(tx)
},
signMessage: async message => {
return signMessageRef.current(message)
},
signTypedData: async typedData => {
return signTypedDataRef.current(typedData)
},
}),
],
transports: {
[mainnet.id]: http(),
},
ssr: false,
})
}, [])
return (
<WagmiProvider config={config} reconnectOnMount={true}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
)
}
export { WagmiConfigProvider }
+8 -46
View File
@@ -1,27 +1,17 @@
// import { Suspense } from 'react'
// import OnboardingPage from '../../../portfolio/src/app/page'
import { ToastContainer } from '@status-im/components'
import { Navbar } from '@status-im/wallet/components'
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import {
createRootRouteWithContext,
HeadContent,
// Navigate,
Outlet,
redirect,
} from '@tanstack/react-router'
// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
// import { NotAllowed } from '../../../portfolio/src/app/_components/not-allowed'
// import { AccountsProvider } from '../../../portfolio/src/app/_providers/accounts-context'
// import { ConnectKitProvider } from '../../../portfolio/src/app/_providers/connectkit-provider'
import { QueryClientProvider } from '../../../portfolio/src/app/_providers/query-client-provider'
// import { StatusProvider } from '../../../portfolio/src/app/_providers/status-provider'
import { WagmiProvider } from '../../../portfolio/src/app/_providers/wagmi-provider'
import { Link } from '../components/link'
import { apiClient } from '../providers/api-client'
import { PendingTransactionsProvider } from '../providers/pending-transactions-context'
import { SignerProvider } from '../providers/signer-context'
import { WagmiConfigProvider } from '../providers/wagmi-provider'
import { WalletProvider } from '../providers/wallet-context'
// import { Inter } from 'next/font/google'
@@ -131,29 +121,12 @@ export const Route = createRootRouteWithContext<{
function RootComponent() {
return (
<>
{/* <div className="min-h-screen bg-neutral-100 text-white-100">
<div className="flex gap-4 p-4">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
<Link to="/onboarding" className="[&.active]:font-bold">
Onboarding
</Link>
</div>
<hr />
<Outlet />
</div> */}
<HeadContent />
<div id="app" className="isolate" data-customisation="blue">
{/* <StatusProvider> */}
<WagmiProvider>
<QueryClientProvider>
{/* <Suspense fallback={<div>Loading...</div>}> */}
{/* <AccountsProvider> */}
{/* <ConnectKitProvider> */}
<WalletProvider>
<WalletProvider>
<SignerProvider>
<WagmiConfigProvider>
<PendingTransactionsProvider>
<div className="flex min-h-[56px] items-center px-2">
<Navbar hasFeedback linkComponent={Link} />
@@ -169,21 +142,10 @@ function RootComponent() {
<ToastContainer />
</div>
</PendingTransactionsProvider>
</WalletProvider>
{/* </ConnectKitProvider> */}
{/* </AccountsProvider> */}
{/* </Suspense> */}
</QueryClientProvider>
</WagmiProvider>
{/* </StatusProvider> */}
</WagmiConfigProvider>
</SignerProvider>
</WalletProvider>
</div>
{/* <ReactQueryDevtools buttonPosition="bottom-right" />
<TanStackRouterDevtools position="bottom-left" />
<div className="fixed inset-x-0 bottom-0 flex justify-center gap-4 bg-blur-neutral-100/70 p-4 text-white-100">
<Link to="/">/index</Link>
<Link to="/onboarding">/onboarding</Link>
<Link to="/portfolio/assets">/portfolio</Link>
</div> */}
</>
)
}
@@ -11,6 +11,7 @@ import {
BuyIcon,
ReceiveBlurIcon,
SendBlurIcon,
SwapIcon,
} from '@status-im/icons/20'
import {
type Account,
@@ -41,7 +42,9 @@ import { useEthBalance } from '@/hooks/use-eth-balance'
import { renderMarkdown } from '@/lib/markdown'
import { apiClient } from '@/providers/api-client'
import { usePendingTransactions } from '@/providers/pending-transactions-context'
import { useWalletSigner } from '@/providers/signer-context'
import { useWallet } from '@/providers/wallet-context'
import { ExchangeDrawer } from '~/components/exchange-drawer'
import { AssetChart } from './asset-chart'
import {
@@ -80,6 +83,7 @@ const Token = (props: Props) => {
const toast = useToast()
const { currentWallet } = useWallet()
const { addPendingTransaction } = usePendingTransactions()
const { isUnlocked, unlock } = useWalletSigner()
const [activeDataType, setActiveDataType] =
useState<ChartDataType>(DEFAULT_DATA_TYPE)
@@ -305,6 +309,12 @@ const Token = (props: Props) => {
decimals: asset.decimals ?? 18,
}
const isNativeETH = finalTokenDetail.summary.symbol === 'ETH'
const fromTokenAddress = isNativeETH
? '0x0000000000000000000000000000000000000000'
: finalTokenDetail.summary.contracts?.ethereum ||
(ticker.startsWith('0x') ? ticker : undefined)
// Mock wallet data. Replace with actual wallet data from the user's account.
const account: Account = {
address,
@@ -469,6 +479,19 @@ const Token = (props: Props) => {
<span className="block max-w-20 truncate">Buy</span>
</Button>
</BuyCryptoDrawer>
{fromTokenAddress && (
<ExchangeDrawer
account={account}
fromChain={1}
fromToken={fromTokenAddress}
isUnlocked={isUnlocked}
onUnlock={unlock}
>
<Button size="32" iconBefore={<SwapIcon />} variant="outline">
<span className="block max-w-20 truncate">Exchange</span>
</Button>
</ExchangeDrawer>
)}
<ReceiveCryptoDrawer account={account} onCopy={copy}>
<Button
size="32"
@@ -523,6 +546,20 @@ const Token = (props: Props) => {
</Button>
</BuyCryptoDrawer>
{fromTokenAddress && (
<ExchangeDrawer
account={account}
fromChain={1}
fromToken={fromTokenAddress}
isUnlocked={isUnlocked}
onUnlock={unlock}
>
<Button size="32" iconBefore={<SwapIcon />} variant="outline">
Exchange
</Button>
</ExchangeDrawer>
)}
<ReceiveCryptoDrawer account={account} onCopy={copy}>
<Button
size="32"
+4 -2
View File
@@ -18,10 +18,12 @@ export default defineConfig({
mode === 'production'
? "'self' 'wasm-unsafe-eval'"
: "'self' 'wasm-unsafe-eval' http://localhost:4000/ http://localhost:8097/"
const rpcEndpoints =
'https://eth.merkle.io/ https://ethereum-rpc.publicnode.com/ https://rpc.ankr.com/ https://nodes.mewapi.io/ https://mainnet.infura.io/ https://cloudflare-eth.com/ https://rpc.flashbots.net/ https://rpc.tenderly.co/ https://rpc.ethernode.com/ https://ethereum.publicnode.com/ https://eth.drpc.org/'
const connectSrc =
mode === 'production'
? 'https://status-api-status-im-web.vercel.app/ https://status-api-status-im-web.vercel.app/api/'
: 'ws: http://localhost:3030/ https://localhost:3030/'
? `https://status-api-status-im-web.vercel.app/ https://status-api-status-im-web.vercel.app/api/ https://li.quest/ https://registry.npmjs.org/ ${rpcEndpoints}`
: `ws: http://localhost:3030/ https://localhost:3030/ https://li.quest/ https://registry.npmjs.org/ ${rpcEndpoints}`
return {
version: '0.1.0',
@@ -13,7 +13,7 @@ const Close = forwardRef<
React.ComponentRef<typeof Dialog.Close>,
React.ComponentPropsWithoutRef<typeof Dialog.Close> & { children?: never }
>((props, ref) => (
<div className="absolute right-3 top-3">
<div className="absolute right-3 top-3 z-50">
<Dialog.Close {...props} ref={ref} asChild>
<Button
icon={<CloseIcon />}
@@ -0,0 +1,27 @@
'use client'
import * as Drawer from '../drawer'
export type ExchangeDrawerProps = {
open: boolean
onOpenChange: (open: boolean) => void
trigger: React.ReactElement
children?: React.ReactNode
}
export const ExchangeDrawer = (props: ExchangeDrawerProps) => {
const { open, onOpenChange, trigger, children } = props
return (
<Drawer.Root modal open={open} onOpenChange={onOpenChange}>
<Drawer.Trigger asChild>{trigger}</Drawer.Trigger>
<Drawer.Content className="p-0">
<Drawer.Title className="sr-only">Exchange</Drawer.Title>
<Drawer.Body className="flex h-full flex-col overflow-hidden">
<div className="h-10 flex-shrink-0" />
<div className="min-h-0 flex-1">{children}</div>
</Drawer.Body>
</Drawer.Content>
</Drawer.Root>
)
}
+2
View File
@@ -33,9 +33,11 @@ export {
} from './create-password-form'
export { CurrencyAmount } from './currency-amount'
export { DeleteAddressAlert } from './delete-address-alert'
export * as Drawer from './drawer'
export { DropdownSort } from './dropdown-sort'
export { EmptyState } from './empty-state'
export { EmptyStateActions } from './empty-state-actions'
export { ExchangeDrawer, type ExchangeDrawerProps } from './exchange-drawer'
export { FeedbackPopover, FeedbackSection } from './feedback'
export { Image, type ImageProps } from './image'
export {
@@ -106,11 +106,13 @@ const PasswordModal = (props: PasswordModalProps) => {
</Dialog.Close>
</div>
<p className="mb-5 text-13 text-neutral-50">To sign transaction</p>
<Dialog.Description className="mb-5 text-13 text-neutral-50">
To sign transaction
</Dialog.Description>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-1 flex-col place-content-between content-between justify-between"
className="flex flex-1 flex-col place-content-between"
>
<div className="mb-4 w-full">
<Controller
@@ -122,6 +124,7 @@ const PasswordModal = (props: PasswordModalProps) => {
{...field}
type={showPassword ? 'text' : 'password'}
placeholder="Type password"
aria-label="Password"
isInvalid={!!errors.password}
isDisabled={isLoading}
onKeyDown={e => {
@@ -133,6 +136,9 @@ const PasswordModal = (props: PasswordModalProps) => {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
aria-label={
showPassword ? 'Hide password' : 'Show password'
}
className="absolute right-4 top-1/2 -translate-y-1/2 text-neutral-50 hover:text-neutral-70"
>
{showPassword ? <HideIcon /> : <RevealIcon />}
+6361 -356
View File
File diff suppressed because it is too large Load Diff