feat(finance): edit app USDT receive addresses

This commit is contained in:
zhx 2026-07-15 13:09:53 +08:00
parent ee900d0127
commit 9045de6ad2
13 changed files with 474 additions and 41 deletions

View File

@ -4593,6 +4593,40 @@
"x-permissions": ["finance-order:coin-seller-recharge:view"]
}
},
"/admin/finance/usdt-addresses/{app_code}/{chain}": {
"put": {
"operationId": "upsertFinanceUSDTAddress",
"parameters": [
{
"in": "path",
"name": "app_code",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "chain",
"required": true,
"schema": {
"type": "string",
"enum": ["TRON", "BSC"]
}
}
],
"requestBody": {
"$ref": "#/components/requestBodies/FinanceUSDTAddressRequest"
},
"responses": {
"200": {
"$ref": "#/components/responses/FinanceUSDTAddressResponse"
}
},
"x-permission": "finance-order:usdt-address:update",
"x-permissions": ["finance-order:usdt-address:update"]
}
},
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
"get": {
"operationId": "getFinanceCoinSellerRechargeExchangeRate",
@ -8229,6 +8263,15 @@
}
}
},
"FinanceUSDTAddressRequest": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FinanceUSDTAddressInput"
}
}
}
},
"FinanceWithdrawalAuditRequest": {
"content": {
"application/json": {
@ -8777,6 +8820,16 @@
}
}
},
"FinanceUSDTAddressResponse": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponseFinanceUSDTAddressUpsert"
}
}
}
},
"FinanceCoinSellerRechargeReceiptVerificationResponse": {
"description": "OK",
"content": {
@ -11019,6 +11072,21 @@
}
]
},
"ApiResponseFinanceUSDTAddressUpsert": {
"allOf": [
{
"$ref": "#/components/schemas/Envelope"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/FinanceUSDTAddressUpsert"
}
}
}
]
},
"ApiResponseFinanceCoinSellerRechargeReceiptVerification": {
"allOf": [
{
@ -13137,7 +13205,7 @@
},
"chain": {
"type": "string",
"enum": ["TRON", "BSC"]
"enum": ["C2C", "OFFCHAIN", "TRON", "BSC"]
},
"externalOrderNo": {
"type": "string"
@ -13335,7 +13403,7 @@
},
"chain": {
"type": "string",
"enum": ["TRON", "BSC"]
"enum": ["C2C", "OFFCHAIN", "TRON", "BSC"]
},
"remark": {
"type": "string",
@ -13479,6 +13547,35 @@
}
}
},
"FinanceUSDTAddressInput": {
"type": "object",
"required": ["address"],
"properties": {
"address": {
"type": "string"
}
}
},
"FinanceUSDTAddressUpsert": {
"type": "object",
"required": ["appCode", "chain", "address", "updatedAtMs"],
"properties": {
"appCode": {
"type": "string"
},
"chain": {
"type": "string",
"enum": ["TRON", "BSC"]
},
"address": {
"type": "string"
},
"updatedAtMs": {
"type": "integer",
"format": "int64"
}
}
},
"FinanceCoinSellerRechargeExchangeRateInput": {
"type": "object",
"required": ["tiers", "whitelist"],
@ -13550,7 +13647,7 @@
},
"chain": {
"type": "string",
"enum": ["TRON", "BSC"]
"enum": ["C2C", "OFFCHAIN", "TRON", "BSC"]
},
"receiveAddress": {
"type": "string"
@ -13582,7 +13679,7 @@
},
"chain": {
"type": "string",
"enum": ["TRON", "BSC"]
"enum": ["C2C", "OFFCHAIN", "TRON", "BSC"]
},
"usdAmount": {
"type": "number",

View File

@ -19,6 +19,7 @@ import {
replaceFinanceCoinSellerRechargeExchangeRate,
rejectFinanceWithdrawalApplication,
quoteFinanceCoinSellerRechargeOrder,
upsertFinanceUSDTAddress,
verifyFinanceCoinSellerRechargeReceipt,
verifyFinanceCoinSellerRechargeOrder,
} from "./api.js";
@ -517,6 +518,18 @@ export function FinanceApp() {
const loadUSDTAddresses = useCallback((appCode) => getFinanceUSDTAddresses(appCode), []);
const saveUSDTAddress = useCallback(
async (appCode, chain, payload) => {
if (!session?.canUpdateUSDTAddress) {
throw new Error("无修改 USDT 收款地址权限");
}
const saved = await upsertFinanceUSDTAddress(appCode, chain, cleanPayload(payload));
showToast(`${chain} 收款地址已保存`, "success");
return saved;
},
[session?.canUpdateUSDTAddress, showToast],
);
const saveCoinSellerRechargeExchangeRate = useCallback(
async (appCode, payload) => {
const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload));
@ -723,6 +736,7 @@ export function FinanceApp() {
canCreate={session.canCreateCoinSellerRechargeOrder}
canConfigureExchangeRate={session.canConfigureCoinSellerRechargeExchangeRate}
canGrant={session.canGrantCoinSellerRechargeOrder}
canUpdateUSDTAddress={session.canUpdateUSDTAddress}
canVerify={session.canVerifyCoinSellerRechargeOrder}
error={coinSellerRechargeOrdersState.error}
filters={coinSellerRechargeOrderFilters}
@ -736,6 +750,7 @@ export function FinanceApp() {
onReload={loadCoinSellerRechargeOrders}
onQuote={quoteCoinSellerRechargeOrder}
onSaveExchangeRate={saveCoinSellerRechargeExchangeRate}
onSaveUSDTAddress={saveUSDTAddress}
onVerify={verifyCoinSellerRechargeOrder}
onVerifyReceipt={verifyCoinSellerRechargeReceipt}
/>

View File

@ -34,6 +34,7 @@ vi.mock("./api.js", () => ({
rejectFinanceWithdrawalApplication: vi.fn(),
refreshFinanceGoogleRechargePaid: vi.fn(),
replaceFinanceCoinSellerRechargeExchangeRate: vi.fn(),
upsertFinanceUSDTAddress: vi.fn(),
quoteFinanceCoinSellerRechargeOrder: vi.fn(),
verifyFinanceCoinSellerRechargeOrder: vi.fn(),
verifyFinanceCoinSellerRechargeReceipt: vi.fn()
@ -45,6 +46,7 @@ const defaultSession = {
canConfigureCoinSellerRechargeExchangeRate: false,
canCreateCoinSellerRechargeOrder: false,
canGrantCoinSellerRechargeOrder: false,
canUpdateUSDTAddress: false,
canVerifyCoinSellerRechargeOrder: false,
canViewAppRechargeDetails: false,
canViewCoinSellerRechargeOrders: false,

View File

@ -16,6 +16,14 @@ import {
const appCodeHeader = "X-App-Code";
function normalizeUSDTAddress(item = {}) {
return {
address: stringValue(item?.address),
chain: stringValue(item?.chain).toUpperCase(),
updatedAtMs: Number(item?.updatedAtMs || 0)
};
}
export async function fetchFinanceSession() {
const session = await currentSession();
const permissions = Array.isArray(session.permissions) ? session.permissions : [];
@ -26,6 +34,7 @@ export async function fetchFinanceSession() {
const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder);
const canVerifyCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.verifyCoinSellerRechargeOrder);
const canGrantCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.grantCoinSellerRechargeOrder);
const canUpdateUSDTAddress = permissions.includes(FINANCE_PERMISSIONS.updateUSDTAddress);
const canConfigureCoinSellerRechargeExchangeRate = permissions.includes(FINANCE_PERMISSIONS.configureCoinSellerRechargeExchangeRate);
return {
@ -38,6 +47,7 @@ export async function fetchFinanceSession() {
canConfigureCoinSellerRechargeExchangeRate,
canCreateCoinSellerRechargeOrder,
canGrantCoinSellerRechargeOrder,
canUpdateUSDTAddress,
canVerifyCoinSellerRechargeOrder,
canViewAppRechargeDetails,
canViewCoinSellerRechargeOrders,
@ -113,15 +123,26 @@ export async function getFinanceUSDTAddresses(appCode) {
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)
}))
.map(normalizeUSDTAddress)
.filter((item) => item.address && item.chain)
};
}
export async function upsertFinanceUSDTAddress(appCode, chain, payload) {
const endpoint = API_ENDPOINTS.upsertFinanceUSDTAddress;
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.upsertFinanceUSDTAddress, {
app_code: appCode,
chain
}), {
body: { address: stringValue(payload?.address) },
method: endpoint.method
});
return {
...normalizeUSDTAddress(data),
appCode: stringValue(data?.appCode ?? appCode)
};
}
export async function replaceFinanceCoinSellerRechargeExchangeRate(appCode, payload) {
const endpoint = API_ENDPOINTS.replaceFinanceCoinSellerRechargeExchangeRate;
return apiRequest(apiEndpointPath(API_OPERATIONS.replaceFinanceCoinSellerRechargeExchangeRate, { app_code: appCode }), {

View File

@ -14,6 +14,7 @@ import {
rejectFinanceWithdrawalApplication,
quoteFinanceCoinSellerRechargeOrder,
replaceFinanceCoinSellerRechargeExchangeRate,
upsertFinanceUSDTAddress,
verifyFinanceCoinSellerRechargeReceipt,
verifyFinanceCoinSellerRechargeOrder
} from "./api.js";
@ -202,6 +203,36 @@ test("finance USDT address API uses the selected app path and normalizes SQL row
});
});
test("finance USDT address update uses the selected app and chain path", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(JSON.stringify({
code: 0,
data: {
address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J",
appCode: "aslan",
chain: "TRON",
updatedAtMs: 456
}
}))
)
);
const result = await upsertFinanceUSDTAddress("aslan", "TRON", { address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" });
const [url, init] = vi.mocked(fetch).mock.calls[0];
expect(String(url)).toContain("/api/v1/admin/finance/usdt-addresses/aslan/TRON");
expect(init?.method).toBe("PUT");
expect(JSON.parse(String(init?.body))).toEqual({ address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" });
expect(result).toEqual({
address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J",
appCode: "aslan",
chain: "TRON",
updatedAtMs: 456
});
});
test("finance recharge details query every app with app header and merge by recharge time", async () => {
vi.stubGlobal(
"fetch",

View File

@ -42,6 +42,7 @@ export function FinanceCoinSellerRechargeOrderList({
canCreate,
canConfigureExchangeRate,
canGrant,
canUpdateUSDTAddress,
canVerify,
error,
filters,
@ -54,6 +55,7 @@ export function FinanceCoinSellerRechargeOrderList({
onReload,
onQuote,
onSaveExchangeRate,
onSaveUSDTAddress,
onVerify,
onVerifyReceipt,
orders,
@ -508,10 +510,12 @@ export function FinanceCoinSellerRechargeOrderList({
/>
<FinanceUSDTAddressDialog
apps={apps}
canUpdate={canUpdateUSDTAddress}
initialAppCode={filters.appCode}
open={usdtAddressOpen}
onClose={() => setUSDTAddressOpen(false)}
onLoad={onLoadUSDTAddresses}
onSave={onSaveUSDTAddress}
/>
<FinanceCoinSellerGrantDialog
loading={Boolean(grantTarget && actionLoading === `grant:${grantTarget.id}`)}

View File

@ -133,6 +133,18 @@ test("makeup order keeps zero coins while creating a normal recharge record", as
expect(onQuote).not.toHaveBeenCalled();
});
test("USDT chain selector exposes Binance Off-chain as an explicit verification source", async () => {
render(<FinanceCoinSellerRechargeOrderList {...baseProps()} />);
fireEvent.click(screen.getByRole("button", { name: "创建币商充值" }));
fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" }));
fireEvent.click(screen.getByRole("option", { name: "USDT" }));
fireEvent.mouseDown(screen.getByRole("combobox", { name: "USDT 链" }));
expect(screen.getByRole("option", { name: "Binance 站内转账 / Off-chain" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Binance Pay / C2C" })).toBeInTheDocument();
});
test("coin seller recharge order actions follow verify and grant status", () => {
const onVerify = vi.fn();
const onGrant = vi.fn();
@ -260,6 +272,7 @@ function baseProps(overrides = {}) {
canCreate: true,
canConfigureExchangeRate: false,
canGrant: true,
canUpdateUSDTAddress: false,
canVerify: true,
error: "",
filters: {
@ -280,6 +293,7 @@ function baseProps(overrides = {}) {
onReload: vi.fn(),
onQuote: vi.fn(async () => ({ coinAmount: 800000, coinsPerUsd: 80000, rateSource: "tier" })),
onSaveExchangeRate: vi.fn(),
onSaveUSDTAddress: vi.fn(),
onVerify: vi.fn(),
onVerifyReceipt: vi.fn(),
orders: {

View File

@ -1,4 +1,6 @@
import AddOutlined from "@mui/icons-material/AddOutlined";
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import Alert from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
@ -8,18 +10,22 @@ 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 { useEffect, useMemo, useState } from "react";
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { formatTime } from "../format.js";
const EMPTY_DATA = { appCode: "", items: [] };
const ADDRESS_CHAINS = ["TRON", "BSC"];
export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, onLoad }) {
export function FinanceUSDTAddressDialog({ apps, canUpdate = false, initialAppCode, open, onClose, onLoad, onSave }) {
const [appCode, setAppCode] = useState("");
const [data, setData] = useState(EMPTY_DATA);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [copiedChain, setCopiedChain] = useState("");
const [editingChain, setEditingChain] = useState("");
const [addressDraft, setAddressDraft] = useState("");
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) {
@ -27,6 +33,8 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose,
}
setAppCode(initialAppCode || apps[0]?.appCode || "");
setCopiedChain("");
setEditingChain("");
setAddressDraft("");
setError("");
}, [apps, initialAppCode, open]);
@ -40,6 +48,8 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose,
setLoading(true);
setError("");
setCopiedChain("");
setEditingChain("");
setAddressDraft("");
onLoad(appCode)
.then((result) => {
if (active) {
@ -62,6 +72,13 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose,
};
}, [appCode, onLoad, open]);
const itemsByChain = useMemo(
() => new Map((data.items || []).map((item) => [item.chain, item])),
[data.items],
);
// TRON/BSC
const visibleChains = canUpdate ? ADDRESS_CHAINS : ADDRESS_CHAINS.filter((chain) => itemsByChain.has(chain));
const copyAddress = async (item) => {
try {
if (!navigator.clipboard?.writeText) {
@ -77,13 +94,53 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose,
}
};
const beginEdit = (chain) => {
setEditingChain(chain);
setAddressDraft(itemsByChain.get(chain)?.address || "");
setCopiedChain("");
setError("");
};
const saveAddress = async () => {
const address = addressDraft.trim();
if (!validUSDTAddress(editingChain, address)) {
setError(`${chainLabel(editingChain)} 收款地址格式不正确`);
return;
}
if (!onSave) {
setError("收款地址保存接口未配置");
return;
}
setSaving(true);
setError("");
try {
const saved = await onSave(appCode, editingChain, { address });
const savedItem = {
address: saved?.address || address,
chain: saved?.chain || editingChain,
updatedAtMs: Number(saved?.updatedAtMs || Date.now()),
};
// admin-server SQL
setData((current) => ({
...current,
items: [...current.items.filter((item) => item.chain !== savedItem.chain), savedItem],
}));
setEditingChain("");
setAddressDraft("");
} catch (saveError) {
setError(saveError.message || "保存 USDT 收款地址失败");
} finally {
setSaving(false);
}
};
return (
<Dialog fullWidth maxWidth="sm" open={open} onClose={loading ? undefined : onClose}>
<Dialog fullWidth maxWidth="sm" open={open} onClose={loading || saving ? undefined : onClose}>
<DialogTitle>USDT 收款地址</DialogTitle>
<DialogContent className="finance-usdt-address-dialog">
{error ? <Alert severity="error">{error}</Alert> : null}
<TextField
disabled={loading || !apps.length}
disabled={loading || saving || !apps.length}
label="APP"
select
value={appCode}
@ -96,36 +153,82 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose,
))}
</TextField>
{loading ? <LinearProgress aria-label="加载 USDT 收款地址" /> : null}
{!loading && data.items.length ? (
{!loading && visibleChains.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>
))}
{visibleChains.map((chain) => {
const item = itemsByChain.get(chain);
const editing = editingChain === chain;
return (
<section className="finance-usdt-address-item" key={chain}>
<div className="finance-usdt-address-item__meta">
<strong>{chainLabel(chain)}</strong>
<small>{item?.updatedAtMs ? `更新于 ${formatTime(item.updatedAtMs)}` : "未配置"}</small>
</div>
{editing ? (
<div className="finance-usdt-address-item__editor">
<TextField
autoFocus
fullWidth
label={`${chainLabel(chain)} 收款地址`}
size="small"
value={addressDraft}
onChange={(event) => setAddressDraft(event.target.value)}
/>
<div className="finance-usdt-address-item__actions">
<Button disabled={saving} size="small" variant="outlined" onClick={() => setEditingChain("")}>
取消
</Button>
<Button
aria-label={`保存 ${chain} 地址`}
disabled={saving}
size="small"
variant="contained"
onClick={saveAddress}
>
{saving ? "保存中" : "保存"}
</Button>
</div>
</div>
) : (
<div className="finance-usdt-address-item__value">
{item ? <code>{item.address}</code> : <span className="finance-usdt-address-item__missing">尚未添加</span>}
<div className="finance-usdt-address-item__actions">
{item ? (
<Button
aria-label={`复制 ${chain} 地址`}
size="small"
startIcon={<ContentCopyOutlined fontSize="small" />}
variant="outlined"
onClick={() => copyAddress(item)}
>
{copiedChain === chain ? "已复制" : "复制"}
</Button>
) : null}
{canUpdate ? (
<Button
aria-label={`${item ? "修改" : "添加"} ${chain} 地址`}
size="small"
startIcon={item ? <EditOutlined fontSize="small" /> : <AddOutlined fontSize="small" />}
variant="outlined"
onClick={() => beginEdit(chain)}
>
{item ? "修改" : "添加"}
</Button>
) : null}
</div>
</div>
)}
</section>
);
})}
</div>
) : null}
{!loading && appCode && !data.items.length && !error ? (
{!loading && appCode && !visibleChains.length && !error ? (
<div className="finance-usdt-address-empty">当前 APP 未配置 USDT 收款地址</div>
) : null}
</DialogContent>
<DialogActions>
<Button disabled={loading} variant="outlined" onClick={onClose}>
<Button disabled={loading || saving} variant="outlined" onClick={onClose}>
关闭
</Button>
</DialogActions>
@ -142,3 +245,13 @@ function chainLabel(chain) {
}
return chain;
}
function validUSDTAddress(chain, address) {
if (chain === "TRON") {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(address);
}
if (chain === "BSC") {
return /^0x[0-9a-fA-F]{40}$/.test(address);
}
return false;
}

View File

@ -41,3 +41,52 @@ test("loads the selected app USDT address and copies the exact SQL value", async
await waitFor(() => expect(writeText).toHaveBeenCalledWith("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"));
expect(screen.getByRole("button", { name: "复制 TRON 地址" })).toHaveTextContent("已复制");
});
test("modifies an existing TRON address and keeps the saved SQL value in the row", async () => {
const originalAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
const updatedAddress = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J";
const onSave = vi.fn(async (appCode, chain, payload) => ({
address: payload.address,
appCode,
chain,
updatedAtMs: 200,
}));
render(
<FinanceUSDTAddressDialog
apps={[{ appCode: "aslan", appName: "Aslan" }]}
canUpdate
initialAppCode="aslan"
open
onClose={vi.fn()}
onLoad={vi.fn(async () => ({ appCode: "aslan", items: [{ address: originalAddress, chain: "TRON" }] }))}
onSave={onSave}
/>,
);
await screen.findByText(originalAddress);
fireEvent.click(screen.getByRole("button", { name: "修改 TRON 地址" }));
fireEvent.change(screen.getByRole("textbox", { name: "TRON · TRC20 收款地址" }), { target: { value: updatedAddress } });
fireEvent.click(screen.getByRole("button", { name: "保存 TRON 地址" }));
await waitFor(() => expect(onSave).toHaveBeenCalledWith("aslan", "TRON", { address: updatedAddress }));
expect(await screen.findByText(updatedAddress)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "修改 TRON 地址" })).toBeInTheDocument();
});
test("shows an add action when the selected app has no TRON address", async () => {
render(
<FinanceUSDTAddressDialog
apps={[{ appCode: "aslan", appName: "Aslan" }]}
canUpdate
initialAppCode="aslan"
open
onClose={vi.fn()}
onLoad={vi.fn(async () => ({ appCode: "aslan", items: [] }))}
onSave={vi.fn()}
/>,
);
expect(await screen.findByRole("button", { name: "添加 TRON 地址" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "添加 BSC 地址" })).toBeInTheDocument();
});

View File

@ -3,6 +3,7 @@ export const FINANCE_PERMISSIONS = {
configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate",
createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create",
grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant",
updateUSDTAddress: "finance-order:usdt-address:update",
verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify",
viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view",
viewAppRechargeDetails: "payment-bill:view",
@ -22,7 +23,8 @@ export const COIN_SELLER_RECHARGE_PROVIDERS = [
];
export const COIN_SELLER_RECHARGE_CHAINS = [
["C2C", "C2C"],
["C2C", "Binance Pay / C2C"],
["OFFCHAIN", "Binance 站内转账 / Off-chain"],
["TRON", "TRON"],
["BSC", "BSC"],
];

View File

@ -1897,6 +1897,24 @@ a {
flex: 0 0 auto;
}
.finance-usdt-address-item__editor {
display: grid;
gap: var(--space-2);
}
.finance-usdt-address-item__actions {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
gap: var(--space-2);
}
.finance-usdt-address-item__missing {
color: var(--text-tertiary);
font-size: 13px;
}
.finance-usdt-address-empty {
color: var(--text-secondary);
padding: var(--space-5) 0;

View File

@ -390,6 +390,7 @@ export const API_OPERATIONS = {
updateWeeklyStarCycle: "updateWeeklyStarCycle",
uploadFile: "uploadFile",
uploadImage: "uploadImage",
upsertFinanceUSDTAddress: "upsertFinanceUSDTAddress",
upsertLuckyGiftConfig: "upsertLuckyGiftConfig",
upsertResourceShopItems: "upsertResourceShopItems",
upsertRule: "upsertRule",
@ -3023,6 +3024,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "upload:create",
permissions: ["upload:create"]
},
upsertFinanceUSDTAddress: {
method: "PUT",
operationId: API_OPERATIONS.upsertFinanceUSDTAddress,
path: "/v1/admin/finance/usdt-addresses/{app_code}/{chain}",
permission: "finance-order:usdt-address:update",
permissions: ["finance-order:usdt-address:update"]
},
upsertLuckyGiftConfig: {
method: "PUT",
operationId: API_OPERATIONS.upsertLuckyGiftConfig,

View File

@ -2604,6 +2604,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/finance/usdt-addresses/{app_code}/{chain}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: operations["upsertFinanceUSDTAddress"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": {
parameters: {
query?: never;
@ -5443,6 +5459,9 @@ export interface components {
ApiResponseFinanceUSDTAddresses: components["schemas"]["Envelope"] & {
data?: components["schemas"]["FinanceUSDTAddresses"];
};
ApiResponseFinanceUSDTAddressUpsert: components["schemas"]["Envelope"] & {
data?: components["schemas"]["FinanceUSDTAddressUpsert"];
};
ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & {
data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"];
};
@ -6113,7 +6132,7 @@ export interface components {
/** @enum {string} */
providerCode: "mifapay" | "v5pay" | "usdt";
/** @enum {string} */
chain?: "TRON" | "BSC";
chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC";
externalOrderNo: string;
providerCountryCode?: string;
providerCurrencyCode?: string;
@ -6183,7 +6202,7 @@ export interface components {
providerCurrencyCode?: string;
providerAmountMinor?: number;
/** @enum {string} */
chain?: "TRON" | "BSC";
chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC";
remark?: string;
};
FinanceCoinSellerRechargeQuoteInput: {
@ -6230,6 +6249,17 @@ export interface components {
appCode: string;
items: components["schemas"]["FinanceUSDTAddress"][];
};
FinanceUSDTAddressInput: {
address: string;
};
FinanceUSDTAddressUpsert: {
appCode: string;
/** @enum {string} */
chain: "TRON" | "BSC";
address: string;
/** Format: int64 */
updatedAtMs: number;
};
FinanceCoinSellerRechargeExchangeRateInput: {
tiers: {
minUsdAmount: number;
@ -6250,7 +6280,7 @@ export interface components {
currencyCode?: string;
providerAmountMinor?: number;
/** @enum {string} */
chain?: "TRON" | "BSC";
chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC";
receiveAddress?: string;
verifiedAtMs?: number;
failureReason?: string;
@ -6262,7 +6292,7 @@ export interface components {
providerCode: "mifapay" | "v5pay" | "usdt";
externalOrderNo: string;
/** @enum {string} */
chain?: "TRON" | "BSC";
chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC";
usdAmount?: number;
usdMinorAmount?: number;
/**
@ -7095,6 +7125,15 @@ export interface components {
};
};
/** @description OK */
FinanceUSDTAddressResponse: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiResponseFinanceUSDTAddressUpsert"];
};
};
/** @description OK */
FinanceCoinSellerRechargeReceiptVerificationResponse: {
headers: {
[name: string]: unknown;
@ -7514,6 +7553,11 @@ export interface components {
"application/json": components["schemas"]["FinanceCoinSellerRechargeReceiptVerificationInput"];
};
};
FinanceUSDTAddressRequest: {
content: {
"application/json": components["schemas"]["FinanceUSDTAddressInput"];
};
};
FinanceWithdrawalAuditRequest: {
content: {
"application/json": {
@ -10833,6 +10877,21 @@ export interface operations {
200: components["responses"]["FinanceUSDTAddressesResponse"];
};
};
upsertFinanceUSDTAddress: {
parameters: {
query?: never;
header?: never;
path: {
app_code: string;
chain: "TRON" | "BSC";
};
cookie?: never;
};
requestBody?: components["requestBodies"]["FinanceUSDTAddressRequest"];
responses: {
200: components["responses"]["FinanceUSDTAddressResponse"];
};
};
getFinanceCoinSellerRechargeExchangeRate: {
parameters: {
query?: never;