diff --git a/miniapps/teleport/src/App.test.tsx b/miniapps/teleport/src/App.test.tsx
index 6223f5066..82cdcdfe5 100644
--- a/miniapps/teleport/src/App.test.tsx
+++ b/miniapps/teleport/src/App.test.tsx
@@ -236,6 +236,62 @@ describe('Teleport App', () => {
expect(screen.getByText(/0 NBT 可用/)).toBeInTheDocument()
})
+ it('should query balances by portal chain assets when account chain is alias', async () => {
+ const mockedGetTransmitAssetTypeList = vi.mocked(getTransmitAssetTypeList)
+ mockedGetTransmitAssetTypeList.mockResolvedValueOnce({
+ transmitSupport: {
+ BFMCHAIN: {
+ BFM: {
+ enable: true,
+ isAirdrop: false,
+ assetType: 'BFM',
+ recipientAddress: 'bReceiver',
+ targetChain: 'BFMETAV2',
+ targetAsset: 'BFM',
+ ratio: { numerator: 1, denominator: 250 },
+ transmitDate: {
+ startDate: '2020-01-01',
+ endDate: '2030-12-31',
+ },
+ },
+ },
+ },
+ })
+
+ mockBio.request.mockImplementation(
+ ({ method, params }: { method: string; params?: Array<{ chain?: string; asset?: string }> }) => {
+ if (method === 'bio_selectAccount') {
+ return Promise.resolve({ address: 'bSource', chain: 'bfmeta', name: 'Source' })
+ }
+ if (method === 'bio_getBalance') {
+ return Promise.resolve('99991361')
+ }
+ if (method === 'bio_closeSplashScreen') {
+ return Promise.resolve(null)
+ }
+ return Promise.resolve(null)
+ },
+ )
+
+ render(, { wrapper: createWrapper() })
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' })).toBeInTheDocument()
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' }))
+
+ await waitFor(() => {
+ expect(screen.getByTestId('asset-card-BFM')).toBeInTheDocument()
+ })
+
+ expect(mockBio.request).toHaveBeenCalledWith({
+ method: 'bio_getBalance',
+ params: [{ address: 'bSource', chain: 'bfmeta', asset: 'BFM' }],
+ })
+ expect(screen.getByText(/0\.99991361 BFM 可用/)).toBeInTheDocument()
+ })
+
it('should not exclude source address when selecting cross-chain target wallet', async () => {
mockBio.request.mockImplementation(({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
if (method === 'bio_selectAccount') {
@@ -284,6 +340,75 @@ describe('Teleport App', () => {
})
})
+ it('should exclude source address when selecting same-chain target wallet through alias', async () => {
+ const mockedGetTransmitAssetTypeList = vi.mocked(getTransmitAssetTypeList)
+ mockedGetTransmitAssetTypeList.mockResolvedValueOnce({
+ transmitSupport: {
+ BFMCHAIN: {
+ BFM: {
+ enable: true,
+ isAirdrop: false,
+ assetType: 'BFM',
+ recipientAddress: 'bReceiver',
+ targetChain: 'BFMCHAIN',
+ targetAsset: 'BFM',
+ ratio: { numerator: 1, denominator: 1 },
+ transmitDate: {
+ startDate: '2020-01-01',
+ endDate: '2030-12-31',
+ },
+ },
+ },
+ },
+ })
+
+ mockBio.request.mockImplementation(({ method }: { method: string }) => {
+ if (method === 'bio_selectAccount') {
+ return Promise.resolve({ address: 'bSource', chain: 'bfmeta', name: 'Source' })
+ }
+ if (method === 'bio_getBalance') {
+ return Promise.resolve('100000000')
+ }
+ if (method === 'bio_pickWallet') {
+ return Promise.resolve({ address: 'bTarget', chain: 'bfmeta', name: 'Target' })
+ }
+ if (method === 'bio_closeSplashScreen') {
+ return Promise.resolve(null)
+ }
+ return Promise.resolve(null)
+ })
+
+ render(, { wrapper: createWrapper() })
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' })).toBeInTheDocument()
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' }))
+
+ await waitFor(() => {
+ expect(screen.getByTestId('asset-card-BFM')).toBeInTheDocument()
+ })
+
+ fireEvent.click(screen.getByTestId('asset-card-BFM'))
+ await waitFor(() => {
+ expect(screen.getByTestId('amount-input')).toBeInTheDocument()
+ })
+ fireEvent.change(screen.getByTestId('amount-input'), { target: { value: '1' } })
+ fireEvent.click(screen.getByTestId('next-button'))
+ await waitFor(() => {
+ expect(screen.getByTestId('target-button')).toBeInTheDocument()
+ })
+ fireEvent.click(screen.getByTestId('target-button'))
+
+ await waitFor(() => {
+ expect(mockBio.request).toHaveBeenCalledWith({
+ method: 'bio_pickWallet',
+ params: [{ chain: 'BFMCHAIN', exclude: 'bSource' }],
+ })
+ })
+ })
+
it('should show sender/receiver addresses on confirm step and remove free-fee badge', async () => {
mockBio.request.mockImplementation(({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
if (method === 'bio_selectAccount') {
@@ -485,7 +610,7 @@ describe('Teleport App', () => {
mockBio.request.mockImplementation(
({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
if (method === 'bio_selectAccount') {
- return Promise.resolve({ address: '0xSourceBsc', chain: params?.[0]?.chain ?? 'BSC', name: 'Source' })
+ return Promise.resolve({ address: '0xSourceBsc', chain: 'binance', name: 'Source' })
}
if (method === 'bio_getBalance') {
return Promise.resolve('1000000000')
diff --git a/miniapps/teleport/src/App.tsx b/miniapps/teleport/src/App.tsx
index f9e6c0b81..503720a46 100644
--- a/miniapps/teleport/src/App.tsx
+++ b/miniapps/teleport/src/App.tsx
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import type { BioAccount, BioSignedTransaction, BioUnsignedTransaction } from '@biochain/bio-sdk';
+import { normalizeChainId, type BioAccount, type BioSignedTransaction, type BioUnsignedTransaction } from '@biochain/bio-sdk';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -212,6 +212,18 @@ const CHAIN_COLORS: Record = {
const normalizeInternalChainName = (value: string): InternalChainName =>
value.toUpperCase() as InternalChainName;
+const normalizeTeleportChain = (value: string | null | undefined): string => {
+ const normalized = value?.trim();
+ if (!normalized) return '';
+ return normalizeChainId(normalized);
+};
+
+const isSameTeleportChain = (left: string | null | undefined, right: string | null | undefined): boolean => {
+ const normalizedLeft = normalizeTeleportChain(left);
+ const normalizedRight = normalizeTeleportChain(right);
+ return normalizedLeft.length > 0 && normalizedLeft === normalizedRight;
+};
+
const normalizeInputAmount = (value: string, decimals: number): string => {
const normalized = value.trim();
if (!/^\d+(\.\d+)?$/.test(normalized)) {
@@ -312,7 +324,7 @@ export default function App() {
if (!assets || !sourceAccount) return [];
const sourceChain = selectedSourceChain ?? sourceAccount.chain;
return assets
- .filter((asset) => asset.chain.toLowerCase() === sourceChain.toLowerCase())
+ .filter((asset) => isSameTeleportChain(asset.chain, sourceChain))
.map((asset) => ({
...asset,
balance: formatRawBalance(
@@ -365,8 +377,17 @@ export default function App() {
});
setSourceAccount(account);
const accountChain = account.chain || portalChain;
- const chainAssets = (assets ?? []).filter((asset) => asset.chain.toLowerCase() === accountChain.toLowerCase());
+ const sourceAssetChain = portalChain;
+ const chainAssetsByPortal = (assets ?? []).filter(
+ (asset) => isSameTeleportChain(asset.chain, sourceAssetChain),
+ );
+ const chainAssets =
+ chainAssetsByPortal.length > 0
+ ? chainAssetsByPortal
+ : (assets ?? []).filter((asset) => isSameTeleportChain(asset.chain, accountChain));
const uniqueAssetTypes = [...new Set(chainAssets.map((asset) => asset.assetType.toUpperCase()))];
+ const balanceQueryChain =
+ normalizeTeleportChain(account.chain) || normalizeTeleportChain(portalChain);
if (uniqueAssetTypes.length > 0) {
const balanceEntries = await Promise.all(
@@ -374,7 +395,7 @@ export default function App() {
try {
const rawBalance = await bio.request({
method: 'bio_getBalance',
- params: [{ address: account.address, chain: account.chain, asset: assetType }],
+ params: [{ address: account.address, chain: balanceQueryChain, asset: assetType }],
});
return [assetType, rawBalance] as const;
} catch {
@@ -413,7 +434,7 @@ export default function App() {
if (!window.bio || !sourceAccount || !selectedAsset) return;
setLoading(true);
setError(null);
- const shouldExcludeSameAddress = sourceAccount.chain.toLowerCase() === selectedAsset.targetChain.toLowerCase();
+ const shouldExcludeSameAddress = isSameTeleportChain(sourceAccount.chain, selectedAsset.targetChain);
try {
const account = await window.bio.request({
method: 'bio_pickWallet',
@@ -446,12 +467,12 @@ export default function App() {
assetType: selectedAsset.targetAsset,
};
- const chainLower = sourceAccount.chain.toLowerCase();
+ const sourceChain = normalizeTeleportChain(sourceAccount.chain);
const isInternalChain =
- chainLower !== 'eth' &&
- chainLower !== 'bsc' &&
- chainLower !== 'tron' &&
- chainLower !== 'trc20';
+ sourceChain !== 'ethereum' &&
+ sourceChain !== 'binance' &&
+ sourceChain !== 'tron' &&
+ sourceChain !== 'trc20';
const remark = isInternalChain
? {
@@ -492,12 +513,12 @@ export default function App() {
// 4. 构造 fromTrJson(根据链类型)
// 注意:EVM 需要 raw signed tx 的 hex;TRON/内链需要结构化交易体
const fromTrJson: FromTrJson = {};
- const isTronChain = chainLower === 'tron' || chainLower === 'trc20';
- const isTrc20 = chainLower === 'trc20' || (chainLower === 'tron' && !!selectedAsset.contractAddress);
+ const isTronChain = sourceChain === 'tron' || sourceChain === 'trc20';
+ const isTrc20 = sourceChain === 'trc20' || (sourceChain === 'tron' && !!selectedAsset.contractAddress);
- if (chainLower === 'eth') {
+ if (sourceChain === 'ethereum') {
fromTrJson.eth = { signTransData: extractEvmSignedTxData(signedTx.data, 'ETH') };
- } else if (chainLower === 'bsc') {
+ } else if (sourceChain === 'binance') {
fromTrJson.bsc = { signTransData: extractEvmSignedTxData(signedTx.data, 'BSC') };
} else if (isTronChain) {
if (isTrc20) {
diff --git a/src/services/authorize/dweb.ts b/src/services/authorize/dweb.ts
index bbe85206f..2bdea3a99 100644
--- a/src/services/authorize/dweb.ts
+++ b/src/services/authorize/dweb.ts
@@ -1,6 +1,9 @@
import { dwebServiceWorker, type ServiceWorkerFetchEvent } from '@plaoc/plugins'
+import { normalizeChainId } from '@biochain/bio-sdk'
import { WALLET_PLAOC_PATH } from './paths'
import type { CallerAppInfo, IPlaocAdapter } from './types'
+import { getChainProvider } from '@/services/chain-adapter/providers'
+import { chainConfigService } from '@/services/chain-config/service'
type WireEnvelope = Readonly<{ data: T }>
@@ -85,7 +88,7 @@ function parseAssetTypeBalancePayload(signaturedata: string): {
return { chainName, senderAddress, assetTypes }
}
-function computeAssetTypeBalances(req: ReturnType): Record<
+async function computeAssetTypeBalances(req: ReturnType): Promise {
+>> {
if (!req) return {}
- // tokens 数据已从 walletStore 移除 - 需要从 chain-provider 获取
- // TODO: 使用 getChainProvider(req.chainName).tokenBalances 获取实时余额
- // 目前返回所有请求的 assetType 的默认值
-
+ const resolvedChain = normalizeChainId(req.chainName)
+ const provider = getChainProvider(resolvedChain)
+ const defaultDecimals = chainConfigService.getDecimals(resolvedChain)
const result: Record<
string,
{
@@ -110,13 +112,35 @@ function computeAssetTypeBalances(req: ReturnType = {}
- for (const reqAsset of req.assetTypes) {
- const wantedAssetType = reqAsset.assetType
- result[wantedAssetType] = {
- assetType: wantedAssetType,
- decimals: 0,
- balance: '0',
- ...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
+ try {
+ const balances = await provider.allBalances.fetch({ address: req.senderAddress })
+
+ for (const reqAsset of req.assetTypes) {
+ const wantedAssetType = reqAsset.assetType
+ const wantedAssetUpper = wantedAssetType.toUpperCase()
+ const wantedContract = reqAsset.contractAddress?.trim().toLowerCase()
+ const matched = balances.find((item) => {
+ if (wantedContract) {
+ return (item.contractAddress?.toLowerCase() ?? '') === wantedContract
+ }
+ return item.symbol.toUpperCase() === wantedAssetUpper
+ })
+
+ result[wantedAssetType] = {
+ assetType: wantedAssetType,
+ decimals: matched?.decimals ?? defaultDecimals,
+ balance: matched?.amount.toRawString() ?? '0',
+ ...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
+ }
+ }
+ } catch {
+ for (const reqAsset of req.assetTypes) {
+ result[reqAsset.assetType] = {
+ assetType: reqAsset.assetType,
+ decimals: defaultDecimals,
+ balance: '0',
+ ...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
+ }
}
}
@@ -155,7 +179,7 @@ async function handleAuthorizeFetch(event: ServiceWorkerFetchEvent): Promise {
}
let handleGetBalance: typeof import('../handlers/wallet').handleGetBalance
- let mockFetch: ReturnType
+ let mockNativeFetch: ReturnType
+ let mockAllBalancesFetch: ReturnType
let mockGetChainProvider: ReturnType
beforeEach(async () => {
@@ -18,10 +19,14 @@ describe('handleGetBalance', () => {
vi.resetModules()
// Create fresh mocks
- mockFetch = vi.fn()
+ mockNativeFetch = vi.fn()
+ mockAllBalancesFetch = vi.fn()
mockGetChainProvider = vi.fn(() => ({
nativeBalance: {
- fetch: mockFetch,
+ fetch: mockNativeFetch,
+ },
+ allBalances: {
+ fetch: mockAllBalancesFetch,
},
}))
@@ -71,7 +76,7 @@ describe('handleGetBalance', () => {
describe('successful balance query', () => {
it('returns balance from chain provider', async () => {
- mockFetch.mockResolvedValue({
+ mockNativeFetch.mockResolvedValue({
amount: { toRawString: () => '1000000000' },
})
@@ -82,11 +87,11 @@ describe('handleGetBalance', () => {
expect(result).toBe('1000000000')
expect(mockGetChainProvider).toHaveBeenCalledWith('bfmeta')
- expect(mockFetch).toHaveBeenCalledWith({ address: 'b123456789' })
+ expect(mockNativeFetch).toHaveBeenCalledWith({ address: 'b123456789' })
})
it('returns "0" for account with no balance', async () => {
- mockFetch.mockResolvedValue({
+ mockNativeFetch.mockResolvedValue({
amount: { toRawString: () => '0' },
})
@@ -99,7 +104,7 @@ describe('handleGetBalance', () => {
})
it('returns "0" when balance is null', async () => {
- mockFetch.mockResolvedValue(null)
+ mockNativeFetch.mockResolvedValue(null)
const result = await handleGetBalance(
{ address: 'b123', chain: 'bfmeta' },
@@ -112,7 +117,7 @@ describe('handleGetBalance', () => {
describe('error handling', () => {
it('returns "0" when provider throws error', async () => {
- mockFetch.mockRejectedValue(new Error('Network error'))
+ mockNativeFetch.mockRejectedValue(new Error('Network error'))
const result = await handleGetBalance(
{ address: 'b123', chain: 'bfmeta' },
@@ -123,7 +128,7 @@ describe('handleGetBalance', () => {
})
it('returns "0" when amount is undefined', async () => {
- mockFetch.mockResolvedValue({ amount: undefined })
+ mockNativeFetch.mockResolvedValue({ amount: undefined })
const result = await handleGetBalance(
{ address: 'b123', chain: 'bfmeta' },
@@ -136,7 +141,7 @@ describe('handleGetBalance', () => {
describe('different chains', () => {
it('works with ethereum chain', async () => {
- mockFetch.mockResolvedValue({
+ mockNativeFetch.mockResolvedValue({
amount: { toRawString: () => '5000000000000000000' },
})
@@ -150,7 +155,7 @@ describe('handleGetBalance', () => {
})
it('works with tron chain', async () => {
- mockFetch.mockResolvedValue({
+ mockNativeFetch.mockResolvedValue({
amount: { toRawString: () => '100000000' },
})
@@ -162,5 +167,122 @@ describe('handleGetBalance', () => {
expect(result).toBe('100000000')
expect(mockGetChainProvider).toHaveBeenCalledWith('tron')
})
+
+ it('normalizes API chain alias before querying provider', async () => {
+ mockNativeFetch.mockResolvedValue({
+ amount: { toRawString: () => '5000000000000000000' },
+ })
+
+ const result = await handleGetBalance(
+ { address: '0x1234567890abcdef', chain: 'BSC' },
+ mockContext
+ )
+
+ expect(result).toBe('5000000000000000000')
+ expect(mockGetChainProvider).toHaveBeenCalledWith('binance')
+ })
+ })
+
+ describe('asset-based balance query', () => {
+ it('returns specific asset raw balance when asset is provided', async () => {
+ mockAllBalancesFetch.mockResolvedValue([
+ {
+ symbol: 'USDT',
+ decimals: 18,
+ amount: { toRawString: () => '123000000000000000000' },
+ },
+ ])
+
+ const result = await handleGetBalance(
+ { address: '0x123', chain: 'bsc', asset: 'USDT' },
+ mockContext
+ )
+
+ expect(result).toBe('123000000000000000000')
+ expect(mockAllBalancesFetch).toHaveBeenCalledWith({ address: '0x123' })
+ })
+
+ it('returns requested assets map for assets[] batch mode', async () => {
+ mockAllBalancesFetch.mockResolvedValue([
+ {
+ symbol: 'USDT',
+ contractAddress: '0x55d398326f99059ff775485246999027b3197955',
+ decimals: 18,
+ amount: { toRawString: () => '1000000000000000000' },
+ },
+ {
+ symbol: 'BSC',
+ decimals: 18,
+ amount: { toRawString: () => '2000000000000000000' },
+ },
+ ])
+
+ const result = await handleGetBalance(
+ {
+ address: '0xabc',
+ chain: 'bsc',
+ assets: [
+ {
+ assetType: 'USDT',
+ contractAddress: '0x55d398326f99059ff775485246999027b3197955',
+ },
+ { assetType: 'BSC' },
+ { assetType: 'MISSING' },
+ ],
+ },
+ mockContext
+ )
+
+ expect(result).toEqual({
+ USDT: {
+ assetType: 'USDT',
+ decimals: 18,
+ balance: '1000000000000000000',
+ contracts: '0x55d398326f99059ff775485246999027b3197955',
+ contractAddress: '0x55d398326f99059ff775485246999027b3197955',
+ },
+ BSC: {
+ assetType: 'BSC',
+ decimals: 18,
+ balance: '2000000000000000000',
+ },
+ MISSING: {
+ assetType: 'MISSING',
+ decimals: 18,
+ balance: '0',
+ },
+ })
+ })
+
+ it('returns zeroed map when batch provider request fails', async () => {
+ mockAllBalancesFetch.mockRejectedValue(new Error('Network error'))
+
+ const result = await handleGetBalance(
+ {
+ address: '0xabc',
+ chain: 'bsc',
+ assets: [
+ { assetType: 'USDT', contractAddress: '0x55d398326f99059ff775485246999027b3197955' },
+ { assetType: 'BSC' },
+ ],
+ },
+ mockContext
+ )
+
+ expect(result).toEqual({
+ USDT: {
+ assetType: 'USDT',
+ decimals: 18,
+ balance: '0',
+ contracts: '0x55d398326f99059ff775485246999027b3197955',
+ contractAddress: '0x55d398326f99059ff775485246999027b3197955',
+ },
+ BSC: {
+ assetType: 'BSC',
+ decimals: 18,
+ balance: '0',
+ },
+ })
+ })
})
})
diff --git a/src/services/ecosystem/handlers/wallet.ts b/src/services/ecosystem/handlers/wallet.ts
index 668e58f17..4754a8e22 100644
--- a/src/services/ecosystem/handlers/wallet.ts
+++ b/src/services/ecosystem/handlers/wallet.ts
@@ -4,9 +4,12 @@
import type { MethodHandler, BioAccount } from '../types'
import { BioErrorCodes } from '../types'
+import { normalizeChainId } from '@biochain/bio-sdk'
import { HandlerContext } from './context'
import { enqueueMiniappSheet } from '../sheet-queue'
import { getChainProvider } from '@/services/chain-adapter/providers'
+import { chainConfigService } from '@/services/chain-config/service'
+import { chainConfigActions, chainConfigStore } from '@/stores/chain-config'
import { walletStore } from '@/stores/wallet'
// 兼容旧 API,逐步迁移到 HandlerContext
@@ -121,25 +124,111 @@ export const handleChainId: MethodHandler = async (_params, _context) => {
/** bio_getBalance - Get balance */
export const handleGetBalance: MethodHandler = async (params, _context) => {
- const opts = params as { address?: string; chain?: string; asset?: string } | undefined
+ const opts = params as
+ | {
+ address?: string
+ chain?: string
+ asset?: string
+ assets?: Array<{ assetType?: string; contractAddress?: string }>
+ }
+ | undefined
if (!opts?.address || !opts?.chain) {
throw Object.assign(new Error('Missing address or chain'), { code: BioErrorCodes.INVALID_PARAMS })
}
- const provider = getChainProvider(opts.chain)
+ const resolvedChain = normalizeChainId(opts.chain)
const wantedAsset = opts.asset?.trim().toUpperCase()
+ const wantedAssets = Array.isArray(opts.assets) ? opts.assets : []
+
+ if (!chainConfigStore.state.snapshot) {
+ await chainConfigActions.initialize()
+ }
+
+ const provider = getChainProvider(resolvedChain)
+
+ const buildBatchFallback = () => {
+ const defaultDecimals = chainConfigService.getDecimals(resolvedChain)
+ const fallback: Record<
+ string,
+ {
+ assetType: string
+ decimals: number
+ balance: string
+ contracts?: string
+ contractAddress?: string
+ }
+ > = {}
+
+ for (const item of wantedAssets) {
+ const requestedAssetType = item.assetType?.trim().toUpperCase()
+ if (!requestedAssetType) continue
+ fallback[requestedAssetType] = {
+ assetType: requestedAssetType,
+ decimals: defaultDecimals,
+ balance: '0',
+ ...(item.contractAddress
+ ? { contracts: item.contractAddress, contractAddress: item.contractAddress }
+ : {}),
+ }
+ }
+ return fallback
+ }
try {
+ if (wantedAssets.length > 0) {
+ const balances = await provider.allBalances.fetch({ address: opts.address })
+ const defaultDecimals = chainConfigService.getDecimals(resolvedChain)
+
+ const result: Record<
+ string,
+ {
+ assetType: string
+ decimals: number
+ balance: string
+ contracts?: string
+ contractAddress?: string
+ }
+ > = {}
+
+ for (const item of wantedAssets) {
+ const requestedAssetType = item.assetType?.trim().toUpperCase()
+ if (!requestedAssetType) continue
+ const requestedContract = item.contractAddress?.trim().toLowerCase()
+
+ const matched = balances.find((balanceItem) => {
+ if (requestedContract) {
+ return (balanceItem.contractAddress?.toLowerCase() ?? '') === requestedContract
+ }
+ return balanceItem.symbol.toUpperCase() === requestedAssetType
+ })
+
+ result[requestedAssetType] = {
+ assetType: requestedAssetType,
+ decimals: matched?.decimals ?? defaultDecimals,
+ balance: matched?.amount.toRawString() ?? '0',
+ ...(item.contractAddress
+ ? { contracts: item.contractAddress, contractAddress: item.contractAddress }
+ : {}),
+ }
+ }
+ return result
+ }
+
if (wantedAsset) {
const balances = await provider.allBalances.fetch({ address: opts.address })
const matched = balances.find((item) => item.symbol.toUpperCase() === wantedAsset)
- return matched?.amount.toRawString() ?? '0'
+ const raw = matched?.amount.toRawString() ?? '0'
+ return raw
}
// 使用 ChainProvider 的 nativeBalance fetcher
const balance = await provider.nativeBalance.fetch({ address: opts.address })
- return balance?.amount.toRawString() ?? '0'
+ const raw = balance?.amount.toRawString() ?? '0'
+ return raw
} catch {
+ if (wantedAssets.length > 0) {
+ return buildBatchFallback()
+ }
return '0'
}
}