Skip to content

Commit 3efa621

Browse files
committed
fix(miniapp): normalize chain aliases in balance flow
1 parent 7439040 commit 3efa621

5 files changed

Lines changed: 425 additions & 44 deletions

File tree

miniapps/teleport/src/App.test.tsx

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,62 @@ describe('Teleport App', () => {
236236
expect(screen.getByText(/0 NBT /)).toBeInTheDocument()
237237
})
238238

239+
it('should query balances by portal chain assets when account chain is alias', async () => {
240+
const mockedGetTransmitAssetTypeList = vi.mocked(getTransmitAssetTypeList)
241+
mockedGetTransmitAssetTypeList.mockResolvedValueOnce({
242+
transmitSupport: {
243+
BFMCHAIN: {
244+
BFM: {
245+
enable: true,
246+
isAirdrop: false,
247+
assetType: 'BFM',
248+
recipientAddress: 'bReceiver',
249+
targetChain: 'BFMETAV2',
250+
targetAsset: 'BDT',
251+
ratio: { numerator: 1, denominator: 250 },
252+
transmitDate: {
253+
startDate: '2020-01-01',
254+
endDate: '2030-12-31',
255+
},
256+
},
257+
},
258+
},
259+
})
260+
261+
mockBio.request.mockImplementation(
262+
({ method, params }: { method: string; params?: Array<{ chain?: string; asset?: string }> }) => {
263+
if (method === 'bio_selectAccount') {
264+
return Promise.resolve({ address: 'bSource', chain: 'bfmeta', name: 'Source' })
265+
}
266+
if (method === 'bio_getBalance') {
267+
return Promise.resolve('99991361')
268+
}
269+
if (method === 'bio_closeSplashScreen') {
270+
return Promise.resolve(null)
271+
}
272+
return Promise.resolve(null)
273+
},
274+
)
275+
276+
render(<App />, { wrapper: createWrapper() })
277+
278+
await waitFor(() => {
279+
expect(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' })).toBeInTheDocument()
280+
})
281+
282+
fireEvent.click(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' }))
283+
284+
await waitFor(() => {
285+
expect(screen.getByTestId('asset-card-BFM')).toBeInTheDocument()
286+
})
287+
288+
expect(mockBio.request).toHaveBeenCalledWith({
289+
method: 'bio_getBalance',
290+
params: [{ address: 'bSource', chain: 'bfmeta', asset: 'BFM' }],
291+
})
292+
expect(screen.getByText(/0\.99991361 BFM /)).toBeInTheDocument()
293+
})
294+
239295
it('should not exclude source address when selecting cross-chain target wallet', async () => {
240296
mockBio.request.mockImplementation(({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
241297
if (method === 'bio_selectAccount') {
@@ -284,6 +340,75 @@ describe('Teleport App', () => {
284340
})
285341
})
286342

343+
it('should exclude source address when selecting same-chain target wallet through alias', async () => {
344+
const mockedGetTransmitAssetTypeList = vi.mocked(getTransmitAssetTypeList)
345+
mockedGetTransmitAssetTypeList.mockResolvedValueOnce({
346+
transmitSupport: {
347+
BFMCHAIN: {
348+
BFM: {
349+
enable: true,
350+
isAirdrop: false,
351+
assetType: 'BFM',
352+
recipientAddress: 'bReceiver',
353+
targetChain: 'BFMCHAIN',
354+
targetAsset: 'BFM',
355+
ratio: { numerator: 1, denominator: 1 },
356+
transmitDate: {
357+
startDate: '2020-01-01',
358+
endDate: '2030-12-31',
359+
},
360+
},
361+
},
362+
},
363+
})
364+
365+
mockBio.request.mockImplementation(({ method }: { method: string }) => {
366+
if (method === 'bio_selectAccount') {
367+
return Promise.resolve({ address: 'bSource', chain: 'bfmeta', name: 'Source' })
368+
}
369+
if (method === 'bio_getBalance') {
370+
return Promise.resolve('100000000')
371+
}
372+
if (method === 'bio_pickWallet') {
373+
return Promise.resolve({ address: 'bTarget', chain: 'bfmeta', name: 'Target' })
374+
}
375+
if (method === 'bio_closeSplashScreen') {
376+
return Promise.resolve(null)
377+
}
378+
return Promise.resolve(null)
379+
})
380+
381+
render(<App />, { wrapper: createWrapper() })
382+
383+
await waitFor(() => {
384+
expect(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' })).toBeInTheDocument()
385+
})
386+
387+
fireEvent.click(screen.getByRole('button', { name: '启动 BFMCHAIN 传送门' }))
388+
389+
await waitFor(() => {
390+
expect(screen.getByTestId('asset-card-BFM')).toBeInTheDocument()
391+
})
392+
393+
fireEvent.click(screen.getByTestId('asset-card-BFM'))
394+
await waitFor(() => {
395+
expect(screen.getByTestId('amount-input')).toBeInTheDocument()
396+
})
397+
fireEvent.change(screen.getByTestId('amount-input'), { target: { value: '1' } })
398+
fireEvent.click(screen.getByTestId('next-button'))
399+
await waitFor(() => {
400+
expect(screen.getByTestId('target-button')).toBeInTheDocument()
401+
})
402+
fireEvent.click(screen.getByTestId('target-button'))
403+
404+
await waitFor(() => {
405+
expect(mockBio.request).toHaveBeenCalledWith({
406+
method: 'bio_pickWallet',
407+
params: [{ chain: 'BFMCHAIN', exclude: 'bSource' }],
408+
})
409+
})
410+
})
411+
287412
it('should show sender/receiver addresses on confirm step and remove free-fee badge', async () => {
288413
mockBio.request.mockImplementation(({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
289414
if (method === 'bio_selectAccount') {
@@ -485,7 +610,7 @@ describe('Teleport App', () => {
485610
mockBio.request.mockImplementation(
486611
({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => {
487612
if (method === 'bio_selectAccount') {
488-
return Promise.resolve({ address: '0xSourceBsc', chain: params?.[0]?.chain ?? 'BSC', name: 'Source' })
613+
return Promise.resolve({ address: '0xSourceBsc', chain: 'binance', name: 'Source' })
489614
}
490615
if (method === 'bio_getBalance') {
491616
return Promise.resolve('1000000000')

miniapps/teleport/src/App.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useCallback, useEffect, useMemo } from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import type { BioAccount, BioSignedTransaction, BioUnsignedTransaction } from '@biochain/bio-sdk';
3+
import { normalizeChainId, type BioAccount, type BioSignedTransaction, type BioUnsignedTransaction } from '@biochain/bio-sdk';
44
import { Button } from '@/components/ui/button';
55
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
66
import { Input } from '@/components/ui/input';
@@ -212,6 +212,18 @@ const CHAIN_COLORS: Record<string, string> = {
212212
const normalizeInternalChainName = (value: string): InternalChainName =>
213213
value.toUpperCase() as InternalChainName;
214214

215+
const normalizeTeleportChain = (value: string | null | undefined): string => {
216+
const normalized = value?.trim();
217+
if (!normalized) return '';
218+
return normalizeChainId(normalized);
219+
};
220+
221+
const isSameTeleportChain = (left: string | null | undefined, right: string | null | undefined): boolean => {
222+
const normalizedLeft = normalizeTeleportChain(left);
223+
const normalizedRight = normalizeTeleportChain(right);
224+
return normalizedLeft.length > 0 && normalizedLeft === normalizedRight;
225+
};
226+
215227
const normalizeInputAmount = (value: string, decimals: number): string => {
216228
const normalized = value.trim();
217229
if (!/^\d+(\.\d+)?$/.test(normalized)) {
@@ -312,7 +324,7 @@ export default function App() {
312324
if (!assets || !sourceAccount) return [];
313325
const sourceChain = selectedSourceChain ?? sourceAccount.chain;
314326
return assets
315-
.filter((asset) => asset.chain.toLowerCase() === sourceChain.toLowerCase())
327+
.filter((asset) => isSameTeleportChain(asset.chain, sourceChain))
316328
.map((asset) => ({
317329
...asset,
318330
balance: formatRawBalance(
@@ -365,16 +377,25 @@ export default function App() {
365377
});
366378
setSourceAccount(account);
367379
const accountChain = account.chain || portalChain;
368-
const chainAssets = (assets ?? []).filter((asset) => asset.chain.toLowerCase() === accountChain.toLowerCase());
380+
const sourceAssetChain = portalChain;
381+
const chainAssetsByPortal = (assets ?? []).filter(
382+
(asset) => isSameTeleportChain(asset.chain, sourceAssetChain),
383+
);
384+
const chainAssets =
385+
chainAssetsByPortal.length > 0
386+
? chainAssetsByPortal
387+
: (assets ?? []).filter((asset) => isSameTeleportChain(asset.chain, accountChain));
369388
const uniqueAssetTypes = [...new Set(chainAssets.map((asset) => asset.assetType.toUpperCase()))];
389+
const balanceQueryChain =
390+
normalizeTeleportChain(account.chain) || normalizeTeleportChain(portalChain);
370391

371392
if (uniqueAssetTypes.length > 0) {
372393
const balanceEntries = await Promise.all(
373394
uniqueAssetTypes.map(async (assetType) => {
374395
try {
375396
const rawBalance = await bio.request<string>({
376397
method: 'bio_getBalance',
377-
params: [{ address: account.address, chain: account.chain, asset: assetType }],
398+
params: [{ address: account.address, chain: balanceQueryChain, asset: assetType }],
378399
});
379400
return [assetType, rawBalance] as const;
380401
} catch {
@@ -413,7 +434,7 @@ export default function App() {
413434
if (!window.bio || !sourceAccount || !selectedAsset) return;
414435
setLoading(true);
415436
setError(null);
416-
const shouldExcludeSameAddress = sourceAccount.chain.toLowerCase() === selectedAsset.targetChain.toLowerCase();
437+
const shouldExcludeSameAddress = isSameTeleportChain(sourceAccount.chain, selectedAsset.targetChain);
417438
try {
418439
const account = await window.bio.request<BioAccount>({
419440
method: 'bio_pickWallet',
@@ -446,12 +467,12 @@ export default function App() {
446467
assetType: selectedAsset.targetAsset,
447468
};
448469

449-
const chainLower = sourceAccount.chain.toLowerCase();
470+
const sourceChain = normalizeTeleportChain(sourceAccount.chain);
450471
const isInternalChain =
451-
chainLower !== 'eth' &&
452-
chainLower !== 'bsc' &&
453-
chainLower !== 'tron' &&
454-
chainLower !== 'trc20';
472+
sourceChain !== 'ethereum' &&
473+
sourceChain !== 'binance' &&
474+
sourceChain !== 'tron' &&
475+
sourceChain !== 'trc20';
455476

456477
const remark = isInternalChain
457478
? {
@@ -492,12 +513,12 @@ export default function App() {
492513
// 4. 构造 fromTrJson(根据链类型)
493514
// 注意:EVM 需要 raw signed tx 的 hex;TRON/内链需要结构化交易体
494515
const fromTrJson: FromTrJson = {};
495-
const isTronChain = chainLower === 'tron' || chainLower === 'trc20';
496-
const isTrc20 = chainLower === 'trc20' || (chainLower === 'tron' && !!selectedAsset.contractAddress);
516+
const isTronChain = sourceChain === 'tron' || sourceChain === 'trc20';
517+
const isTrc20 = sourceChain === 'trc20' || (sourceChain === 'tron' && !!selectedAsset.contractAddress);
497518

498-
if (chainLower === 'eth') {
519+
if (sourceChain === 'ethereum') {
499520
fromTrJson.eth = { signTransData: extractEvmSignedTxData(signedTx.data, 'ETH') };
500-
} else if (chainLower === 'bsc') {
521+
} else if (sourceChain === 'binance') {
501522
fromTrJson.bsc = { signTransData: extractEvmSignedTxData(signedTx.data, 'BSC') };
502523
} else if (isTronChain) {
503524
if (isTrc20) {

src/services/authorize/dweb.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { dwebServiceWorker, type ServiceWorkerFetchEvent } from '@plaoc/plugins'
2+
import { normalizeChainId } from '@biochain/bio-sdk'
23
import { WALLET_PLAOC_PATH } from './paths'
34
import type { CallerAppInfo, IPlaocAdapter } from './types'
5+
import { getChainProvider } from '@/services/chain-adapter/providers'
6+
import { chainConfigService } from '@/services/chain-config/service'
47

58
type WireEnvelope<T> = Readonly<{ data: T }>
69

@@ -85,21 +88,20 @@ function parseAssetTypeBalancePayload(signaturedata: string): {
8588
return { chainName, senderAddress, assetTypes }
8689
}
8790

88-
function computeAssetTypeBalances(req: ReturnType<typeof parseAssetTypeBalancePayload>): Record<
91+
async function computeAssetTypeBalances(req: ReturnType<typeof parseAssetTypeBalancePayload>): Promise<Record<
8992
string,
9093
{
9194
assetType: string
9295
decimals: number
9396
balance: string
9497
contracts?: string
9598
}
96-
> {
99+
>> {
97100
if (!req) return {}
98101

99-
// tokens 数据已从 walletStore 移除 - 需要从 chain-provider 获取
100-
// TODO: 使用 getChainProvider(req.chainName).tokenBalances 获取实时余额
101-
// 目前返回所有请求的 assetType 的默认值
102-
102+
const resolvedChain = normalizeChainId(req.chainName)
103+
const provider = getChainProvider(resolvedChain)
104+
const defaultDecimals = chainConfigService.getDecimals(resolvedChain)
103105
const result: Record<
104106
string,
105107
{
@@ -110,13 +112,35 @@ function computeAssetTypeBalances(req: ReturnType<typeof parseAssetTypeBalancePa
110112
}
111113
> = {}
112114

113-
for (const reqAsset of req.assetTypes) {
114-
const wantedAssetType = reqAsset.assetType
115-
result[wantedAssetType] = {
116-
assetType: wantedAssetType,
117-
decimals: 0,
118-
balance: '0',
119-
...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
115+
try {
116+
const balances = await provider.allBalances.fetch({ address: req.senderAddress })
117+
118+
for (const reqAsset of req.assetTypes) {
119+
const wantedAssetType = reqAsset.assetType
120+
const wantedAssetUpper = wantedAssetType.toUpperCase()
121+
const wantedContract = reqAsset.contractAddress?.trim().toLowerCase()
122+
const matched = balances.find((item) => {
123+
if (wantedContract) {
124+
return (item.contractAddress?.toLowerCase() ?? '') === wantedContract
125+
}
126+
return item.symbol.toUpperCase() === wantedAssetUpper
127+
})
128+
129+
result[wantedAssetType] = {
130+
assetType: wantedAssetType,
131+
decimals: matched?.decimals ?? defaultDecimals,
132+
balance: matched?.amount.toRawString() ?? '0',
133+
...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
134+
}
135+
}
136+
} catch {
137+
for (const reqAsset of req.assetTypes) {
138+
result[reqAsset.assetType] = {
139+
assetType: reqAsset.assetType,
140+
decimals: defaultDecimals,
141+
balance: '0',
142+
...(reqAsset.contractAddress ? { contracts: reqAsset.contractAddress } : {}),
143+
}
120144
}
121145
}
122146

@@ -155,7 +179,7 @@ async function handleAuthorizeFetch(event: ServiceWorkerFetchEvent): Promise<voi
155179

156180
const fastPayload = signaturedata ? parseAssetTypeBalancePayload(signaturedata) : null
157181
if (fastPayload) {
158-
const balances = computeAssetTypeBalances(fastPayload)
182+
const balances = await computeAssetTypeBalances(fastPayload)
159183
await respondJson(event, [balances])
160184
return
161185
}

0 commit comments

Comments
 (0)