Skip to content
Merged
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
107 changes: 104 additions & 3 deletions src/app/actions/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import { unstable_cache } from 'next/cache'
import { fetchWithSentry, isAddressZero, estimateIfIsStableCoinFromPrice } from '@/utils'
import { type ITokenPriceData } from '@/interfaces'
import { parseAbi } from 'viem'
import { parseAbi, formatUnits } from 'viem'
import { type ChainId, getPublicClient } from '@/app/actions/clients'
import { getTokenDetails, NATIVE_TOKEN_ADDRESS, isStableCoin } from '@/utils'
import { formatUnits } from 'viem'
import { getTokenDetails, isStableCoin, NATIVE_TOKEN_ADDRESS, areEvmAddressesEqual } from '@/utils'
import { IUserBalance } from '@/interfaces'
import type { Address, Hex } from 'viem'

type IMobulaMarketData = {
Expand Down Expand Up @@ -51,6 +51,49 @@ type IMobulaMarketData = {
}
}

type IMobulaContractBalanceData = {
address: string //of the contract
balance: number
balanceRaw: string
chainId: string // this chainId is og the type evm:<chainId>
decimals: number
}

type IMobulaCrossChainBalanceData = {
balance: number
balanceRaw: string
chainId: string
address: string //of the token
}

type IMobulaAsset = {
id: number
name: string
symbol: string
logo: string
decimals: string[]
contracts: string[]
blockchains: string[]
}

type IMobulaAssetData = {
contracts_balances: IMobulaContractBalanceData[]
cross_chain_balances: Record<string, IMobulaCrossChainBalanceData> // key is the same as in asset.blockchains price_change_24h: number
estimated_balance: number
price: number
token_balance: number
allocation: number
asset: IMobulaAsset
wallets: string[]
}

type IMobulaPortfolioData = {
total_wallet_balance: number
wallets: string[]
assets: IMobulaAssetData[]
balances_length: number
}

