Skip to content
Open
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
19 changes: 19 additions & 0 deletions .changeset/nervous-clouds-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@relayprotocol/relay-kit-ui': major
---

Migrate wallet balance fetching from Dune (sunset) to Codex

Breaking changes:

- `duneConfig` provider option removed — use `codexConfig` (`{ apiBaseUrl?, apiKey? }`). The default api base url is `https://graph.codex.io`; override it to proxy requests and protect your api key.
- `useDuneBalances` hook removed — use `useCodexBalances(address, queryOptions)`. The mainnet/testnet parameter is gone; balances are fetched from Codex-supported networks and filtered by your configured chains.
- `isDuneBalance` removed from `useCurrencyBalance`'s return value.
- `useMultiWalletBalances` no longer takes the mainnet/testnet parameter.

Other changes:

- New `useSolanaBalance` hook: selected SVM tokens (Solana and Eclipse) now fetch balances directly from the chain's RPC (native via `getBalance`, SPL via `getTokenAccountsByOwner`) instead of an indexer, so post-swap balances are fresh and the aggressive periodic refetch workaround is gone. The internal eclipse-only balance hook is gone; Eclipse flows through the same path.
- Native SVM balances (SOL, Eclipse ETH) in the consolidated token selector list are fetched from their RPCs and merged with Codex results, since Codex does not index native SVM balances (nor Eclipse at all). Their USD values come from Codex prices for wrapped SOL / mainnet WETH.
- Spam filtering: Codex's `removeScams` plus an `isScam` drop, and a liquidity heuristic replacing Dune's spam scoring — tokens whose pool liquidity is under $1k or below the balance's usd value have their usd value stripped so they can't crowd out real holdings in the token selector (balances still display).
- Known loss: Soon balances are gone from the token selector list; Soon is no longer a supported chain.
6 changes: 3 additions & 3 deletions demo/components/providers/RelayKitProviderWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ export const RelayKitProviderWrapper: FC<{
baseApiUrl: relayApi,
source: 'relay-demo',
logLevel: LogLevel.Verbose,
duneConfig: {
apiBaseUrl: process.env.NEXT_PUBLIC_DUNE_API_URL,
apiKey: process.env.NEXT_PUBLIC_DUNE_API_KEY
codexConfig: {
apiBaseUrl: process.env.NEXT_PUBLIC_CODEX_API_URL,
apiKey: process.env.NEXT_PUBLIC_CODEX_API_KEY
},
chains: dynamicChains,
privateChainIds: process.env.NEXT_PUBLIC_INCLUDE_CHAINS?.split(','),
Expand Down
63 changes: 63 additions & 0 deletions demo/pages/api/codex/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<void> {
const CODEX_API_KEY = process.env.CODEX_API_KEY
const allowedDomains = process.env.ALLOWED_API_DOMAINS
? process.env.ALLOWED_API_DOMAINS.split(',')
: []
let origin = req.headers.origin || req.headers.referer || ''

try {
origin = new URL(origin).origin
} catch (e) {}

if (allowedDomains.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Methods', 'POST,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
}

if (req.method === 'OPTIONS') {
res.status(200).end()
return
}

if (req.method !== 'POST') {
res.status(405).json({ message: 'Method not allowed' })
return
}

if (!CODEX_API_KEY) {
res.status(500).json({
error: 'Server configuration error'
})
return
}

if (allowedDomains.length > 0 && !allowedDomains.includes(origin)) {
res.status(403).json({ message: 'Forbidden: Origin not allowed' })
return
}

const codexResponse = await fetch('https://graph.codex.io/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: CODEX_API_KEY
},
body: JSON.stringify(req.body)
})

const response = await codexResponse.json()

if (!codexResponse.ok) {
res.status(codexResponse.status).json(response)
return
}

res.json(response)
}
89 changes: 0 additions & 89 deletions demo/pages/api/dune/[...path].ts

This file was deleted.

