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
50 changes: 47 additions & 3 deletions miniapps/forge/src/api/recharge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { apiClient } from './client'
import { API_ENDPOINTS } from './config'
import { tronHexToBase58, isTronHexAddress } from '@/lib/tron-address'
import type {
RechargeSupportResDto,
RechargeV2ReqDto,
Expand All @@ -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<ExternalAssetInfoItem> {
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<RechargeSupportResDto> {
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<RechargeSupportResDto> {
return apiClient.get(API_ENDPOINTS.RECHARGE_SUPPORT)
/** 获取支持的充值配置 (TRON addresses converted to Base58) */
async getSupport(): Promise<RechargeSupportResDto> {
const response = await apiClient.get<RechargeSupportResDto>(API_ENDPOINTS.RECHARGE_SUPPORT)
return transformSupportResponse(response)
},

/** 发起充值(锻造) */
Expand Down
64 changes: 64 additions & 0 deletions miniapps/forge/src/lib/tron-address.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
102 changes: 102 additions & 0 deletions miniapps/forge/src/lib/tron-address.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> {
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<string> {
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)
}