Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/zerodev-signer-demo/src/app/wagmi-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const config = createConfig({
// (testnets above), so the demo points it at real mainnet chains.
smartRoutingAddress: {
enabled: true,
destinationChains: [arbitrum],
destinationChains: [arbitrum, mainnet],
sourceChains: [mainnet, arbitrum],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'
import { mainnet, optimism } from 'viem/chains'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import { createStore } from '../store'
import { createStore } from '../../store'
import { useSmartRoutingAddress } from './useSmartRoutingAddress'

const mockCreateSmartRoutingAddress = vi.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,36 @@ export function useSmartRoutingFlow() {

const step = useStore(store, (state) => state.smartRouting.step)
const stepHistory = useStore(store, (state) => state.smartRouting.stepHistory)
const sendChain = useStore(store, (state) => state.smartRouting.sendChain)
const receiveChain = useStore(
store,
(state) => state.smartRouting.receiveChain,
)
const editingChainSlot = useStore(
store,
(state) => state.smartRouting.editingChainSlot,
)
const sourceChains = useStore(
store,
(state) => state.smartRouting.config?.sourceChains,
)
const destinationChains = useStore(
store,
(state) => state.smartRouting.config?.destinationChains,
)

return {
step,
sendChain,
receiveChain,
editingChainSlot,
sourceChains,
destinationChains,
goToStep: store.getState().smartRouting.goToStep,
goBack:
stepHistory.length > 0 ? store.getState().smartRouting.goBack : null,
stepHistory.length > 1 ? store.getState().smartRouting.goBack : null,
setEditingChainSlot: store.getState().smartRouting.setEditingChainSlot,
setChain: store.getState().smartRouting.setChain,
reset: store.getState().smartRouting.reset,
}
}
27 changes: 23 additions & 4 deletions packages/react-kit/src/smart-routing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@ import { ScreenWrapper } from '../shared/components/ScreenWrapper'
import { TopNav } from '../shared/components/TopNav'
import { QrModal } from './components/QrModal'
import { useSmartRoutingFlow } from './hooks/useSmartRoutingFlow'
import { SelectNetwork } from './pages/SelectNetwork'
import { TransferFromWallet } from './pages/TransferFromWallet'
import type { SmartRoutingStep } from './types'

const TITLE_BY_STEP: Partial<Record<SmartRoutingStep, string>> = {
'transfer-from-wallet': 'Transfer from wallet',
'select-network': 'Select a network',
}

function renderStep(
step: SmartRoutingStep | null,
onGotIt: () => void,
onShowQr: (address: string) => void,
onSelectNetwork: () => void,
): ReactNode {
switch (step) {
case 'transfer-from-wallet':
return <TransferFromWallet onGotIt={onGotIt} onShowQr={onShowQr} />
return (
<TransferFromWallet
onGotIt={onGotIt}
onShowQr={onShowQr}
onSelectNetwork={onSelectNetwork}
/>
)
case 'select-network':
return <SelectNetwork />
default:
return null
}
Expand All @@ -40,7 +51,7 @@ export function SmartRoutingAddress({
style?: CSSProperties | undefined
onClose?: () => void
} = {}) {
const { step, goToStep, reset } = useSmartRoutingFlow()
const { step, goToStep, goBack, reset } = useSmartRoutingFlow()
const [qrAddress, setQrAddress] = useState<string | null>(null)

// When the card mounts and no step is active, kick off the flow at its
Expand All @@ -62,7 +73,9 @@ export function SmartRoutingAddress({
setQrAddress(null)
}

const content = renderStep(step, handleClose, setQrAddress)
const content = renderStep(step, handleClose, setQrAddress, () =>
goToStep('select-network'),
)
if (!content) return null

const title = step ? TITLE_BY_STEP[step] : undefined
Expand All @@ -71,7 +84,13 @@ export function SmartRoutingAddress({
<ScreenWrapper
{...(className && { className })}
{...(style && { style })}
topNav={<TopNav onClose={handleClose} {...(title && { title })} />}
topNav={
<TopNav
{...(goBack !== null && { onBack: goBack })}
onClose={handleClose}
{...(title && { title })}
/>
}
overlay={
qrAddress ? (
<QrModal
Expand Down
95 changes: 95 additions & 0 deletions packages/react-kit/src/smart-routing/pages/SelectNetwork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useState } from 'react'
import type { Chain } from 'viem'
import { useChains } from 'wagmi'

import { Input } from '../../shared/components/Input'
import { TokenListItem } from '../../shared/components/TokenListItem'
import { useSmartRoutingFlow } from '../hooks/useSmartRoutingFlow'
import { getChainIcon } from '../utils/chainIcon'

function NetworkRow({
chain,
onSelect,
}: {
chain: Chain
onSelect: (chain: Chain) => void
}) {
const iconName = getChainIcon(chain.id)
return (
<TokenListItem
symbol={chain.name}
iconVariant="network"
{...(iconName && { iconName })}
onClick={() => onSelect(chain)}
/>
)
}

export function SelectNetwork() {
const wagmiChains = useChains()
const {
editingChainSlot,
sendChain,
receiveChain,
sourceChains,
destinationChains,
setChain,
setEditingChainSlot,
goBack,
} = useSmartRoutingFlow()
const [query, setQuery] = useState('')

// Pick the slot-specific list from the connector's SRA config (which lists
// SRA-supported mainnets) so the picker doesn't surface chains the API
// would reject. Falls back to wagmi's chains if the consumer didn't
// configure source/destination lists.
const chains =
editingChainSlot === 'send'
? (sourceChains ?? wagmiChains)
: editingChainSlot === 'receive'
? (destinationChains ?? wagmiChains)
: wagmiChains

const filtered = query
? chains.filter((c) =>
c.name.toLowerCase().includes(query.trim().toLowerCase()),
)
: chains

// Picking the chain that's currently in the *other* slot swaps the two,
// since Across can't quote a same-chain route.
const handleSelect = (chain: Chain) => {
if (!editingChainSlot) {
goBack?.()
return
}
const otherSlot = editingChainSlot === 'send' ? 'receive' : 'send'
const otherChain = editingChainSlot === 'send' ? receiveChain : sendChain
const currentChain = editingChainSlot === 'send' ? sendChain : receiveChain
if (otherChain && otherChain.id === chain.id && currentChain) {
setChain(otherSlot, currentChain)
}
setChain(editingChainSlot, chain)
setEditingChainSlot(null)
goBack?.()
}

return (
<div className="flex flex-col h-full">
<div className="flex-1 min-h-0 overflow-y-auto pt-4 pb-2 flex flex-col gap-4">
<Input
iconName="search"
placeholder="Network name"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>

<div className="flex flex-col gap-1">
{filtered.map((chain) => (
<NetworkRow key={chain.id} chain={chain} onSelect={handleSelect} />
))}
</div>
</div>
</div>
)
}
35 changes: 26 additions & 9 deletions packages/react-kit/src/smart-routing/pages/TransferFromWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,41 @@ import { DataRow } from '../../signing/components/DataRow'
import { DetailsContainer } from '../../signing/components/DetailsContainer'
import { AddressDisplay } from '../components/AddressDisplay'
import { useSmartRoutingAddress } from '../hooks/useSmartRoutingAddress'
import { useSmartRoutingFlow } from '../hooks/useSmartRoutingFlow'
import { getChainIcon } from '../utils/chainIcon'

const PLACEHOLDER_ADDRESS = '0x0000000000000000000000000000000000000000'

interface TransferFromWalletProps {
onGotIt: () => void
onShowQr: (address: string) => void
onSelectNetwork: () => void
}

export function TransferFromWallet({
onGotIt,
onShowQr,
onSelectNetwork,
}: TransferFromWalletProps) {
const { address: connectedAddress } = useAccount()
// Placeholder src/dest pair until the UI has real selectors. We use a
// specific token (USDC) — the SRA API's Across simulation cannot quote a
// route when the src/dest tokens resolve to a generic FLEX placeholder.
const { sendChain, receiveChain, setEditingChainSlot } = useSmartRoutingFlow()

const sendChainIcon = getChainIcon(sendChain?.id) ?? 'arbitrum'
const receiveChainIcon = getChainIcon(receiveChain?.id) ?? 'arbitrum'

const openChainPicker = (slot: 'send' | 'receive') => {
setEditingChainSlot(slot)
onSelectNetwork()
}
// The SRA refetches whenever the user picks a new chain in either slot.
// We use a specific token (USDC) — the SRA API's Across simulation cannot
// quote a route when the src/dest tokens resolve to a generic FLEX
// placeholder. Defaults fall back to mainnet/arbitrum (which the SRA API
// supports) until the user makes a selection.
const { data, error, isPending } = useSmartRoutingAddress({
owner: (connectedAddress ?? zeroAddress) as Address,
destChain: arbitrum,
srcTokens: [{ tokenType: 'USDC', chain: mainnet }],
destChain: receiveChain ?? arbitrum,
srcTokens: [{ tokenType: 'USDC', chain: sendChain ?? mainnet }],
})
const address = data?.smartRoutingAddress ?? PLACEHOLDER_ADDRESS
const canCopy = !!data?.smartRoutingAddress
Expand Down Expand Up @@ -75,9 +90,10 @@ export function TransferFromWallet({
className="flex-1 basis-0"
/>
<Select
label="Arbitrum"
iconName="arbitrum"
label={sendChain?.name ?? 'Arbitrum'}
iconName={sendChainIcon}
className="flex-1 basis-0"
onClick={() => openChainPicker('send')}
/>
</SectionCard>

Expand All @@ -90,11 +106,12 @@ export function TransferFromWallet({
className="flex-1 basis-0"
/>
<Select
label="Arbitrum"
iconName="arbitrum"
label={receiveChain?.name ?? 'Arbitrum'}
iconName={receiveChainIcon}
trailingIcon={false}
variant="ghost"
className="flex-1 basis-0"
onClick={() => openChainPicker('receive')}
/>
</SectionCard>

Expand Down
46 changes: 45 additions & 1 deletion packages/react-kit/src/smart-routing/smartRoutingStoreSlice.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import type { Chain } from 'viem'
import type { StateCreator } from 'zustand'
import type { SmartRoutingAddressConfig, SmartRoutingStep } from './types'

export type ChainSlot = 'send' | 'receive'

export interface SmartRoutingStoreSlice {
smartRouting: {
config: SmartRoutingAddressConfig | null
step: SmartRoutingStep | null
stepHistory: SmartRoutingStep[]
/** Chain selected for the Send (source) row. */
sendChain: Chain | null
/** Chain selected for the Receive (destination) row. */
receiveChain: Chain | null
/**
* Which chain row triggered the `select-network` step — read by
* `SelectNetwork` to know which slot's chain to update on tap.
*/
editingChainSlot: ChainSlot | null
initialize: (config: SmartRoutingAddressConfig) => void
goToStep: (step: SmartRoutingStep | null) => void
goBack: () => void
setEditingChainSlot: (slot: ChainSlot | null) => void
setChain: (slot: ChainSlot, chain: Chain) => void
reset: () => void
}
}
Expand All @@ -23,10 +37,22 @@ export const createSmartRoutingStoreSlice: StateCreator<
config: null,
step: null,
stepHistory: [],
sendChain: null,
receiveChain: null,
editingChainSlot: null,

initialize: (config) => {
set((state) => ({
smartRouting: { ...state.smartRouting, config },
smartRouting: {
...state.smartRouting,
config,
// Seed slot defaults to the first configured chain so the UI's
// displayed chain matches the SRA hook's effective inputs. The
// user can override either via `SelectNetwork`.
sendChain: config.sourceChains?.[0] ?? state.smartRouting.sendChain,
receiveChain:
config.destinationChains?.[0] ?? state.smartRouting.receiveChain,
},
}))
},

Expand Down Expand Up @@ -57,12 +83,30 @@ export const createSmartRoutingStoreSlice: StateCreator<
}))
},

setEditingChainSlot: (slot) => {
set((state) => ({
smartRouting: { ...state.smartRouting, editingChainSlot: slot },
}))
},

setChain: (slot, chain) => {
set((state) => ({
smartRouting: {
...state.smartRouting,
...(slot === 'send' ? { sendChain: chain } : { receiveChain: chain }),
},
}))
},

reset: () => {
set((state) => ({
smartRouting: {
...state.smartRouting,
step: null,
stepHistory: [],
sendChain: null,
receiveChain: null,
editingChainSlot: null,
},
}))
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-kit/src/smart-routing/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Address, Chain } from 'viem'
* the entry component dispatches each step to its page, and steps push/pop
* a history so the TopNav back button works.
*/
export type SmartRoutingStep = 'transfer-from-wallet'
export type SmartRoutingStep = 'transfer-from-wallet' | 'select-network'

/**
* Connector-level config for the Smart Routing Address (SRA) feature.
Expand Down
Loading