From 56106aa97de5bcb8d26f58ede96ae6faebfaaff9 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Sun, 11 Jan 2026 23:16:58 +0800 Subject: [PATCH] fix(forge): add deposit address format validation Problem: - TRON chain requires base58 address format (T...), but API may return EVM format (0x...) causing 'Invalid base58 character: 0' error Solution: - Add validateDepositAddress() to validate address format per chain - Add isValidTronBase58Address(), isTronHexAddress(), isEvmAddress() - Validate deposit address before creating transaction in useForge - Show descriptive error if address format is wrong Tests: - 19 new tests for chain utilities - 2 new tests for TRON address handling in useForge - All 59 tests passing --- miniapps/forge/src/hooks/useForge.test.ts | 72 +++++++++++- miniapps/forge/src/hooks/useForge.ts | 8 ++ miniapps/forge/src/lib/chain.test.ts | 131 ++++++++++++++++++++++ miniapps/forge/src/lib/chain.ts | 58 ++++++++++ 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 miniapps/forge/src/lib/chain.test.ts diff --git a/miniapps/forge/src/hooks/useForge.test.ts b/miniapps/forge/src/hooks/useForge.test.ts index 364e8841f..832ebe520 100644 --- a/miniapps/forge/src/hooks/useForge.test.ts +++ b/miniapps/forge/src/hooks/useForge.test.ts @@ -17,9 +17,9 @@ const mockBio = { const mockForgeParams: ForgeParams = { externalChain: 'ETH', externalAsset: 'ETH', - depositAddress: '0xdeposit123', + depositAddress: '0x1234567890abcdef1234567890abcdef12345678', amount: '1.5', - externalAccount: { address: '0xexternal123', chain: 'eth', publicKey: '0x' }, + externalAccount: { address: '0xabcdef1234567890abcdef1234567890abcdef12', chain: 'eth', publicKey: '0x' }, internalChain: 'bfmeta', internalAsset: 'BFM', internalAccount: { address: 'bfmeta123', chain: 'bfmeta', publicKey: '0x' }, @@ -212,4 +212,72 @@ describe('useForge', () => { expect(submitCall.fromTrJson).toHaveProperty('bsc') expect(submitCall.fromTrJson.bsc?.signTransData).toBe('0xsignedBscTx') }) + + it('should build correct fromTrJson for TRON with base58 address', async () => { + // TRON depositAddress should be base58 format (starts with T) + const tronDepositAddress = 'TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g' + + mockBio.request + .mockResolvedValueOnce({ txHash: 'unsigned' }) + .mockResolvedValueOnce({ data: { txID: 'tronTxId123', signature: ['sig'] } }) + .mockResolvedValueOnce({ signature: 'sig', publicKey: 'pubkey' }) + + vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order' }) + + const { result } = renderHook(() => useForge()) + + const tronParams: ForgeParams = { + ...mockForgeParams, + externalChain: 'TRON', + externalAsset: 'TRX', + depositAddress: tronDepositAddress, + externalAccount: { address: 'TUserAddress123456789012345678901234', chain: 'tron', publicKey: '' }, + } + + act(() => { + result.current.forge(tronParams) + }) + + await waitFor(() => { + expect(result.current.step).toBe('success') + }) + + // Verify bio_createTransaction was called with correct depositAddress + const createTxCall = mockBio.request.mock.calls[0] + expect(createTxCall[0].method).toBe('bio_createTransaction') + expect(createTxCall[0].params[0].to).toBe(tronDepositAddress) + + const submitCall = vi.mocked(rechargeApi.submitRecharge).mock.calls[0][0] + expect(submitCall.fromTrJson).toHaveProperty('tron') + }) + + it('should handle TRON transaction with 0x address format error', async () => { + // This test verifies early validation catches invalid address format + const invalidTronAddress = '0x1234567890abcdef1234567890abcdef12345678' + + const { result } = renderHook(() => useForge()) + + const tronParams: ForgeParams = { + ...mockForgeParams, + externalChain: 'TRON', + externalAsset: 'TRX', + depositAddress: invalidTronAddress, + externalAccount: { address: 'TUserAddress123456789012345678901234', chain: 'tron', publicKey: '' }, + } + + act(() => { + result.current.forge(tronParams) + }) + + await waitFor(() => { + expect(result.current.step).toBe('error') + }) + + // Should get a helpful error about address format, not a cryptic base58 error + expect(result.current.error).toContain('Invalid TRON address') + expect(result.current.error).toContain('EVM format') + + // bio.request should NOT have been called since we catch the error early + expect(mockBio.request).not.toHaveBeenCalled() + }) }) diff --git a/miniapps/forge/src/hooks/useForge.ts b/miniapps/forge/src/hooks/useForge.ts index e6e06a988..da0c05a87 100644 --- a/miniapps/forge/src/hooks/useForge.ts +++ b/miniapps/forge/src/hooks/useForge.ts @@ -7,6 +7,7 @@ import type { BioAccount, BioSignedTransaction } from '@biochain/bio-sdk' import { normalizeChainId } from '@biochain/bio-sdk' import { rechargeApi } from '@/api' import { encodeRechargeV2ToTrInfoData, createRechargeMessage } from '@/api/helpers' +import { validateDepositAddress } from '@/lib/chain' import type { ExternalChainName, FromTrJson, @@ -89,6 +90,13 @@ export function useForge() { return } + // Validate deposit address format before proceeding + const addressError = validateDepositAddress(externalChain, depositAddress) + if (addressError) { + setState({ step: 'error', orderId: null, error: addressError }) + return + } + try { // Step 1: Create and sign external chain transaction setState({ step: 'signing_external', orderId: null, error: null }) diff --git a/miniapps/forge/src/lib/chain.test.ts b/miniapps/forge/src/lib/chain.test.ts new file mode 100644 index 000000000..1bded5532 --- /dev/null +++ b/miniapps/forge/src/lib/chain.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect } from 'vitest' +import { + getChainType, + isEvmChain, + isTronChain, + isValidTronBase58Address, + isTronHexAddress, + isEvmAddress, + validateDepositAddress, +} from './chain' + +describe('chain utilities', () => { + describe('getChainType', () => { + it('should return evm for ETH', () => { + expect(getChainType('ETH')).toBe('evm') + expect(getChainType('eth')).toBe('evm') + }) + + it('should return evm for BSC', () => { + expect(getChainType('BSC')).toBe('evm') + expect(getChainType('bsc')).toBe('evm') + }) + + it('should return tron for TRON', () => { + expect(getChainType('TRON')).toBe('tron') + expect(getChainType('tron')).toBe('tron') + }) + + it('should return bio for other chains', () => { + expect(getChainType('bfmeta')).toBe('bio') + expect(getChainType('bfchain')).toBe('bio') + }) + }) + + describe('isEvmChain', () => { + it('should return true for EVM chains', () => { + expect(isEvmChain('ETH')).toBe(true) + expect(isEvmChain('BSC')).toBe(true) + }) + + it('should return false for non-EVM chains', () => { + expect(isEvmChain('TRON')).toBe(false) + expect(isEvmChain('bfmeta')).toBe(false) + }) + }) + + describe('isTronChain', () => { + it('should return true for TRON', () => { + expect(isTronChain('TRON')).toBe(true) + expect(isTronChain('tron')).toBe(true) + }) + + it('should return false for non-TRON chains', () => { + expect(isTronChain('ETH')).toBe(false) + expect(isTronChain('BSC')).toBe(false) + }) + }) + + describe('isValidTronBase58Address', () => { + it('should validate correct TRON base58 addresses', () => { + // Real TRON address format: T + 33 base58 chars = 34 chars total + expect(isValidTronBase58Address('TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBe(true) + expect(isValidTronBase58Address('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t')).toBe(true) + }) + + it('should reject invalid addresses', () => { + // Too short + expect(isValidTronBase58Address('T123')).toBe(false) + // Wrong prefix + expect(isValidTronBase58Address('0x1234567890abcdef1234567890abcdef12345678')).toBe(false) + // Contains invalid base58 chars (0, O, I, l) + expect(isValidTronBase58Address('T0UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBe(false) + // Empty + expect(isValidTronBase58Address('')).toBe(false) + }) + }) + + describe('isTronHexAddress', () => { + it('should validate correct TRON hex addresses', () => { + expect(isTronHexAddress('41a614f803b6fd780986a42c78ec9c7f77e6ded13c')).toBe(true) + }) + + it('should reject invalid hex addresses', () => { + expect(isTronHexAddress('0x1234567890abcdef1234567890abcdef12345678')).toBe(false) + expect(isTronHexAddress('TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBe(false) + }) + }) + + describe('isEvmAddress', () => { + it('should validate correct EVM addresses', () => { + expect(isEvmAddress('0x1234567890abcdef1234567890abcdef12345678')).toBe(true) + expect(isEvmAddress('0xABCDEF1234567890ABCDEF1234567890ABCDEF12')).toBe(true) + }) + + it('should reject invalid EVM addresses', () => { + expect(isEvmAddress('1234567890abcdef1234567890abcdef12345678')).toBe(false) + expect(isEvmAddress('0x123')).toBe(false) + expect(isEvmAddress('TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBe(false) + }) + }) + + describe('validateDepositAddress', () => { + it('should validate EVM addresses for ETH/BSC', () => { + expect(validateDepositAddress('ETH', '0x1234567890abcdef1234567890abcdef12345678')).toBeNull() + expect(validateDepositAddress('BSC', '0x1234567890abcdef1234567890abcdef12345678')).toBeNull() + }) + + it('should reject invalid EVM addresses', () => { + const error = validateDepositAddress('ETH', 'invalid') + expect(error).toContain('Invalid EVM address format') + }) + + it('should validate TRON base58 addresses', () => { + expect(validateDepositAddress('TRON', 'TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBeNull() + }) + + it('should reject EVM format for TRON with helpful message', () => { + const error = validateDepositAddress('TRON', '0x1234567890abcdef1234567890abcdef12345678') + expect(error).toContain('Invalid TRON address') + expect(error).toContain('EVM format') + expect(error).toContain('expected base58') + }) + + it('should reject hex format for TRON with helpful message', () => { + const error = validateDepositAddress('TRON', '41a614f803b6fd780986a42c78ec9c7f77e6ded13c') + expect(error).toContain('Invalid TRON address') + expect(error).toContain('hex format') + expect(error).toContain('expected base58') + }) + }) +}) diff --git a/miniapps/forge/src/lib/chain.ts b/miniapps/forge/src/lib/chain.ts index 478d8de41..2a6833b51 100644 --- a/miniapps/forge/src/lib/chain.ts +++ b/miniapps/forge/src/lib/chain.ts @@ -43,3 +43,61 @@ export function isEvmChain(apiChainName: string): boolean { export function isTronChain(apiChainName: string): boolean { return getChainType(apiChainName) === 'tron' } + +/** Base58 alphabet for TRON addresses */ +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +/** + * Check if address is valid TRON base58 format (starts with T, 34 chars) + */ +export function isValidTronBase58Address(address: string): boolean { + if (!address || address.length !== 34 || !address.startsWith('T')) { + return false + } + for (const char of address) { + if (!BASE58_ALPHABET.includes(char)) { + return false + } + } + return true +} + +/** + * Check if address is TRON hex format (starts with 41, 42 chars) + */ +export function isTronHexAddress(address: string): boolean { + return /^41[a-fA-F0-9]{40}$/.test(address) +} + +/** + * Check if address is EVM format (starts with 0x, 42 chars) + */ +export function isEvmAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/i.test(address) +} + +/** + * Validate deposit address format for the given chain + * Returns error message if invalid, null if valid + */ +export function validateDepositAddress(chain: string, address: string): string | null { + const chainType = getChainType(chain) + + if (chainType === 'evm') { + if (!isEvmAddress(address)) { + return `Invalid EVM address format: expected 0x... (42 chars), got ${address.slice(0, 10)}...` + } + } else if (chainType === 'tron') { + if (!isValidTronBase58Address(address)) { + if (isEvmAddress(address)) { + return `Invalid TRON address: received EVM format (0x...), expected base58 format (T...)` + } + if (isTronHexAddress(address)) { + return `Invalid TRON address: received hex format (41...), expected base58 format (T...)` + } + return `Invalid TRON address format: expected T... (34 chars), got ${address.slice(0, 10)}...` + } + } + + return null +}