diff --git a/miniapps/forge/src/api/recharge.ts b/miniapps/forge/src/api/recharge.ts index 539baf427..d10d81398 100644 --- a/miniapps/forge/src/api/recharge.ts +++ b/miniapps/forge/src/api/recharge.ts @@ -4,6 +4,7 @@ import { apiClient } from './client' import { API_ENDPOINTS } from './config' +import { tronHexToBase58, isTronHexAddress } from '@/lib/tron-address' import type { RechargeSupportResDto, RechargeV2ReqDto, @@ -15,12 +16,55 @@ import type { RechargeRecordDetailReqDto, RechargeRecordDetailResDto, RetryOnChainReqDto, + ExternalAssetInfoItem, } from './types' +/** + * Convert TRON hex addresses in ExternalAssetInfoItem to Base58 format + */ +async function convertTronAddresses(item: ExternalAssetInfoItem): Promise { + const result = { ...item } + + // Convert depositAddress if it's TRON hex format + if (result.depositAddress && isTronHexAddress(result.depositAddress)) { + result.depositAddress = await tronHexToBase58(result.depositAddress) + } + + // Convert contract address if it's TRON hex format + if (result.contract && isTronHexAddress(result.contract)) { + result.contract = await tronHexToBase58(result.contract) + } + + return result +} + +/** + * Transform API response to convert all TRON hex addresses to Base58 + */ +async function transformSupportResponse(response: RechargeSupportResDto): Promise { + const recharge = { ...response.recharge } + + for (const chainName of Object.keys(recharge)) { + const assets = recharge[chainName] + for (const assetType of Object.keys(assets)) { + const item = assets[assetType] + if (item.supportChain?.TRON) { + item.supportChain = { + ...item.supportChain, + TRON: await convertTronAddresses(item.supportChain.TRON), + } + } + } + } + + return { recharge } +} + export const rechargeApi = { - /** 获取支持的充值配置 */ - getSupport(): Promise { - return apiClient.get(API_ENDPOINTS.RECHARGE_SUPPORT) + /** 获取支持的充值配置 (TRON addresses converted to Base58) */ + async getSupport(): Promise { + const response = await apiClient.get(API_ENDPOINTS.RECHARGE_SUPPORT) + return transformSupportResponse(response) }, /** 发起充值(锻造) */ diff --git a/miniapps/forge/src/lib/tron-address.test.ts b/miniapps/forge/src/lib/tron-address.test.ts new file mode 100644 index 000000000..c894803a1 --- /dev/null +++ b/miniapps/forge/src/lib/tron-address.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' +import { + tronHexToBase58, + isTronHexAddress, + isValidTronBase58Address +} from './tron-address' + +describe('TRON Address Conversion', () => { + // Known test vectors (verified with backend) + const testVectors = [ + { + hex: '412c9d3ed50dd097bc491d4164e39fe14d5288b554', + base58: 'TE3776zaaZvY5J5EEdvkYTGQePDYdJs84N', + description: 'depositAddress from API', + }, + { + hex: '41a614f803b6fd780986a42c78ec9c7f77e6ded13c', + base58: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + description: 'USDT TRC20 contract', + }, + ] + + describe('isTronHexAddress', () => { + it('should return true for valid TRON hex addresses', () => { + expect(isTronHexAddress('412c9d3ed50dd097bc491d4164e39fe14d5288b554')).toBe(true) + expect(isTronHexAddress('41a614f803b6fd780986a42c78ec9c7f77e6ded13c')).toBe(true) + }) + + it('should return false for invalid formats', () => { + expect(isTronHexAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f5bC12')).toBe(false) + expect(isTronHexAddress('TE3776zaaZvY5J5EEdvkYTGQePDYdJs84N')).toBe(false) + expect(isTronHexAddress('')).toBe(false) + expect(isTronHexAddress('41')).toBe(false) + }) + }) + + describe('isValidTronBase58Address', () => { + it('should return true for valid TRON base58 addresses', () => { + expect(isValidTronBase58Address('TE3776zaaZvY5J5EEdvkYTGQePDYdJs84N')).toBe(true) + expect(isValidTronBase58Address('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t')).toBe(true) + }) + + it('should return false for invalid formats', () => { + expect(isValidTronBase58Address('0x742d35Cc6634C0532925a3b844Bc9e7595f5bC12')).toBe(false) + expect(isValidTronBase58Address('412c9d3ed50dd097bc491d4164e39fe14d5288b554')).toBe(false) + expect(isValidTronBase58Address('')).toBe(false) + expect(isValidTronBase58Address('T')).toBe(false) + }) + }) + + describe('tronHexToBase58', () => { + for (const { hex, base58, description } of testVectors) { + it(`should convert ${description}`, async () => { + const result = await tronHexToBase58(hex) + expect(result).toBe(base58) + }) + } + + it('should throw for invalid hex address', async () => { + await expect(tronHexToBase58('invalid')).rejects.toThrow('Invalid TRON hex address') + await expect(tronHexToBase58('0x742d35Cc6634C0532925a3b844Bc9e7595f5bC12')).rejects.toThrow() + }) + }) +}) diff --git a/miniapps/forge/src/lib/tron-address.ts b/miniapps/forge/src/lib/tron-address.ts new file mode 100644 index 000000000..049bb7856 --- /dev/null +++ b/miniapps/forge/src/lib/tron-address.ts @@ -0,0 +1,102 @@ +/** + * TRON Address Conversion Utilities + * + * Converts between TRON Hex format (41...) and Base58 format (T...) + */ + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +/** + * Base58 encode a buffer + */ +function encodeBase58(buffer: Uint8Array): string { + const digits: number[] = [0] + + for (const byte of buffer) { + let carry = byte + for (let i = 0; i < digits.length; i++) { + carry += digits[i] << 8 + digits[i] = carry % 58 + carry = (carry / 58) | 0 + } + while (carry > 0) { + digits.push(carry % 58) + carry = (carry / 58) | 0 + } + } + + // Handle leading zeros + let leadingZeros = '' + for (const byte of buffer) { + if (byte === 0) { + leadingZeros += BASE58_ALPHABET[0] + } else { + break + } + } + + return leadingZeros + digits.reverse().map(d => BASE58_ALPHABET[d]).join('') +} + +/** + * SHA256 hash using Web Crypto API + */ +async function sha256(data: Uint8Array): Promise { + const hashBuffer = await crypto.subtle.digest('SHA-256', data.buffer as ArrayBuffer) + return new Uint8Array(hashBuffer) +} + +/** + * Check if address is TRON Hex format (41 + 40 hex chars) + */ +export function isTronHexAddress(address: string): boolean { + return /^41[a-fA-F0-9]{40}$/.test(address) +} + +/** + * 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 +} + +/** + * Convert TRON Hex address to Base58 format + * + * @param hexAddress - TRON hex address (41 + 40 hex chars) + * @returns Base58 encoded address (starts with T) + * @throws Error if invalid hex format + */ +export async function tronHexToBase58(hexAddress: string): Promise { + let hex = hexAddress.toLowerCase() + if (hex.startsWith('0x')) { + hex = hex.slice(2) + } + + if (!isTronHexAddress(hex)) { + throw new Error(`Invalid TRON hex address: ${hexAddress}`) + } + + // Convert hex to bytes + const addressBytes = new Uint8Array(hex.match(/.{2}/g)!.map(byte => parseInt(byte, 16))) + + // Calculate checksum: SHA256(SHA256(address)) first 4 bytes + const hash1 = await sha256(addressBytes) + const hash2 = await sha256(hash1) + const checksum = hash2.slice(0, 4) + + // Concatenate address + checksum + const payload = new Uint8Array(addressBytes.length + checksum.length) + payload.set(addressBytes) + payload.set(checksum, addressBytes.length) + + return encodeBase58(payload) +}