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
72 changes: 70 additions & 2 deletions miniapps/forge/src/hooks/useForge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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()
})
})
8 changes: 8 additions & 0 deletions miniapps/forge/src/hooks/useForge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 })
Expand Down
131 changes: 131 additions & 0 deletions miniapps/forge/src/lib/chain.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
58 changes: 58 additions & 0 deletions miniapps/forge/src/lib/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}