From 4160c2f26950fe0c10ac2ab60de33a74d8bed35e Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 10 Mar 2026 09:47:54 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20v18:=20=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA=20-=20TR?= =?UTF-8?q?C20/BSC/ETH=E6=8E=A5=E6=94=B6=E5=9C=B0=E5=9D=80=E4=BB=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AF=BB=E5=8F=96=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=8F=AA=E8=AF=BB=E6=98=BE=E7=A4=BA=EF=BC=8C=E4=BB=85?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF=E5=9C=A8=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=EF=BC=9BAdmin=E5=90=8E=E5=8F=B0=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=8E=A5=E6=94=B6=E5=9C=B0=E5=9D=80=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=EF=BC=88=E7=BA=A2=E8=89=B2=E8=AD=A6=E5=91=8A?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=89=EF=BC=9Bconst.ts=E4=BF=AE=E5=A4=8DM?= =?UTF-8?q?anus=20OAuth=E5=86=85=E8=81=94=EF=BC=8C=E5=BD=93VITE=5FOAUTH=5F?= =?UTF-8?q?PORTAL=5FURL=E6=8C=87=E5=90=91manus.im=E6=97=B6=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E5=AE=9A=E5=90=91=E5=88=B0/admin=EF=BC=9Bsch?= =?UTF-8?q?ema=E9=87=8D=E5=A4=8D=E5=AE=9A=E4=B9=89=E5=B7=B2=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=9BTypeScript=200=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 36 +- client/src/const.ts | 18 +- client/src/pages/Admin.tsx | 112 ++- client/src/pages/Home.tsx | 38 +- drizzle/0010_grey_johnny_blaze.sql | 10 + drizzle/meta/0010_snapshot.json | 1027 ++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 16 + server/routers.ts | 80 ++- todo.md | 33 + 10 files changed, 1356 insertions(+), 21 deletions(-) create mode 100644 drizzle/0010_grey_johnny_blaze.sql create mode 100644 drizzle/meta/0010_snapshot.json diff --git a/client/src/App.tsx b/client/src/App.tsx index cb11478..3db58eb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,16 +1,43 @@ import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import NotFound from "@/pages/NotFound"; -import { Route, Switch } from "wouter"; +import { Route, Switch, useLocation } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; import Home from "./pages/Home"; import Tutorial from "./pages/Tutorial"; import Admin from "./pages/Admin"; import Bridge from "./pages/Bridge"; +import { useEffect } from "react"; + +// 跨链桥专属域名列表 — 访问这些域名时直接显示 Bridge 页面 +const BRIDGE_HOSTNAMES = ["trc-ico.newassetchain.io"]; + +function BridgeRedirect() { + const [, setLocation] = useLocation(); + useEffect(() => { + setLocation("/bridge", { replace: true }); + }, [setLocation]); + return null; +} function Router() { - // make sure to consider if you need authentication for certain routes + // 检测当前 hostname,若为跨链桥专属域名则直接渲染 Bridge 页面 + const isBridgeDomain = BRIDGE_HOSTNAMES.includes(window.location.hostname); + + if (isBridgeDomain) { + // 整个站点在此域名下只渲染 Bridge,/bridge 路由和根路由均指向 Bridge + return ( + + + + + + + ); + } + + // 普通域名(pre-sale.newassetchain.io / ico.newassetchain.io)完整路由 return ( @@ -24,11 +51,6 @@ function Router() { ); } -// NOTE: About Theme -// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css -// to keep consistent foreground/background color across components -// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook - function App() { return ( diff --git a/client/src/const.ts b/client/src/const.ts index bee8daa..a1ede79 100644 --- a/client/src/const.ts +++ b/client/src/const.ts @@ -1,17 +1,27 @@ export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; // Generate login URL at runtime so redirect URI reflects the current origin. +// IMPORTANT: This presale site is deployed on NAC's own servers (newassetchain.io). +// We do NOT use Manus OAuth — admin login is handled by the local /admin page. export const getLoginUrl = () => { const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || ""; const appId = import.meta.env.VITE_APP_ID || ""; - const redirectUri = `${window.location.origin}/api/oauth/callback`; - const state = btoa(redirectUri); - if (!oauthPortalUrl || !appId) { - // OAuth not configured — redirect to admin login page + // If OAuth portal is not configured, or it points to Manus (not NAC's own OAuth), + // redirect to the local admin login page instead. + if ( + !oauthPortalUrl || + !appId || + oauthPortalUrl.includes("manus.im") || + oauthPortalUrl.includes("manus.space") || + oauthPortalUrl.includes("manus.computer") + ) { return "/admin"; } + const redirectUri = `${window.location.origin}/api/oauth/callback`; + const state = btoa(redirectUri); + const url = new URL(`${oauthPortalUrl}/app-auth`); url.searchParams.set("appId", appId); url.searchParams.set("redirectUri", redirectUri); diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 1101a17..13bffa7 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -119,7 +119,114 @@ function LoginForm({ onLogin }: { onLogin: (token: string) => void }) { ); } -// ─── Main D// ─── Settings Panel ─────────────────────────────────────────────── +// ─── Receiving Address Panel (安全加固:接收地址仅管理员可修改) ──────────────────────────── +function ReceivingAddressPanel({ token }: { token: string }) { + const { data: currentAddresses, refetch } = trpc.settings.getReceivingAddresses.useQuery(); + const [values, setValues] = useState>({}); + const [saving, setSaving] = useState(null); + const [saved, setSaved] = useState>(new Set()); + + useEffect(() => { + if (currentAddresses) { + setValues({ + trc20_receiving_address: currentAddresses.trc20_receiving_address || "", + bsc_receiving_address: currentAddresses.bsc_receiving_address || "", + eth_receiving_address: currentAddresses.eth_receiving_address || "", + }); + } + }, [currentAddresses]); + + const updateMutation = trpc.settings.updateReceivingAddress.useMutation({ + onSuccess: (_, vars) => { + setSaving(null); + setSaved(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; }); + refetch(); + setTimeout(() => setSaved(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2500); + }, + onError: (err) => { + setSaving(null); + alert(`保存失败: ${err.message}`); + }, + }); + + const handleSave = (key: "trc20_receiving_address" | "bsc_receiving_address" | "eth_receiving_address") => { + setSaving(key); + updateMutation.mutate({ token, key, value: values[key] || "" }); + }; + + const fields = [ + { + key: "trc20_receiving_address" as const, + label: "TRC20 USDT 接收地址", + hint: "TRON 网络上接收 USDT 的钉包地址(T 开头,34 位)", + placeholder: "TWc2ug...", + color: "#FF0013", + }, + { + key: "bsc_receiving_address" as const, + label: "BSC 预售合约地址", + hint: "BSC 网络上的预售合约地址(0x 开头,42 位)", + placeholder: "0x...", + color: "#F0B90B", + }, + { + key: "eth_receiving_address" as const, + label: "ETH 预售合约地址", + hint: "Ethereum 网络上的预售合约地址(0x 开头,42 位)", + placeholder: "0x...", + color: "#627EEA", + }, + ]; + + return ( +
+
+ ⚠️ +

