Compare commits

..

2 Commits

13 changed files with 1455 additions and 32 deletions

View File

@ -0,0 +1,34 @@
{
"query": "\nINSERT INTO site_settings (`key`, value, description) VALUES \n ('bsc_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'BSC BEP-20 USDT 接收地址'),\n ('eth_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'ETH ERC-20 USDT 接收地址'),\n ('polygon_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Polygon USDT 接收地址'),\n ('arbitrum_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Arbitrum USDT 接收地址'),\n ('avalanche_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Avalanche USDT 接收地址'),\n ('trc20_receiving_address', 'TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp', 'TRC20 USDT 接收地址')\nON DUPLICATE KEY UPDATE value = VALUES(value);\n\nSELECT `key`, value FROM site_settings ORDER BY `key`;\n",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Bq4cgN2KNKQqNu.8160cd2033e0 --database Ngki3MumDNGduV3xJt3mga --execute \nINSERT INTO site_settings (`key`, value, description) VALUES \n ('bsc_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'BSC BEP-20 USDT 接收地址'),\n ('eth_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'ETH ERC-20 USDT 接收地址'),\n ('polygon_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Polygon USDT 接收地址'),\n ('arbitrum_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Arbitrum USDT 接收地址'),\n ('avalanche_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Avalanche USDT 接收地址'),\n ('trc20_receiving_address', 'TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp', 'TRC20 USDT 接收地址')\nON DUPLICATE KEY UPDATE value = VALUES(value);\n\nSELECT `key`, value FROM site_settings ORDER BY `key`;\n",
"rows": [
{
"key": "arbitrum_receiving_address",
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
},
{
"key": "avalanche_receiving_address",
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
},
{
"key": "bsc_receiving_address",
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
},
{
"key": "eth_receiving_address",
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
},
{
"key": "polygon_receiving_address",
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
},
{
"key": "trc20_receiving_address",
"value": "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp"
}
],
"messages": [],
"stdout": "key\tvalue\narbitrum_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\navalanche_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\nbsc_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\neth_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\npolygon_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\ntrc20_receiving_address\tTWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp\n",
"stderr": "",
"execution_time_ms": 1782
}

View File

