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
7 changes: 3 additions & 4 deletions packages/shared/src/hooks/useUserTotalBalance.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useQuery } from '@tanstack/react-query'
import { DeBankFungibleToken } from '../../../web3-providers/src/DeBank/index.js'
import { getWalletPortfolioTotalValue } from '@masknet/web3-providers'

export function useUserTotalBalance(address: string | undefined) {
return useQuery({
queryKey: ['user-total-balance', address],
queryKey: ['wallet', 'portfolio-total', address],
queryFn: async () => {
if (!address) return 0
const res = await DeBankFungibleToken.getTotalBalance(address)
return res.total_usd_value
return getWalletPortfolioTotalValue(address)
},
})
}
7 changes: 1 addition & 6 deletions packages/web3-providers/src/DeBank/apis/FungibleTokenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createPageable, createIndicator } from '@masknet/shared-base'
import type { ChainId, SchemaType } from '@masknet/web3-shared-evm'
import { FungibleTokenAPI as EVM_FungibleTokenAPI } from '../../Web3/EVM/apis/FungibleTokenAPI.js'
import { formatAssets } from '../helpers.js'
import type { WalletTokenRecord, UserTotalBalance } from '../types.js'
import type { WalletTokenRecord } from '../types.js'
import { DEBANK_OPEN_API } from '../constants.js'
import { Duration } from '../../helpers/fetchCached.js'
import { fetchCachedJSON } from '../../helpers/fetchJSON.js'
Expand Down Expand Up @@ -70,10 +70,5 @@ class DeBankFungibleTokenAPI implements FungibleTokenAPI.Provider<ChainId, Schem
createIndicator(options?.indicator),
)
}

async getTotalBalance(/** address */ id: string) {
const url = urlcat(DEBANK_OPEN_API, '/v1/user/total_balance', { id })
return fetchCachedJSON<UserTotalBalance>(url)
}
}
export const DeBankFungibleToken = new DeBankFungibleTokenAPI()
13 changes: 0 additions & 13 deletions packages/web3-providers/src/DeBank/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,3 @@ export type GasPriceResponse = [
estimated_seconds: number
},
]

export interface UserTotalBalance {
total_usd_value: number
chain_list: Array<{
id: string
community_id: number
name: string
native_token_id: string
logo_url: string
wrapped_token_id: string
usd_value: number
}>
}
21 changes: 21 additions & 0 deletions packages/web3-providers/src/Firefly/Portfolio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isValidAddress as isValidAddressEVM } from '@masknet/web3-shared-evm'
import urlcat from 'urlcat'
import type { FireflyFarcasterAPI } from '@masknet/web3-providers/types'
import { fetchCachedJSON } from '../entry-helpers.js'
import { FIREFLY_BASE_URL } from './constants.js'
import { resolveFireflyResponseData } from './helpers.js'

/** `/v1/wallet/portfolio/summary` — only the total field this provider reads. */
type PortfolioSummaryResponse = FireflyFarcasterAPI.Response<{ total_assets?: { usd_value?: string } }>

/** Total portfolio USD value (tokens, predictions and perps) from the backend wallet summary. */
export async function getWalletPortfolioTotalValue(address: string) {
const url = urlcat(FIREFLY_BASE_URL, '/v1/wallet/portfolio/summary', {
// Lowercase EVM so checksummed variants hit the same server cache; other chains stay as-is.
address: isValidAddressEVM(address) ? address.toLowerCase() : address,
})
const res = await fetchCachedJSON<PortfolioSummaryResponse>(url)
const value = Number(resolveFireflyResponseData(res)?.total_assets?.usd_value)
if (!Number.isFinite(value)) throw new Error('Invalid portfolio summary')
return value
}
1 change: 1 addition & 0 deletions packages/web3-providers/src/Firefly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './RedPacket.js'
export * from './Twitter.js'
export * from './Farcaster.js'
export * from './Domain.js'
export * from './Portfolio.js'
export * from './WalletHistory.js'
export { FIREFLY_BASE_URL, FIREFLY_SITE_URL, PRIVY_SUPPORTED_CHAINS } from './constants.js'
export { FireflyEmbeddedWalletClient } from './EmbeddedWalletClient.js'
Expand Down
1 change: 1 addition & 0 deletions packages/web3-providers/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export {
FIREFLY_SITE_URL,
FireflyDomain,
FireflyWalletHistory,
getWalletPortfolioTotalValue,
// Embedded wallet (replaces @privy-io/react-auth)
FireflyEmbeddedWalletClient,
FireflyEmbeddedProvider,
Expand Down
63 changes: 63 additions & 0 deletions packages/web3-providers/tests/Firefly/Portfolio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
fetchCachedJSON: vi.fn(),
}))

vi.mock('../../src/helpers/fetchJSON.js', () => ({
fetchCachedJSON: mocks.fetchCachedJSON,
}))

// The real helper pulls in the `@masknet/web3-providers/types` package alias, which
// does not resolve under vitest. Mirror its behavior instead.
vi.mock('../../src/Firefly/helpers.js', () => ({
resolveFireflyResponseData: <T>({ data, error }: { data?: T; error?: string[] | string }) => {
if (error) throw new Error(Array.isArray(error) ? error[0] : error)
return data as T
},
}))

const { getWalletPortfolioTotalValue } = await import('../../src/Firefly/Portfolio.js')

describe('getWalletPortfolioTotalValue', () => {
beforeEach(() => {
mocks.fetchCachedJSON.mockReset()
})

it('returns the numeric total and lowercases the address', async () => {
mocks.fetchCachedJSON.mockResolvedValue({ code: 0, data: { total_assets: { usd_value: '26.66' } } })

await expect(getWalletPortfolioTotalValue('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')).resolves.toBe(26.66)
expect(mocks.fetchCachedJSON).toHaveBeenCalledWith(
'https://api.firefly.land/v1/wallet/portfolio/summary?address=0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
)
})

it('keeps a Solana base58 address as-is', async () => {
mocks.fetchCachedJSON.mockResolvedValue({ code: 0, data: { total_assets: { usd_value: '1.5' } } })
const solana = '7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs'

await expect(getWalletPortfolioTotalValue(solana)).resolves.toBe(1.5)
expect(mocks.fetchCachedJSON).toHaveBeenCalledWith(
`https://api.firefly.land/v1/wallet/portfolio/summary?address=${solana}`,
)
})

it('throws when the total is missing or not numeric', async () => {
mocks.fetchCachedJSON.mockResolvedValue({ code: 0, data: {} })
await expect(getWalletPortfolioTotalValue('0x0000000000000000000000000000000000000001')).rejects.toThrow(
'Invalid portfolio summary',
)

mocks.fetchCachedJSON.mockResolvedValue({ code: 0, data: { total_assets: { usd_value: '-' } } })
await expect(getWalletPortfolioTotalValue('0x0000000000000000000000000000000000000001')).rejects.toThrow(
'Invalid portfolio summary',
)
})

it('throws when the backend reports an error', async () => {
mocks.fetchCachedJSON.mockResolvedValue({ code: 1, error: ['boom'] })

await expect(getWalletPortfolioTotalValue('0x0000000000000000000000000000000000000001')).rejects.toThrow('boom')
})
})
Loading