32 changes: 12 additions & 20 deletions packages/ui/src/components/common/TokenSelector/PaymentMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,17 @@ const PaymentMethod: FC<PaymentMethodProps> = ({

const {
balanceMap: tokenBalances,
data: duneTokens,
data: walletTokens,
isLoading: isLoadingBalances
} = useMultiWalletBalances(
linkedWallets,
address,
relayClient?.baseApiUrl?.includes('testnet') ? 'testnet' : 'mainnet'
)
} = useMultiWalletBalances(linkedWallets, address)

const filteredDuneTokenBalances = useMemo(() => {
return duneTokens?.balances
}, [duneTokens?.balances])
const filteredTokenBalances = useMemo(() => {
return walletTokens?.balances
}, [walletTokens?.balances])

const userTokensQuery = useMemo(() => {
if (filteredDuneTokenBalances && filteredDuneTokenBalances.length > 0) {
const sortedBalances = [...filteredDuneTokenBalances]
if (filteredTokenBalances && filteredTokenBalances.length > 0) {
const sortedBalances = [...filteredTokenBalances]
.sort((a, b) => {
const aValue = a.value_usd || 0
const bValue = b.value_usd || 0
Expand All @@ -216,7 +212,7 @@ const PaymentMethod: FC<PaymentMethodProps> = ({
)
}
return undefined
}, [filteredDuneTokenBalances])
}, [filteredTokenBalances])

const { data: userTokens, isLoading: isLoadingUserTokens } = useTokenList(
relayClient?.baseApiUrl,
Expand All @@ -229,7 +225,7 @@ const PaymentMethod: FC<PaymentMethodProps> = ({
}
: undefined,
{
enabled: !!filteredDuneTokenBalances
enabled: !!filteredTokenBalances
}
)

Expand Down Expand Up @@ -516,9 +512,7 @@ const PaymentMethod: FC<PaymentMethodProps> = ({
</Button>
</Flex>

<Flex
className="relay:flex-1 relay:gap-3 relay:overflow-hidden relay:min-w-0 relay:max-w-full"
>
<Flex className="relay:flex-1 relay:gap-3 relay:overflow-hidden relay:min-w-0 relay:max-w-full">
{/* Main Token Content */}
<AccessibleList
onSelect={(value) => {
Expand Down Expand Up @@ -648,7 +642,7 @@ const PaymentMethod: FC<PaymentMethodProps> = ({
) : (
<Flex direction="column" className="relay:gap-3">
{(() => {
const hasLoadedBalanceData = Boolean(duneTokens)
const hasLoadedBalanceData = Boolean(walletTokens)
const userTokensReady = !isLoadingUserTokens
const isWaitingForBalanceData =
address &&
Expand Down Expand Up @@ -761,9 +755,7 @@ const PaymentMethod: FC<PaymentMethodProps> = ({
<>
<div onClick={() => onOpenChange(true)}>{trigger}</div>
{open && (
<Box
className="relay:absolute relay:top-0 relay:left-0 relay:right-0 relay:bottom-0 relay:z-[100] relay:bg-[var(--relay-colors-widget-background)] relay:flex relay:flex-col relay:overflow-hidden"
>
<Box className="relay:absolute relay:top-0 relay:left-0 relay:right-0 relay:bottom-0 relay:z-[100] relay:bg-[var(--relay-colors-widget-background)] relay:flex relay:flex-col relay:overflow-hidden">
{paymentMethodContent}
</Box>
)}
Expand Down
21 changes: 10 additions & 11 deletions packages/ui/src/components/common/TokenSelector/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { Token } from '../../../types/index.js'
import { type ChainFilterValue } from './ChainFilter.js'
import useRelayClient from '../../../hooks/useRelayClient.js'
import { type Address } from 'viem'
import { useDebounceState, useDuneBalances } from '../../../hooks/index.js'
import { useDebounceState, useCodexBalances } from '../../../hooks/index.js'
import { useMediaQuery } from 'usehooks-ts'
import {
type Currency,
Expand Down Expand Up @@ -211,38 +211,37 @@ const TokenSelector: FC<TokenSelectorProps> = ({

// Get user's token balances
const {
data: duneTokens,
data: walletTokens,
balanceMap: tokenBalances,
isLoading: isLoadingBalances
} = useDuneBalances(
} = useCodexBalances(
address &&
address !== evmDeadAddress &&
address !== solDeadAddress &&
address !== bitcoinDeadAddress &&
isValidAddress
? address
: undefined,
relayClient?.baseApiUrl?.includes('testnet') ? 'testnet' : 'mainnet',
{
staleTime: 60000,
gcTime: 60000
}
)

// Filter dune token balances based on configured chains
const filteredDuneTokenBalances = useMemo(() => {
return duneTokens?.balances?.filter((balance) =>
// Filter wallet token balances based on configured chains
const filteredTokenBalances = useMemo(() => {
return walletTokens?.balances?.filter((balance) =>
configuredChainIds.includes(balance.chain_id)
)
}, [duneTokens?.balances, configuredChainIds])
}, [walletTokens?.balances, configuredChainIds])

const userTokensQuery = useMemo(() => {
if (depositAddressOnly) {
return undefined
}

if (filteredDuneTokenBalances && filteredDuneTokenBalances.length > 0) {
const sortedBalances = [...filteredDuneTokenBalances]
if (filteredTokenBalances && filteredTokenBalances.length > 0) {
const sortedBalances = [...filteredTokenBalances]
.sort((a, b) => (b.value_usd ?? 0) - (a.value_usd ?? 0))
.slice(0, 100)

Expand All @@ -251,7 +250,7 @@ const TokenSelector: FC<TokenSelectorProps> = ({
)
}
return undefined
}, [filteredDuneTokenBalances, depositAddressOnly])
}, [filteredTokenBalances, depositAddressOnly])

const solverUserTokens = useMemo<Currency[] | undefined>(() => {
if (!depositAddressOnly) {
Expand Down
Loading