+ 接收地址配置 — 仅管理员可修改 +

+
+

+ 此处配置的地址将在首页以只读方式展示给用户。修改前请确认地址正确,错误地址将导致用户资金损失。 +

+
+ {fields.map(({ key, label, hint, placeholder, color }) => ( +
+
+
+ {label} +
+

{hint}

+
+ setValues(prev => ({ ...prev, [key]: e.target.value }))} + placeholder={placeholder} + className="flex-1 px-3 py-2 rounded-lg text-sm font-mono" + style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)", color: "white", outline: "none" }} + /> + +
+
+ ))} +
+
+ ); +} + +// ─── Settings Panel ─────────────────────────────────────────────── function SettingsPanel({ token }: { token: string }) { const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token }); const [editValues, setEditValues] = useState>({}); @@ -361,6 +468,9 @@ function SettingsPanel({ token }: { token: string }) { })}
+ {/* ── Receiving Addresses 接收地址配置 —— 仅管理员可修改 —— */} + + {/* Telegram Notifications */}

Telegram Notifications

diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index fc606a9..193eeaf 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -133,8 +133,13 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u }, }); + // 从数据库加载接收地址(只读,不可编辑) + const { data: receivingAddresses } = trpc.settings.getReceivingAddresses.useQuery(); + const trc20ReceivingAddress = receivingAddresses?.trc20_receiving_address || ""; + const copyAddress = () => { - navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet); + if (!trc20ReceivingAddress) return; + navigator.clipboard.writeText(trc20ReceivingAddress); setCopied(true); toast.success(lang === "zh" ? "地址已复制到剪贴板!" : "Address copied to clipboard!"); setTimeout(() => setCopied(false), 2000); @@ -271,18 +276,37 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
-

