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
1 change: 1 addition & 0 deletions miniapps/forge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@base-ui/react": "^1.0.0",
"@biochain/bio-sdk": "workspace:*",
"@biochain/chain-effect": "workspace:*",
"@biochain/key-ui": "workspace:*",
"@biochain/key-utils": "workspace:*",
"@biochain/keyapp-sdk": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions miniapps/forge/src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const mockConfig = {
bfmeta: {
BFM: {
enable: true,
chainName: 'bfmeta',
assetType: 'BFM',
logo: '',
applyAddress: 'b0000000000000000000000000000000000000000',
supportChain: {
Expand Down
30 changes: 30 additions & 0 deletions miniapps/forge/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ export default function App() {
await forgeHook.forge({
externalChain: selectedOption.externalChain,
externalAsset: selectedOption.externalAsset,
externalDecimals: selectedOption.externalInfo.decimals,
depositAddress: selectedOption.externalInfo.depositAddress,
externalContract: selectedOption.externalInfo.contract,
amount,
externalAccount,
internalChain: selectedOption.internalChain,
Expand All @@ -255,6 +257,12 @@ export default function App() {
return groups;
}, [forgeOptions]);

const externalDecimals = selectedOption?.externalInfo.decimals;
const contractAddress = selectedOption?.externalInfo.contract?.trim();
const contractDisplay = contractAddress
? `${contractAddress.slice(0, 6)}...${contractAddress.slice(-4)}`
: null;

const handleSelectOption = (option: ForgeOption) => {
setSelectedOption(option);
setPickerOpen(false);
Expand Down Expand Up @@ -442,6 +450,17 @@ export default function App() {
<div className="text-muted-foreground font-mono text-xs break-all">
{externalAccount?.address}
</div>
<div className="text-muted-foreground space-y-1 text-xs">
<div>{t('forge.amountHint', { symbol: selectedOption.externalAsset })}</div>
{typeof externalDecimals === 'number' && (
<div>{t('forge.decimalsHint', { decimals: externalDecimals })}</div>
)}
{contractDisplay && (
<div title={contractAddress ?? undefined}>
{t('forge.contractHint', { address: contractDisplay })}
</div>
)}
</div>
</CardContent>
</Card>

Expand Down Expand Up @@ -569,6 +588,17 @@ export default function App() {
{selectedOption.externalInfo.depositAddress.slice(0, 10)}...
</span>
</div>
<div className="text-muted-foreground space-y-1 text-xs">
<div>{t('forge.amountHint', { symbol: selectedOption.externalAsset })}</div>
{typeof externalDecimals === 'number' && (
<div>{t('forge.decimalsHint', { decimals: externalDecimals })}</div>
)}
{contractDisplay && (
<div title={contractAddress ?? undefined}>
{t('forge.contractHint', { address: contractDisplay })}
</div>
)}
</div>
</CardContent>
</Card>

Expand Down
23 changes: 18 additions & 5 deletions miniapps/forge/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,33 @@ export class ApiError extends Error {

type WrappedResponse = {
success: boolean
result: unknown
result?: unknown
error?: unknown
message?: string
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}

function isWrappedResponse(value: unknown): value is WrappedResponse {
return (
typeof value === 'object' &&
value !== null &&
isRecord(value) &&
'success' in value &&
'result' in value &&
typeof (value as { success: unknown }).success === 'boolean'
)
}

function extractWrappedErrorMessage(data: WrappedResponse): string {
if (data.message) return data.message
if (isRecord(data.error) && 'message' in data.error) {
const message = (data.error as { message?: unknown }).message
if (Array.isArray(message)) return message.join('; ')
if (typeof message === 'string') return message
}
return 'Request failed'
}

interface RequestOptions extends RequestInit {
params?: Record<string, string | number | undefined>
}
Expand Down Expand Up @@ -75,7 +88,7 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
const data: unknown = await response.json()
if (isWrappedResponse(data)) {
if (data.success) return data.result as T
throw new ApiError(data.message || 'Request failed', response.status, data)
throw new ApiError(extractWrappedErrorMessage(data), response.status, data.error ?? data)
}

return data as T
Expand Down
28 changes: 14 additions & 14 deletions miniapps/forge/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* API Configuration
*
* Default base URL is https://walletapi.bfmeta.info (same as other BioForest services).
* Official COT base URL: https://walletapi.bf-meta.org/cot
* Can be overridden via VITE_COT_API_BASE_URL environment variable.
*/

/** Default API Base URL (used by all BioForest chain services) */
const DEFAULT_API_BASE_URL = 'https://walletapi.bfmeta.info'
/** Default API Base URL (official COT host) */
const DEFAULT_API_BASE_URL = 'https://walletapi.bf-meta.org'

/** API Base URL - uses default or environment override */
export const API_BASE_URL =
Expand All @@ -23,19 +23,19 @@ export const API_ENDPOINTS = {
// Recharge Endpoints (充值)
// ============================================================================
/** 获取支持的充值配置 */
RECHARGE_SUPPORT: '/cotbfm/recharge/support',
RECHARGE_SUPPORT: '/cot/recharge/support',
/** 发起充值(锻造) */
RECHARGE_V2: '/cotbfm/recharge/V2',
RECHARGE_V2: '/cot/recharge/V2',
/** 获取合约池信息 */
CONTRACT_POOL_INFO: '/cotbfm/recharge/contractPoolInfo',
CONTRACT_POOL_INFO: '/cot/recharge/contractPoolInfo',
/** 获取充值记录列表 */
RECHARGE_RECORDS: '/cotbfm/recharge/records',
RECHARGE_RECORDS: '/cot/recharge/records',
/** 获取充值记录详情 */
RECHARGE_RECORD_DETAIL: '/cotbfm/recharge/recordDetail',
RECHARGE_RECORD_DETAIL: '/cot/recharge/recordDetail',
/** 充值外链上链重试 */
RECHARGE_RETRY_EXTERNAL: '/cotbfm/recharge/retryExternalOnChain',
RECHARGE_RETRY_EXTERNAL: '/cot/recharge/retryExternalOnChain',
/** 充值内链上链重试 */
RECHARGE_RETRY_INTERNAL: '/cotbfm/recharge/retryInternalOnChain',
RECHARGE_RETRY_INTERNAL: '/cot/recharge/retryInternalOnChain',

// ============================================================================
// Redemption Endpoints (赎回)
Expand All @@ -55,11 +55,11 @@ export const API_ENDPOINTS = {
// Legacy aliases (向后兼容)
// ============================================================================
/** @deprecated Use RECHARGE_RECORDS */
RECORDS: '/cotbfm/recharge/records',
RECORDS: '/cot/recharge/records',
/** @deprecated Use RECHARGE_RECORD_DETAIL */
RECORD_DETAIL: '/cotbfm/recharge/recordDetail',
RECORD_DETAIL: '/cot/recharge/recordDetail',
/** @deprecated Use RECHARGE_RETRY_EXTERNAL */
RETRY_EXTERNAL: '/cotbfm/recharge/retryExternalOnChain',
RETRY_EXTERNAL: '/cot/recharge/retryExternalOnChain',
/** @deprecated Use RECHARGE_RETRY_INTERNAL */
RETRY_INTERNAL: '/cotbfm/recharge/retryInternalOnChain',
RETRY_INTERNAL: '/cot/recharge/retryInternalOnChain',
} as const
15 changes: 13 additions & 2 deletions miniapps/forge/src/api/recharge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ global.fetch = mockFetch
describe('Forge rechargeApi', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.stubEnv('VITE_COT_API_BASE_URL', 'https://walletapi.bfmeta.info')
vi.stubEnv('VITE_COT_API_BASE_URL', 'https://walletapi.bf-meta.org')
})

afterEach(() => {
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('Forge rechargeApi', () => {
const result = await rechargeApi.getSupport()
expect(result).toEqual(mockResult)
expect(mockFetch).toHaveBeenCalledWith(
'https://walletapi.bfmeta.info/cotbfm/recharge/support',
'https://walletapi.bf-meta.org/cot/recharge/support',
expect.any(Object),
)
})
Expand All @@ -53,4 +53,15 @@ describe('Forge rechargeApi', () => {
await expect(promise).rejects.toThrow(ApiError)
await expect(promise).rejects.toThrow('Not allowed')
})

it('should throw ApiError when success is false without result', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: false, message: 'Bad Request', error: { code: 400 } }),
})

const promise = rechargeApi.getSupport()
await expect(promise).rejects.toThrow(ApiError)
await expect(promise).rejects.toThrow('Bad Request')
})
})
43 changes: 34 additions & 9 deletions miniapps/forge/src/api/recharge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
* COT Recharge API
*/

import { apiClient } from './client'
import { apiClient, ApiError } from './client'
import { API_ENDPOINTS } from './config'
import { tronHexToBase58, isTronHexAddress } from '@/lib/tron-address'
import {
rechargeSupportSchema,
rechargeSubmitSchema,
rechargeRecordsSchema,
rechargeRecordDetailSchema,
} from './schemas'
import type {
RechargeSupportResDto,
RechargeV2ReqDto,
Expand Down Expand Up @@ -63,13 +69,22 @@ async function transformSupportResponse(response: RechargeSupportResDto): Promis
export const rechargeApi = {
/** 获取支持的充值配置 (TRON addresses converted to Base58) */
async getSupport(): Promise<RechargeSupportResDto> {
const response = await apiClient.get<RechargeSupportResDto>(API_ENDPOINTS.RECHARGE_SUPPORT)
return transformSupportResponse(response)
const raw = await apiClient.get<unknown>(API_ENDPOINTS.RECHARGE_SUPPORT)
const parsed = rechargeSupportSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid recharge support response', 0, parsed.error.flatten())
}
return transformSupportResponse(parsed.data)
},

/** 发起充值(锻造) */
submitRecharge(data: RechargeV2ReqDto): Promise<RechargeResDto> {
return apiClient.post(API_ENDPOINTS.RECHARGE_V2, data)
async submitRecharge(data: RechargeV2ReqDto): Promise<RechargeResDto> {
const raw = await apiClient.post<unknown>(API_ENDPOINTS.RECHARGE_V2, data)
const parsed = rechargeSubmitSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid recharge response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 获取合约池信息 */
Expand All @@ -78,19 +93,29 @@ export const rechargeApi = {
},

/** 获取充值记录列表 */
getRecords(params: RechargeRecordsReqDto): Promise<RechargeRecordsResDto> {
return apiClient.get(API_ENDPOINTS.RECORDS, {
async getRecords(params: RechargeRecordsReqDto): Promise<RechargeRecordsResDto> {
const raw = await apiClient.get<unknown>(API_ENDPOINTS.RECHARGE_RECORDS, {
page: params.page,
pageSize: params.pageSize,
internalChain: params.internalChain,
internalAddress: params.internalAddress,
recordState: params.recordState,
})
const parsed = rechargeRecordsSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid recharge records response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 获取充值记录详情 */
getRecordDetail(params: RechargeRecordDetailReqDto): Promise<RechargeRecordDetailResDto> {
return apiClient.get(API_ENDPOINTS.RECORD_DETAIL, { orderId: params.orderId })
async getRecordDetail(params: RechargeRecordDetailReqDto): Promise<RechargeRecordDetailResDto> {
const raw = await apiClient.get<unknown>(API_ENDPOINTS.RECHARGE_RECORD_DETAIL, { orderId: params.orderId })
const parsed = rechargeRecordDetailSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid recharge record detail response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 外链上链重试 */
Expand Down
34 changes: 27 additions & 7 deletions miniapps/forge/src/api/redemption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
* 赎回接口:内链资产 → 外链资产
*/

import { apiClient } from './client'
import { apiClient, ApiError } from './client'
import { API_ENDPOINTS } from './config'
import {
redemptionSubmitSchema,
redemptionRecordsSchema,
redemptionRecordDetailSchema,
} from './schemas'
import type {
RedemptionV2ReqDto,
RedemptionV2ResDto,
Expand All @@ -17,23 +22,38 @@ import type {

export const redemptionApi = {
/** 发起赎回 */
submitRedemption(data: RedemptionV2ReqDto): Promise<RedemptionV2ResDto> {
return apiClient.post(API_ENDPOINTS.REDEMPTION_V2, data)
async submitRedemption(data: RedemptionV2ReqDto): Promise<RedemptionV2ResDto> {
const raw = await apiClient.post<unknown>(API_ENDPOINTS.REDEMPTION_V2, data)
const parsed = redemptionSubmitSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid redemption response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 获取赎回记录列表 */
getRecords(params: RedemptionRecordsReqDto): Promise<RedemptionRecordsResDto> {
return apiClient.get(API_ENDPOINTS.REDEMPTION_RECORDS, {
async getRecords(params: RedemptionRecordsReqDto): Promise<RedemptionRecordsResDto> {
const raw = await apiClient.get<unknown>(API_ENDPOINTS.REDEMPTION_RECORDS, {
page: params.page,
pageSize: params.pageSize,
internalChain: params.internalChain,
internalAddress: params.internalAddress,
})
const parsed = redemptionRecordsSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid redemption records response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 获取赎回记录详情 */
getRecordDetail(params: RedemptionRecordDetailReqDto): Promise<RedemptionRecordDetailResDto> {
return apiClient.get(API_ENDPOINTS.REDEMPTION_RECORD_DETAIL, { orderId: params.orderId })
async getRecordDetail(params: RedemptionRecordDetailReqDto): Promise<RedemptionRecordDetailResDto> {
const raw = await apiClient.get<unknown>(API_ENDPOINTS.REDEMPTION_RECORD_DETAIL, { orderId: params.orderId })
const parsed = redemptionRecordDetailSchema.safeParse(raw)
if (!parsed.success) {
throw new ApiError('Invalid redemption record detail response', 0, parsed.error.flatten())
}
return parsed.data
},

/** 内链上链重试 */
Expand Down
Loading