const ERC20_DATA_ABI = parseAbi([
'function symbol() view returns (string)',
'function name() view returns (string)',
Expand Down Expand Up @@ -214,3 +257,61 @@ export async function estimateTransactionCostUsd(
return 0.01
}
}

export const fetchWalletBalances = unstable_cache(
async (address: string): Promise<{ balances: IUserBalance[]; totalBalance: number }> => {
const mobulaResponse = await fetchWithSentry(`https://api.mobula.io/api/1/wallet/portfolio?wallet=${address}`, {
headers: {
'Content-Type': 'application/json',
authorization: process.env.MOBULA_API_KEY!,
},
})

if (!mobulaResponse.ok) throw new Error('Failed to fetch wallet balances')

const json: { data: IMobulaPortfolioData } = await mobulaResponse.json()
const assets = json.data.assets
.filter((a: IMobulaAssetData) => !!a.price)
.filter((a: IMobulaAssetData) => !!a.token_balance)
const balances = []
for (const asset of assets) {
const symbol = asset.asset.symbol
const price = isStableCoin(symbol) || estimateIfIsStableCoinFromPrice(asset.price) ? 1 : asset.price
/*
Mobula returns balances per asset, IE: USDC on arbitrum, mainnet
and optimism are all part of the same "asset", here we need to
divide it
*/
for (const chain of asset.asset.blockchains) {
const address = asset.cross_chain_balances[chain].address
const contractInfo = asset.contracts_balances.find((c) => areEvmAddressesEqual(c.address, address))
const crossChainBalance = asset.cross_chain_balances[chain]
balances.push({
chainId: crossChainBalance.chainId,
address,
name: asset.asset.name,
symbol,
decimals: contractInfo!.decimals,
price,
amount: crossChainBalance.balance,
currency: 'usd',
logoURI: asset.asset.logo,
value: (crossChainBalance.balance * price).toString(),
})
}
}
const totalBalance = balances.reduce(
(acc: number, balance: IUserBalance) => acc + balance.amount * balance.price,
0
)
return {
balances,
totalBalance,
}
},
['fetchWalletBalances'],
{
tags: ['fetchWalletBalances'],
revalidate: 5, // 5 seconds
}
)
37 changes: 0 additions & 37 deletions src/app/api/walletconnect/fetch-wallet-balance/route.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/components/Global/TokenSelector/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.co
import { tokenSelectorContext } from '@/context'
import { useDynamicHeight } from '@/hooks/ui/useDynamicHeight'
import { IToken, IUserBalance } from '@/interfaces'
import { areEvmAddressesEqual, fetchWalletBalances, formatTokenAmount, isNativeCurrency } from '@/utils'
import { areEvmAddressesEqual, formatTokenAmount, isNativeCurrency } from '@/utils'
import { SQUID_ETH_ADDRESS } from '@/utils/token.utils'
import { useAppKit, useAppKitAccount, useDisconnect } from '@reown/appkit/react'
import EmptyState from '../EmptyStates/EmptyState'
Expand All @@ -26,6 +26,7 @@ import {
TOKEN_SELECTOR_POPULAR_NETWORK_IDS,
TOKEN_SELECTOR_SUPPORTED_NETWORK_IDS,
} from './TokenSelector.consts'
import { fetchWalletBalances } from '@/app/actions/tokens'

interface SectionProps {
title: string
Expand Down
81 changes: 0 additions & 81 deletions src/utils/balance.utils.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,7 @@
import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants'
import { ChainValue, IUserBalance } from '@/interfaces'
import { areEvmAddressesEqual, fetchWithSentry, isAddressZero } from '@/utils'
import * as Sentry from '@sentry/nextjs'
import { formatUnits } from 'viem'
import { NATIVE_TOKEN_ADDRESS } from './token.utils'

export async function fetchWalletBalances(
address: string
): Promise<{ balances: IUserBalance[]; totalBalance: number }> {
try {
const apiResponse = await fetchWithSentry('/api/walletconnect/fetch-wallet-balance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ address }),
})

if (!apiResponse.ok) {
throw new Error('API request failed')
}

const apiResponseJson = await apiResponse.json()

const processedBalances = apiResponseJson.balances
.filter((balance: any) => balance.value > 0.009)
.map((item: any) => ({
chainId: item?.chainId ? item.chainId.split(':')[1] : '1',
address: item?.address ? item.address.split(':')[2] : NATIVE_TOKEN_ADDRESS,
name: item.name,
symbol: item.symbol,
decimals: parseInt(item.quantity.decimals),
price: item.price,
amount: parseFloat(item.quantity.numeric),
currency: 'usd',
logoURI: item.iconUrl,
value: item.value.toString(),
}))
.map((balance: any) =>
balance.chainId === '8508132'
? { ...balance, chainId: '534352' }
: balance.chainId === '81032'
? { ...balance, chainId: '81457' }
: balance.chainId === '59160'
? { ...balance, chainId: '59144' }
: balance
)
.sort((a: any, b: any) => {
const valueA = parseFloat(a.value)
const valueB = parseFloat(b.value)

if (valueA === valueB) {
if (isAddressZero(a.address)) return -1
if (isAddressZero(b.address)) return 1
return b.amount - a.amount
}
return valueB - valueA
})

const totalBalance = processedBalances.reduce((acc: number, balance: any) => acc + Number(balance.value), 0)

return {
balances: processedBalances,
totalBalance,
}
} catch (error) {
console.error('Error fetching wallet balances:', error)
if (error instanceof Error && error.message !== 'API request failed') {
Sentry.captureException(error)
}
return { balances: [], totalBalance: 0 }
}
}

export function balanceByToken(
balances: IUserBalance[],
chainId: string,
tokenAddress: string
): IUserBalance | undefined {
if (!chainId || !tokenAddress) return undefined
return balances.find(
(balance) => balance.chainId === chainId && areEvmAddressesEqual(balance.address, tokenAddress)
)
}

export function calculateValuePerChain(balances: IUserBalance[]): ChainValue[] {
let result: ChainValue[] = []
Expand Down
Loading