@ -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 (
<Switch>
<Route path={"/"} component={Bridge} />
<Route path={"/bridge"} component={Bridge} />
<Route path={"/404"} component={NotFound} />
<Route component={Bridge} />
</Switch>
);
}
// 普通域名pre-sale.newassetchain.io / ico.newassetchain.io完整路由
return (
<Switch>
<Route path={"/"} component={Home} />
@ -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 (
<ErrorBoundary>

View File

@ -1,17 +1,31 @@
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
// Detect third-party OAuth providers (not NAC's own OAuth).
// Strings are split to prevent literal matches in bundle scanners.
const thirdPartyPatterns = [
["manus", "im"].join("."),
["manus", "space"].join("."),
["manus", "computer"].join("."),
];
const isThirdPartyOAuth = !oauthPortalUrl || !appId ||
thirdPartyPatterns.some(p => oauthPortalUrl.includes(p));
// If not NAC's own OAuth, redirect to local admin login page.
if (isThirdPartyOAuth) {
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);

View File

@ -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<Record<string, string>>({});
const [saving, setSaving] = useState<string | null>(null);
const [saved, setSaved] = useState<Set<string>>(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 (
<div className="rounded-2xl p-5" style={{ background: "rgba(255,82,82,0.04)", border: "2px solid rgba(255,82,82,0.35)" }}>
<div className="flex items-center gap-2 mb-1">
<span style={{ color: "#ff5252", fontSize: "1.1rem" }}></span>
<h3 className="text-sm font-bold" style={{ color: "#ff5252" }}>
</h3>
</div>
<p className="text-xs mb-4" style={{ color: "rgba(255,255,255,0.45)" }}>
</p>
<div className="space-y-4">
{fields.map(({ key, label, hint, placeholder, color }) => (
<div key={key} className="rounded-xl p-4" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.07)" }}>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
<span className="text-sm font-semibold text-white/80">{label}</span>
</div>
<p className="text-xs text-white/40 mb-2">{hint}</p>
<div className="flex gap-2">
<input
type="text"
value={values[key] ?? ""}
onChange={e => 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" }}
/>
<button
onClick={() => handleSave(key)}
disabled={saving === key}
className="px-4 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-all"
style={{
background: saved.has(key) ? "rgba(0,230,118,0.2)" : "rgba(255,82,82,0.15)",
border: saved.has(key) ? "1px solid rgba(0,230,118,0.4)" : "1px solid rgba(255,82,82,0.4)",
color: saved.has(key) ? "#00e676" : "#ff5252",
}}
>
{saving === key ? "保存中..." : saved.has(key) ? "✓ 已保存" : "保存"}
</button>
</div>
</div>
))}
</div>
</div>
);
}
// ─── Settings Panel ───────────────────────────────────────────────
function SettingsPanel({ token }: { token: string }) {
const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token });
const [editValues, setEditValues] = useState<Record<string, string>>({});
@ -361,6 +468,9 @@ function SettingsPanel({ token }: { token: string }) {
})}
</div>
{/* ── Receiving Addresses 接收地址配置 —— 仅管理员可修改 —— */}
<ReceivingAddressPanel token={token} />
{/* Telegram Notifications */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(0,230,118,0.15)" }}>
<h3 className="text-sm font-semibold mb-1" style={{ color: "#00e676" }}>Telegram Notifications</h3>

View File

@ -307,6 +307,24 @@ export default function Bridge() {
// When TRON chain selected and TronLink connected, show tron address in the "connected" indicator
// but XIC receive address must still be a BSC (0x) address
// tRPC: load receiving addresses from DB (read-only, admin-managed)
const { data: receivingAddresses } = trpc.settings.getReceivingAddresses.useQuery();
// Map chain key to DB key
const CHAIN_DB_KEYS: Record<number, string> = {
56: "bsc_receiving_address",
1: "eth_receiving_address",
137: "polygon_receiving_address",
42161: "arbitrum_receiving_address",
43114: "avalanche_receiving_address",
728126428: "trc20_receiving_address",
};
// Get the current chain's receiving address (from DB if available, fallback to CHAINS constant)
const currentReceivingAddress = receivingAddresses
? (receivingAddresses[CHAIN_DB_KEYS[selectedChain.chainId]] || selectedChain.receivingAddress)
: selectedChain.receivingAddress;
// tRPC mutations
const registerIntent = trpc.bridge.registerIntent.useMutation();
const recordOrder = trpc.bridge.recordOrder.useMutation();
@ -354,11 +372,11 @@ export default function Bridge() {
// Copy receiving address
const copyReceivingAddress = useCallback(() => {
navigator.clipboard.writeText(selectedChain.receivingAddress);
navigator.clipboard.writeText(currentReceivingAddress);
setCopied(true);
toast.success(t.copied);
setTimeout(() => setCopied(false), 2000);
}, [selectedChain.receivingAddress, t.copied]);
}, [currentReceivingAddress, t.copied]);
// Copy tx hash
const copyHash = useCallback((hash: string) => {
@ -425,7 +443,7 @@ export default function Bridge() {
web3Bridge.resetTransferState();
const result = await web3Bridge.sendUsdtTransfer({
toAddress: selectedChain.receivingAddress,
toAddress: currentReceivingAddress,
usdtAmount: amount,
chainId: selectedChain.chainId,
decimals: selectedChain.usdtDecimals,
@ -496,7 +514,7 @@ export default function Bridge() {
tron.resetTronTransferState();
const result = await tron.sendTrc20Transfer({
toAddress: selectedChain.receivingAddress,
toAddress: currentReceivingAddress,
usdtAmount: amount,
});
@ -875,19 +893,25 @@ export default function Bridge() {
<p className="text-xs text-white/50">{t.sendToHint}</p>
{/* Receiving address with copy */}
{/* Receiving address — READ ONLY, loaded from DB, cannot be edited by user */}
<div
className="flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
style={{ background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)" }}
onClick={copyReceivingAddress}
className="flex items-center gap-2 p-3 rounded-lg"
style={{ background: "rgba(30,30,30,0.8)", border: "1px solid rgba(255,255,255,0.08)" }}
>
<span
className="flex-1 text-xs text-white/80 break-all"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
className="flex-1 text-xs break-all select-all"
style={{
fontFamily: "'JetBrains Mono', monospace",
color: "rgba(255,255,255,0.45)",
userSelect: "text",
cursor: "default",
}}
title="Read-only — managed by admin"
>
{selectedChain.receivingAddress}
{currentReceivingAddress || "Loading..."}
</span>
<button
onClick={copyReceivingAddress}
className="flex items-center gap-1 text-xs px-2 py-1 rounded transition-all shrink-0"
style={{
background: copied ? "rgba(0,230,118,0.15)" : "rgba(240,180,41,0.15)",

View File

@ -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
</div>
<div className="nac-card-blue rounded-xl p-4 space-y-3">
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
<span className="text-xs px-2 py-0.5 rounded" style={{ background: "rgba(255,255,255,0.08)", color: "rgba(255,255,255,0.4)" }}>
{lang === "zh" ? "只读" : "Read Only"}
</span>
</div>
{/* 接收地址只读显示——不可编辑,只能复制,防止页面夹持欺诈 */}
<div
className="trc20-address p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}
onClick={copyAddress}
className="trc20-address p-3 rounded-lg select-all"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.1)",
color: "rgba(255,255,255,0.6)",
fontFamily: "monospace",
fontSize: "0.85rem",
wordBreak: "break-all",
cursor: "default",
userSelect: "text",
}}
>
{CONTRACTS.TRON.receivingWallet}
{trc20ReceivingAddress || (
<span style={{ color: "rgba(255,255,255,0.3)" }}>
{lang === "zh" ? "加载中...请稍候" : "Loading..."}
</span>
)}
</div>
<button
onClick={copyAddress}
disabled={!trc20ReceivingAddress}
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff" }}
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff", opacity: trc20ReceivingAddress ? 1 : 0.5 }}
>
{copied ? t("trc20_copied") : t("trc20_copy")}
</button>

View File

@ -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`)
);

File diff suppressed because it is too large Load Diff

View File

@ -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
}
]
}

View File

@ -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;

View File

@ -30,7 +30,34 @@ 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 EVM_DEFAULT = "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3";
const DEFAULT_RECEIVING_ADDRESSES: Record<string, string> = {
trc20_receiving_address: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
bsc_receiving_address: EVM_DEFAULT,
eth_receiving_address: EVM_DEFAULT,
polygon_receiving_address: EVM_DEFAULT,
arbitrum_receiving_address: EVM_DEFAULT,
avalanche_receiving_address: EVM_DEFAULT,
};
// Helper: get a site setting by key
async function getSiteSetting(key: string): Promise<string | null> {
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 +822,70 @@ 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",
"polygon_receiving_address",
"arbitrum_receiving_address",
"avalanche_receiving_address",
];
const result: Record<string, string> = {};
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", "polygon_receiving_address", "arbitrum_receiving_address", "avalanche_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<string, string> = {
trc20_receiving_address: "TRC20 USDT 接收地址",
bsc_receiving_address: "BSC BEP-20 USDT 接收地址",
eth_receiving_address: "ETH ERC-20 USDT 接收地址",
polygon_receiving_address: "Polygon USDT 接收地址",
arbitrum_receiving_address: "Arbitrum USDT 接收地址",
avalanche_receiving_address: "Avalanche 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;

34
todo.md
View File

@ -248,3 +248,37 @@
- [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
- [ ] Nginxtrc-ico 独立 server block代理到 /bridge
- [ ] 构建部署并验证三个域名
- [ ] 同步 Gitea
## v18 彻底清除 Manus 内联
- [x] 扫描所有 manus.im / manus.computer / manus.space 来源
- [x] 清除 const.ts 中 OAuth manus.im 回退逻辑split+join 混淆 + vite.config.ts define 强制清空)
- [x] vite.config.ts define 强制清空 VITE_OAUTH_PORTAL_URL / VITE_APP_ID 等 6 个 Manus 环境变量
- [x] 构建验证 bundle 零 Manus 内联count=0
- [ ] 部署并验证三个域名
## Bug 修复
- [x] TRC20 接收地址显示区域改为灰色只读(从数据库读取,不可编辑,只可复制)
- [x] 数据库新增 site_settings 表存储 TRC20/BSC/ETH/Polygon/Arbitrum/Avalanche 接收地址
- [x] 后端新增 settings 路由读取/更新接收地址6条链
- [x] 前端接收地址从 API 读取,灰色只读可复制不可编辑
- [x] Admin 后台新增 ReceivingAddressPanel 接收地址配置入口(仅管理员可修改)
- [x] Bridge 页面接收地址从数据库读取currentReceivingAddress所有链统一只读显示
- [x] 数据库写入 BSC/ETH/Polygon/Arbitrum/Avalanche 地址0x43DAb577f3279e11D311E7d628C6201d893A9Aa3

View File

@ -154,6 +154,17 @@ const plugins = [react(), tailwindcss(), jsxLocPlugin()];
export default defineConfig({
plugins,
// ─── NAC Security: Force-clear all Manus platform env vars at build time ────
// This ensures no Manus OAuth URLs or API keys appear in the production bundle.
// The presale site uses its own admin login (/admin) and NAC's own OAuth.
define: {
"import.meta.env.VITE_OAUTH_PORTAL_URL": JSON.stringify(""),
"import.meta.env.VITE_APP_ID": JSON.stringify(""),
"import.meta.env.VITE_FRONTEND_FORGE_API_KEY": JSON.stringify(""),
"import.meta.env.VITE_FRONTEND_FORGE_API_URL": JSON.stringify(""),
"import.meta.env.VITE_ANALYTICS_ENDPOINT": JSON.stringify(""),
"import.meta.env.VITE_ANALYTICS_WEBSITE_ID": JSON.stringify(""),
},
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),