{t("trc20_send_to")}

+
+

{t("trc20_send_to")}

+ + {lang === "zh" ? "只读" : "Read Only"} + +
+ {/* 接收地址只读显示——不可编辑,只能复制,防止页面夹持欺诈 */}
- {CONTRACTS.TRON.receivingWallet} + {trc20ReceivingAddress || ( + + {lang === "zh" ? "加载中...请稍候" : "Loading..."} + + )}
diff --git a/drizzle/0010_grey_johnny_blaze.sql b/drizzle/0010_grey_johnny_blaze.sql new file mode 100644 index 0000000..a42087d --- /dev/null +++ b/drizzle/0010_grey_johnny_blaze.sql @@ -0,0 +1,10 @@ +CREATE TABLE `site_settings` ( + `id` int AUTO_INCREMENT NOT NULL, + `key` varchar(64) NOT NULL, + `value` text NOT NULL, + `label` varchar(128), + `description` varchar(256), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `site_settings_id` PRIMARY KEY(`id`), + CONSTRAINT `site_settings_key_unique` UNIQUE(`key`) +); diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..772e07f --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1027 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "15a7b12c-291c-4dc1-8d2e-d6dcf712f2f0", + "prevId": "3cfb2ddb-f45d-428a-80da-78d8e6fed501", + "tables": { + "bridge_intents": { + "name": "bridge_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "senderAddress": { + "name": "senderAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedOrderId": { + "name": "matchedOrderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_intents_id": { + "name": "bridge_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bridge_orders": { + "name": "bridge_orders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "walletAddress": { + "name": "walletAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromToken": { + "name": "fromToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAmount": { + "name": "fromAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "toChainId": { + "name": "toChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 56 + }, + "toToken": { + "name": "toToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'XIC'" + }, + "toAmount": { + "name": "toAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "confirmedAt": { + "name": "confirmedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_orders_id": { + "name": "bridge_orders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "bridge_orders_txHash_unique": { + "name": "bridge_orders_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "fiat_orders": { + "name": "fiat_orders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "orderId": { + "name": "orderId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gatewayOrderId": { + "name": "gatewayOrderId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "enum('alipay','wechat','paypal')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payerEmail": { + "name": "payerEmail", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payerOpenId": { + "name": "payerOpenId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usdtEquivalent": { + "name": "usdtEquivalent", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "originalAmount": { + "name": "originalAmount", + "type": "decimal(20,4)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicAmount": { + "name": "xicAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','paid','distributed','refunded','failed','expired')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "qrCodeUrl": { + "name": "qrCodeUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paymentUrl": { + "name": "paymentUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "callbackPayload": { + "name": "callbackPayload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiredAt": { + "name": "expiredAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "fiat_orders_id": { + "name": "fiat_orders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "fiat_orders_orderId_unique": { + "name": "fiat_orders_orderId_unique", + "columns": [ + "orderId" + ] + } + }, + "checkConstraint": {} + }, + "listener_state": { + "name": "listener_state", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastBlock": { + "name": "lastBlock", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "listener_state_id": { + "name": "listener_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "presale_config": { + "name": "presale_config", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'text'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_config_id": { + "name": "presale_config_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "presale_config_key_unique": { + "name": "presale_config_key_unique", + "columns": [ + "key" + ] + } + }, + "checkConstraint": {} + }, + "presale_stats_cache": { + "name": "presale_stats_cache", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "chain": { + "name": "chain", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtRaised": { + "name": "usdtRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "tokensSold": { + "name": "tokensSold", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "weiRaised": { + "name": "weiRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "lastUpdated": { + "name": "lastUpdated", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_stats_cache_id": { + "name": "presale_stats_cache_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "site_settings": { + "name": "site_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "site_settings_id": { + "name": "site_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "site_settings_key_unique": { + "name": "site_settings_key_unique", + "columns": [ + "key" + ] + } + }, + "checkConstraint": {} + }, + "transaction_logs": { + "name": "transaction_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chainType": { + "name": "chainType", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAddress": { + "name": "fromAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "toAddress": { + "name": "toAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "orderNo": { + "name": "orderNo", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "transaction_logs_id": { + "name": "transaction_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "transaction_logs_txHash_unique": { + "name": "transaction_logs_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "trc20_intents": { + "name": "trc20_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "tronAddress": { + "name": "tronAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedPurchaseId": { + "name": "matchedPurchaseId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_intents_id": { + "name": "trc20_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trc20_purchases": { + "name": "trc20_purchases", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAddress": { + "name": "fromAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtAmount": { + "name": "usdtAmount", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicAmount": { + "name": "xicAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_purchases_id": { + "name": "trc20_purchases_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "trc20_purchases_txHash_unique": { + "name": "trc20_purchases_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index dd54d00..3af4d2b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1773146163886, "tag": "0009_charming_lady_deathstrike", "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1773149657785, + "tag": "0010_grey_johnny_blaze", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index be65ee4..573dde2 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -201,3 +201,19 @@ export const fiatOrders = mysqlTable("fiat_orders", { export type FiatOrder = typeof fiatOrders.$inferSelect; export type InsertFiatOrder = typeof fiatOrders.$inferInsert; + +// ─── Site Settings ──────────────────────────────────────────────────────────── +// Global configuration key-value store managed by admin. +// Keys: trc20_receiving_address, bsc_receiving_address, eth_receiving_address +// Frontend reads via publicProcedure (read-only); admin writes via adminProcedure. +export const siteSettings = mysqlTable("site_settings", { + id: int("id").autoincrement().primaryKey(), + key: varchar("key", { length: 64 }).notNull().unique(), + value: text("value").notNull(), + label: varchar("label", { length: 128 }), + description: varchar("description", { length: 256 }), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type SiteSetting = typeof siteSettings.$inferSelect; +export type InsertSiteSetting = typeof siteSettings.$inferInsert; diff --git a/server/routers.ts b/server/routers.ts index 0740b5a..1fbd38c 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -30,7 +30,30 @@ import { handlePaypalWebhook, generatePaypalOrderId, } from "./services/paypalService"; -import { fiatOrders } from "../drizzle/schema"; +import { fiatOrders, siteSettings } from "../drizzle/schema"; + +// Default receiving addresses (used when DB is empty — admin must update via admin panel) +const DEFAULT_RECEIVING_ADDRESSES: Record = { + trc20_receiving_address: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp", + bsc_receiving_address: "0x0000000000000000000000000000000000000000", + eth_receiving_address: "0x0000000000000000000000000000000000000000", +}; + +// Helper: get a site setting by key +async function getSiteSetting(key: string): Promise { + const db = await getDb(); + if (!db) return null; + const rows = await db.select().from(siteSettings).where(eq(siteSettings.key, key)).limit(1); + return rows[0]?.value ?? null; +} + +// Helper: upsert a site setting +async function upsertSiteSetting(key: string, value: string, label?: string, description?: string) { + const db = await getDb(); + if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Database unavailable" }); + await db.insert(siteSettings).values({ key, value, label: label ?? null, description: description ?? null }) + .onDuplicateKeyUpdate({ set: { value, updatedAt: new Date() } }); +} // Admin password from env (fallback for development) const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!"; @@ -795,7 +818,60 @@ export const appRouter = router({ await setConfig("telegramChatId", input.chatId); return { success: true }; }), + }), + + // ─── Settings (Receiving Addresses) ──────────────────────────────────────────────── + // Public: read receiving addresses (frontend read-only display) + // Admin: update receiving addresses (only via admin panel) + settings: router({ + // Public: get all receiving addresses (read-only for frontend) + getReceivingAddresses: publicProcedure.query(async () => { + const keys = ["trc20_receiving_address", "bsc_receiving_address", "eth_receiving_address"]; + const result: Record = {}; + for (const key of keys) { + const val = await getSiteSetting(key); + result[key] = val ?? DEFAULT_RECEIVING_ADDRESSES[key] ?? ""; + } + return result; + }), + + // Admin: update a receiving address (requires admin token) + updateReceivingAddress: publicProcedure + .input(z.object({ + token: z.string(), + key: z.enum(["trc20_receiving_address", "bsc_receiving_address", "eth_receiving_address"]), + value: z.string().min(10).max(128), + })) + .mutation(async ({ input }) => { + if (!input.token.startsWith("bmFjLWFkbWlu")) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" }); + } + const labels: Record = { + trc20_receiving_address: "TRC20 USDT 接收地址", + bsc_receiving_address: "BSC ERC20 USDT 接收地址", + eth_receiving_address: "ETH ERC20 USDT 接收地址", + }; + await upsertSiteSetting( + input.key, + input.value, + labels[input.key], + "仅管理员可修改,前端只读显示" + ); + console.log(`[Admin] Receiving address updated: ${input.key} = ${input.value}`); + return { success: true }; + }), + + // Admin: get all site settings + getAllSettings: publicProcedure + .input(z.object({ token: z.string() })) + .query(async ({ input }) => { + if (!input.token.startsWith("bmFjLWFkbWlu")) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" }); + } + const db = await getDb(); + if (!db) return []; + return await db.select().from(siteSettings); + }), }), }); - export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index b0e8d5c..318ee6b 100644 --- a/todo.md +++ b/todo.md @@ -248,3 +248,36 @@ - [x] 前端:支付状态轮询(每 5 秒查询订单状态) - [ ] 获取商户账号后填入密钥并进行真实支付测试 - [ ] 构建部署到 AI 服务器并同步 Git 库 + +## v18 跨链桥独立域名 trc-ico.newassetchain.io + +- [ ] 分析当前 Bridge 路由和 Nginx 配置 +- [ ] 前端:访问 trc-ico.newassetchain.io 时直接显示 Bridge 页面 +- [ ] 服务器:配置 Nginx trc-ico.newassetchain.io 反向代理 +- [ ] 构建部署并验证域名访问 +- [ ] 同步 Gitea + +## v18 三域名配置 + +- [ ] 前端:hostname 检测,trc-ico.newassetchain.io 自动跳转 /bridge +- [ ] Nginx:合并 pre-sale + ico 到同一 server block +- [ ] Nginx:trc-ico 独立 server block,代理到 /bridge +- [ ] 构建部署并验证三个域名 +- [ ] 同步 Gitea + +## v18 彻底清除 Manus 内联 + +- [ ] 扫描所有 manus.im / manus.computer / manus.space 来源 +- [ ] 清除 const.ts 中 OAuth manus.im 回退逻辑 +- [ ] 清除 _core 模块中所有 Manus API 硬编码 URL +- [ ] 构建验证 bundle 零 Manus 内联 +- [ ] 部署并验证三个域名 + +## Bug 修复 + +- [ ] TRC20 接收地址显示区域应为灰色只读,不可编辑(当前可能可以点击修改) + +- [ ] 数据库新增 site_settings 表存储 TRC20/BSC/ETH 接收地址 +- [ ] 后端新增 settings 路由读取/更新接收地址 +- [ ] 前端接收地址从 API 读取,灰色只读可复制不可编辑 +- [ ] Admin 后台新增接收地址配置入口