feat(finance): add USDT address lookup
This commit is contained in:
parent
2eb18502a2
commit
ee900d0127
@ -4571,6 +4571,28 @@
|
|||||||
"x-permissions": ["finance-order:coin-seller-recharge:grant"]
|
"x-permissions": ["finance-order:coin-seller-recharge:grant"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/finance/usdt-addresses/{app_code}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getFinanceUSDTAddresses",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "app_code",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/FinanceUSDTAddressesResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "finance-order:coin-seller-recharge:view",
|
||||||
|
"x-permissions": ["finance-order:coin-seller-recharge:view"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
|
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getFinanceCoinSellerRechargeExchangeRate",
|
"operationId": "getFinanceCoinSellerRechargeExchangeRate",
|
||||||
@ -8745,6 +8767,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FinanceUSDTAddressesResponse": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResponseFinanceUSDTAddresses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"FinanceCoinSellerRechargeReceiptVerificationResponse": {
|
"FinanceCoinSellerRechargeReceiptVerificationResponse": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"content": {
|
"content": {
|
||||||
@ -10972,6 +11004,21 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ApiResponseFinanceUSDTAddresses": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Envelope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/FinanceUSDTAddresses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ApiResponseFinanceCoinSellerRechargeReceiptVerification": {
|
"ApiResponseFinanceCoinSellerRechargeReceiptVerification": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -13400,6 +13447,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FinanceUSDTAddress": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["chain", "address", "updatedAtMs"],
|
||||||
|
"properties": {
|
||||||
|
"chain": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["TRON", "BSC"]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAtMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FinanceUSDTAddresses": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["appCode", "items"],
|
||||||
|
"properties": {
|
||||||
|
"appCode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/FinanceUSDTAddress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"FinanceCoinSellerRechargeExchangeRateInput": {
|
"FinanceCoinSellerRechargeExchangeRateInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["tiers", "whitelist"],
|
"required": ["tiers", "whitelist"],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
fetchFinanceSession,
|
fetchFinanceSession,
|
||||||
grantFinanceCoinSellerRechargeOrder,
|
grantFinanceCoinSellerRechargeOrder,
|
||||||
getFinanceCoinSellerRechargeExchangeRate,
|
getFinanceCoinSellerRechargeExchangeRate,
|
||||||
|
getFinanceUSDTAddresses,
|
||||||
listFinanceCoinSellerRechargeOrders,
|
listFinanceCoinSellerRechargeOrders,
|
||||||
listFinanceRechargeDetails,
|
listFinanceRechargeDetails,
|
||||||
listFinanceRechargeRegions,
|
listFinanceRechargeRegions,
|
||||||
@ -514,6 +515,8 @@ export function FinanceApp() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadUSDTAddresses = useCallback((appCode) => getFinanceUSDTAddresses(appCode), []);
|
||||||
|
|
||||||
const saveCoinSellerRechargeExchangeRate = useCallback(
|
const saveCoinSellerRechargeExchangeRate = useCallback(
|
||||||
async (appCode, payload) => {
|
async (appCode, payload) => {
|
||||||
const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload));
|
const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload));
|
||||||
@ -727,6 +730,7 @@ export function FinanceApp() {
|
|||||||
orders={coinSellerRechargeOrdersState.data}
|
orders={coinSellerRechargeOrdersState.data}
|
||||||
onCreate={submitCoinSellerRechargeOrder}
|
onCreate={submitCoinSellerRechargeOrder}
|
||||||
onLoadExchangeRate={loadCoinSellerRechargeExchangeRate}
|
onLoadExchangeRate={loadCoinSellerRechargeExchangeRate}
|
||||||
|
onLoadUSDTAddresses={loadUSDTAddresses}
|
||||||
onFiltersChange={updateCoinSellerRechargeOrderFilters}
|
onFiltersChange={updateCoinSellerRechargeOrderFilters}
|
||||||
onGrant={grantCoinSellerRechargeOrder}
|
onGrant={grantCoinSellerRechargeOrder}
|
||||||
onReload={loadCoinSellerRechargeOrders}
|
onReload={loadCoinSellerRechargeOrders}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ vi.mock("./api.js", () => ({
|
|||||||
fetchFinanceSession: vi.fn(),
|
fetchFinanceSession: vi.fn(),
|
||||||
grantFinanceCoinSellerRechargeOrder: vi.fn(),
|
grantFinanceCoinSellerRechargeOrder: vi.fn(),
|
||||||
getFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
getFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
||||||
|
getFinanceUSDTAddresses: vi.fn(),
|
||||||
listFinanceCoinSellerRechargeOrders: vi.fn(),
|
listFinanceCoinSellerRechargeOrders: vi.fn(),
|
||||||
listFinanceRechargeDetails: vi.fn(),
|
listFinanceRechargeDetails: vi.fn(),
|
||||||
listFinanceRechargeRegions: vi.fn(),
|
listFinanceRechargeRegions: vi.fn(),
|
||||||
|
|||||||
@ -105,6 +105,23 @@ export async function getFinanceCoinSellerRechargeExchangeRate(appCode) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFinanceUSDTAddresses(appCode) {
|
||||||
|
const endpoint = API_ENDPOINTS.getFinanceUSDTAddresses;
|
||||||
|
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.getFinanceUSDTAddresses, { app_code: appCode }), {
|
||||||
|
method: endpoint.method
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
appCode: stringValue(data?.appCode ?? appCode),
|
||||||
|
items: listItems(data?.items)
|
||||||
|
.map((item) => ({
|
||||||
|
address: stringValue(item?.address),
|
||||||
|
chain: stringValue(item?.chain).toUpperCase(),
|
||||||
|
updatedAtMs: Number(item?.updatedAtMs || 0)
|
||||||
|
}))
|
||||||
|
.filter((item) => item.address && item.chain)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function replaceFinanceCoinSellerRechargeExchangeRate(appCode, payload) {
|
export async function replaceFinanceCoinSellerRechargeExchangeRate(appCode, payload) {
|
||||||
const endpoint = API_ENDPOINTS.replaceFinanceCoinSellerRechargeExchangeRate;
|
const endpoint = API_ENDPOINTS.replaceFinanceCoinSellerRechargeExchangeRate;
|
||||||
return apiRequest(apiEndpointPath(API_OPERATIONS.replaceFinanceCoinSellerRechargeExchangeRate, { app_code: appCode }), {
|
return apiRequest(apiEndpointPath(API_OPERATIONS.replaceFinanceCoinSellerRechargeExchangeRate, { app_code: appCode }), {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
fetchFinanceOptions,
|
fetchFinanceOptions,
|
||||||
fetchFinanceSession,
|
fetchFinanceSession,
|
||||||
getFinanceCoinSellerRechargeExchangeRate,
|
getFinanceCoinSellerRechargeExchangeRate,
|
||||||
|
getFinanceUSDTAddresses,
|
||||||
grantFinanceCoinSellerRechargeOrder,
|
grantFinanceCoinSellerRechargeOrder,
|
||||||
listFinanceCoinSellerRechargeOrders,
|
listFinanceCoinSellerRechargeOrders,
|
||||||
listFinanceRechargeDetails,
|
listFinanceRechargeDetails,
|
||||||
@ -174,6 +175,33 @@ test("finance coin seller quote and exchange rate APIs use generated endpoint pa
|
|||||||
expect(calls[2][1]?.method).toBe("PUT");
|
expect(calls[2][1]?.method).toBe("PUT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("finance USDT address API uses the selected app path and normalizes SQL rows", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
appCode: "aslan",
|
||||||
|
items: [{ address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", chain: "TRON", updatedAtMs: 123 }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await getFinanceUSDTAddresses("aslan");
|
||||||
|
|
||||||
|
const [url, init] = vi.mocked(fetch).mock.calls[0];
|
||||||
|
expect(String(url)).toContain("/api/v1/admin/finance/usdt-addresses/aslan");
|
||||||
|
expect(init?.method).toBe("GET");
|
||||||
|
expect(result).toEqual({
|
||||||
|
appCode: "aslan",
|
||||||
|
items: [{ address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", chain: "TRON", updatedAtMs: 123 }]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("finance recharge details query every app with app header and merge by recharge time", async () => {
|
test("finance recharge details query every app with app header and merge by recharge time", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||||
|
import AccountBalanceWalletOutlined from "@mui/icons-material/AccountBalanceWalletOutlined";
|
||||||
import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined";
|
import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined";
|
||||||
import PaidOutlined from "@mui/icons-material/PaidOutlined";
|
import PaidOutlined from "@mui/icons-material/PaidOutlined";
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||||
@ -33,6 +34,7 @@ import {
|
|||||||
import { FinanceCoinSellerRechargeCreateDialog } from "./FinanceCoinSellerRechargeCreateDialog.jsx";
|
import { FinanceCoinSellerRechargeCreateDialog } from "./FinanceCoinSellerRechargeCreateDialog.jsx";
|
||||||
import { FinanceCoinSellerExchangeRateDialog } from "./FinanceCoinSellerExchangeRateDialog.jsx";
|
import { FinanceCoinSellerExchangeRateDialog } from "./FinanceCoinSellerExchangeRateDialog.jsx";
|
||||||
import { FinanceCoinSellerGrantDialog } from "./FinanceCoinSellerGrantDialog.jsx";
|
import { FinanceCoinSellerGrantDialog } from "./FinanceCoinSellerGrantDialog.jsx";
|
||||||
|
import { FinanceUSDTAddressDialog } from "./FinanceUSDTAddressDialog.jsx";
|
||||||
|
|
||||||
export function FinanceCoinSellerRechargeOrderList({
|
export function FinanceCoinSellerRechargeOrderList({
|
||||||
actionLoading,
|
actionLoading,
|
||||||
@ -46,6 +48,7 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
loading,
|
loading,
|
||||||
onCreate,
|
onCreate,
|
||||||
onLoadExchangeRate,
|
onLoadExchangeRate,
|
||||||
|
onLoadUSDTAddresses,
|
||||||
onFiltersChange,
|
onFiltersChange,
|
||||||
onGrant,
|
onGrant,
|
||||||
onReload,
|
onReload,
|
||||||
@ -62,6 +65,7 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
const [quoteError, setQuoteError] = useState("");
|
const [quoteError, setQuoteError] = useState("");
|
||||||
const [quoteLoading, setQuoteLoading] = useState(false);
|
const [quoteLoading, setQuoteLoading] = useState(false);
|
||||||
const [rateConfigOpen, setRateConfigOpen] = useState(false);
|
const [rateConfigOpen, setRateConfigOpen] = useState(false);
|
||||||
|
const [usdtAddressOpen, setUSDTAddressOpen] = useState(false);
|
||||||
const [receiptVerification, setReceiptVerification] = useState(null);
|
const [receiptVerification, setReceiptVerification] = useState(null);
|
||||||
const [receiptLoading, setReceiptLoading] = useState(false);
|
const [receiptLoading, setReceiptLoading] = useState(false);
|
||||||
const [grantTarget, setGrantTarget] = useState(null);
|
const [grantTarget, setGrantTarget] = useState(null);
|
||||||
@ -281,6 +285,13 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<AccountBalanceWalletOutlined fontSize="small" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setUSDTAddressOpen(true)}
|
||||||
|
>
|
||||||
|
USDT 收款地址
|
||||||
|
</Button>
|
||||||
{canConfigureExchangeRate ? (
|
{canConfigureExchangeRate ? (
|
||||||
<Button startIcon={<SettingsOutlined fontSize="small" />} variant="outlined" onClick={() => setRateConfigOpen(true)}>
|
<Button startIcon={<SettingsOutlined fontSize="small" />} variant="outlined" onClick={() => setRateConfigOpen(true)}>
|
||||||
金币汇率配置
|
金币汇率配置
|
||||||
@ -495,6 +506,13 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
onLoad={onLoadExchangeRate}
|
onLoad={onLoadExchangeRate}
|
||||||
onSave={onSaveExchangeRate}
|
onSave={onSaveExchangeRate}
|
||||||
/>
|
/>
|
||||||
|
<FinanceUSDTAddressDialog
|
||||||
|
apps={apps}
|
||||||
|
initialAppCode={filters.appCode}
|
||||||
|
open={usdtAddressOpen}
|
||||||
|
onClose={() => setUSDTAddressOpen(false)}
|
||||||
|
onLoad={onLoadUSDTAddresses}
|
||||||
|
/>
|
||||||
<FinanceCoinSellerGrantDialog
|
<FinanceCoinSellerGrantDialog
|
||||||
loading={Boolean(grantTarget && actionLoading === `grant:${grantTarget.id}`)}
|
loading={Boolean(grantTarget && actionLoading === `grant:${grantTarget.id}`)}
|
||||||
order={grantTarget}
|
order={grantTarget}
|
||||||
|
|||||||
@ -274,6 +274,7 @@ function baseProps(overrides = {}) {
|
|||||||
loading: false,
|
loading: false,
|
||||||
onCreate: vi.fn(),
|
onCreate: vi.fn(),
|
||||||
onLoadExchangeRate: vi.fn(async () => ({ appCode: "lalu", tiers: [], whitelist: [] })),
|
onLoadExchangeRate: vi.fn(async () => ({ appCode: "lalu", tiers: [], whitelist: [] })),
|
||||||
|
onLoadUSDTAddresses: vi.fn(async () => ({ appCode: "lalu", items: [] })),
|
||||||
onFiltersChange: vi.fn(),
|
onFiltersChange: vi.fn(),
|
||||||
onGrant: vi.fn(),
|
onGrant: vi.fn(),
|
||||||
onReload: vi.fn(),
|
onReload: vi.fn(),
|
||||||
|
|||||||
144
finance/src/components/FinanceUSDTAddressDialog.jsx
Normal file
144
finance/src/components/FinanceUSDTAddressDialog.jsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||||
|
import { formatTime } from "../format.js";
|
||||||
|
|
||||||
|
const EMPTY_DATA = { appCode: "", items: [] };
|
||||||
|
|
||||||
|
export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, onLoad }) {
|
||||||
|
const [appCode, setAppCode] = useState("");
|
||||||
|
const [data, setData] = useState(EMPTY_DATA);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [copiedChain, setCopiedChain] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAppCode(initialAppCode || apps[0]?.appCode || "");
|
||||||
|
setCopiedChain("");
|
||||||
|
setError("");
|
||||||
|
}, [apps, initialAppCode, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !appCode) {
|
||||||
|
setData(EMPTY_DATA);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// App 切换会并发触发新请求;旧请求完成后必须被 active 门禁丢弃,不能把上一账套地址覆盖到当前选择。
|
||||||
|
let active = true;
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
setCopiedChain("");
|
||||||
|
onLoad(appCode)
|
||||||
|
.then((result) => {
|
||||||
|
if (active) {
|
||||||
|
setData({ appCode, items: result?.items || [] });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((loadError) => {
|
||||||
|
if (active) {
|
||||||
|
setData({ appCode, items: [] });
|
||||||
|
setError(loadError.message || "加载 USDT 收款地址失败");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (active) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [appCode, onLoad, open]);
|
||||||
|
|
||||||
|
const copyAddress = async (item) => {
|
||||||
|
try {
|
||||||
|
if (!navigator.clipboard?.writeText) {
|
||||||
|
throw new Error("clipboard unavailable");
|
||||||
|
}
|
||||||
|
// 复制值直接使用 API 返回的完整地址,不经过缩写或格式化,避免运营粘贴后链上校验失败。
|
||||||
|
await navigator.clipboard.writeText(item.address);
|
||||||
|
setCopiedChain(item.chain);
|
||||||
|
setError("");
|
||||||
|
} catch {
|
||||||
|
setCopiedChain("");
|
||||||
|
setError("复制失败,请手动选择地址复制");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog fullWidth maxWidth="sm" open={open} onClose={loading ? undefined : onClose}>
|
||||||
|
<DialogTitle>USDT 收款地址</DialogTitle>
|
||||||
|
<DialogContent className="finance-usdt-address-dialog">
|
||||||
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||||
|
<TextField
|
||||||
|
disabled={loading || !apps.length}
|
||||||
|
label="APP"
|
||||||
|
select
|
||||||
|
value={appCode}
|
||||||
|
onChange={(event) => setAppCode(event.target.value)}
|
||||||
|
>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<MenuItem key={app.appCode} value={app.appCode}>
|
||||||
|
<AppIdentity app={app} size="small" />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
{loading ? <LinearProgress aria-label="加载 USDT 收款地址" /> : null}
|
||||||
|
{!loading && data.items.length ? (
|
||||||
|
<div className="finance-usdt-address-list">
|
||||||
|
{data.items.map((item) => (
|
||||||
|
<section className="finance-usdt-address-item" key={item.chain}>
|
||||||
|
<div className="finance-usdt-address-item__meta">
|
||||||
|
<strong>{chainLabel(item.chain)}</strong>
|
||||||
|
<small>{item.updatedAtMs ? `更新于 ${formatTime(item.updatedAtMs)}` : ""}</small>
|
||||||
|
</div>
|
||||||
|
<div className="finance-usdt-address-item__value">
|
||||||
|
<code>{item.address}</code>
|
||||||
|
<Button
|
||||||
|
aria-label={`复制 ${item.chain} 地址`}
|
||||||
|
size="small"
|
||||||
|
startIcon={<ContentCopyOutlined fontSize="small" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => copyAddress(item)}
|
||||||
|
>
|
||||||
|
{copiedChain === item.chain ? "已复制" : "复制"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!loading && appCode && !data.items.length && !error ? (
|
||||||
|
<div className="finance-usdt-address-empty">当前 APP 未配置 USDT 收款地址</div>
|
||||||
|
) : null}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button disabled={loading} variant="outlined" onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chainLabel(chain) {
|
||||||
|
if (chain === "TRON") {
|
||||||
|
return "TRON · TRC20";
|
||||||
|
}
|
||||||
|
if (chain === "BSC") {
|
||||||
|
return "BSC · BEP20";
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
43
finance/src/components/FinanceUSDTAddressDialog.test.jsx
Normal file
43
finance/src/components/FinanceUSDTAddressDialog.test.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
import { FinanceUSDTAddressDialog } from "./FinanceUSDTAddressDialog.jsx";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loads the selected app USDT address and copies the exact SQL value", async () => {
|
||||||
|
const writeText = vi.fn(async () => undefined);
|
||||||
|
vi.stubGlobal("navigator", { clipboard: { writeText } });
|
||||||
|
const onLoad = vi.fn(async (appCode) => ({
|
||||||
|
appCode,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
||||||
|
chain: "TRON",
|
||||||
|
updatedAtMs: 1760000000000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<FinanceUSDTAddressDialog
|
||||||
|
apps={[
|
||||||
|
{ appCode: "lalu", appName: "Lalu" },
|
||||||
|
{ appCode: "aslan", appName: "Aslan" },
|
||||||
|
]}
|
||||||
|
initialAppCode="aslan"
|
||||||
|
open
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onLoad={onLoad}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText("TRON · TRC20")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")).toBeInTheDocument();
|
||||||
|
expect(onLoad).toHaveBeenCalledWith("aslan");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "复制 TRON 地址" }));
|
||||||
|
await waitFor(() => expect(writeText).toHaveBeenCalledWith("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"));
|
||||||
|
expect(screen.getByRole("button", { name: "复制 TRON 地址" })).toHaveTextContent("已复制");
|
||||||
|
});
|
||||||
@ -1850,6 +1850,59 @@ a {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-dialog {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding-top: var(--space-2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item__meta,
|
||||||
|
.finance-usdt-address-item__value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item__meta small {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item__value code {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-item__value .MuiButton-root {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-usdt-address-empty {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: var(--space-5) 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.finance-fchip {
|
.finance-fchip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -137,6 +137,7 @@ export const API_OPERATIONS = {
|
|||||||
getCumulativeRechargeRewardConfig: "getCumulativeRechargeRewardConfig",
|
getCumulativeRechargeRewardConfig: "getCumulativeRechargeRewardConfig",
|
||||||
getFinanceCoinSellerRechargeExchangeRate: "getFinanceCoinSellerRechargeExchangeRate",
|
getFinanceCoinSellerRechargeExchangeRate: "getFinanceCoinSellerRechargeExchangeRate",
|
||||||
getFinanceScope: "getFinanceScope",
|
getFinanceScope: "getFinanceScope",
|
||||||
|
getFinanceUSDTAddresses: "getFinanceUSDTAddresses",
|
||||||
getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig",
|
getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig",
|
||||||
getHumanRoomRobotConfig: "getHumanRoomRobotConfig",
|
getHumanRoomRobotConfig: "getHumanRoomRobotConfig",
|
||||||
getInviteActivityRewardConfig: "getInviteActivityRewardConfig",
|
getInviteActivityRewardConfig: "getInviteActivityRewardConfig",
|
||||||
@ -1267,6 +1268,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "finance:view",
|
permission: "finance:view",
|
||||||
permissions: ["finance:view"]
|
permissions: ["finance:view"]
|
||||||
},
|
},
|
||||||
|
getFinanceUSDTAddresses: {
|
||||||
|
method: "GET",
|
||||||
|
operationId: API_OPERATIONS.getFinanceUSDTAddresses,
|
||||||
|
path: "/v1/admin/finance/usdt-addresses/{app_code}",
|
||||||
|
permission: "finance-order:coin-seller-recharge:view",
|
||||||
|
permissions: ["finance-order:coin-seller-recharge:view"]
|
||||||
|
},
|
||||||
getFirstRechargeRewardConfig: {
|
getFirstRechargeRewardConfig: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getFirstRechargeRewardConfig,
|
operationId: API_OPERATIONS.getFirstRechargeRewardConfig,
|
||||||
|
|||||||
53
src/shared/api/generated/schema.d.ts
vendored
53
src/shared/api/generated/schema.d.ts
vendored
@ -2588,6 +2588,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/finance/usdt-addresses/{app_code}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getFinanceUSDTAddresses"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
|
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -5424,6 +5440,9 @@ export interface components {
|
|||||||
ApiResponseFinanceCoinSellerRechargeExchangeRate: components["schemas"]["Envelope"] & {
|
ApiResponseFinanceCoinSellerRechargeExchangeRate: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["FinanceCoinSellerRechargeExchangeRate"];
|
data?: components["schemas"]["FinanceCoinSellerRechargeExchangeRate"];
|
||||||
};
|
};
|
||||||
|
ApiResponseFinanceUSDTAddresses: components["schemas"]["Envelope"] & {
|
||||||
|
data?: components["schemas"]["FinanceUSDTAddresses"];
|
||||||
|
};
|
||||||
ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & {
|
ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"];
|
data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"];
|
||||||
};
|
};
|
||||||
@ -6200,6 +6219,17 @@ export interface components {
|
|||||||
whitelist: components["schemas"]["FinanceCoinSellerRechargeExchangeRateWhitelist"][];
|
whitelist: components["schemas"]["FinanceCoinSellerRechargeExchangeRateWhitelist"][];
|
||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
};
|
};
|
||||||
|
FinanceUSDTAddress: {
|
||||||
|
/** @enum {string} */
|
||||||
|
chain: "TRON" | "BSC";
|
||||||
|
address: string;
|
||||||
|
/** Format: int64 */
|
||||||
|
updatedAtMs: number;
|
||||||
|
};
|
||||||
|
FinanceUSDTAddresses: {
|
||||||
|
appCode: string;
|
||||||
|
items: components["schemas"]["FinanceUSDTAddress"][];
|
||||||
|
};
|
||||||
FinanceCoinSellerRechargeExchangeRateInput: {
|
FinanceCoinSellerRechargeExchangeRateInput: {
|
||||||
tiers: {
|
tiers: {
|
||||||
minUsdAmount: number;
|
minUsdAmount: number;
|
||||||
@ -7056,6 +7086,15 @@ export interface components {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description OK */
|
/** @description OK */
|
||||||
|
FinanceUSDTAddressesResponse: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["ApiResponseFinanceUSDTAddresses"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description OK */
|
||||||
FinanceCoinSellerRechargeReceiptVerificationResponse: {
|
FinanceCoinSellerRechargeReceiptVerificationResponse: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@ -10780,6 +10819,20 @@ export interface operations {
|
|||||||
200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"];
|
200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
getFinanceUSDTAddresses: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
app_code: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["FinanceUSDTAddressesResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
getFinanceCoinSellerRechargeExchangeRate: {
|
getFinanceCoinSellerRechargeExchangeRate: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user