Checkpoint: 新增功能:1) 购买教程页面(/tutorial) - 支持7种钱包+3种网络的分步指南,中英双语;2) TRC20面板EVM地址输入 - 用户提交BSC/ETH地址用于接收XIC代币;3) 管理员后台(/admin) - 密码登录、TRC20购买记录查看、标记发放状态、CSV导出;4) 导航栏添加Tutorial链接;5) 数据库新增evmAddress列

This commit is contained in:
Manus 2026-03-07 22:17:35 -05:00
parent 80444bfdc6
commit 7acc5d4a0f
22 changed files with 2435 additions and 4 deletions

View File

@ -0,0 +1,106 @@
{
"query": "DESCRIBE trc20_purchases;",
"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 DESCRIBE trc20_purchases;",
"rows": [
{
"Field": "id",
"Type": "int",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "txHash",
"Type": "varchar(128)",
"Null": "NO",
"Key": "UNI",
"Default": "NULL",
"Extra": ""
},
{
"Field": "fromAddress",
"Type": "varchar(64)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "usdtAmount",
"Type": "decimal(20,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "xicAmount",
"Type": "decimal(30,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "blockNumber",
"Type": "bigint",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "status",
"Type": "enum('pending','confirmed','distributed','failed')",
"Null": "NO",
"Key": "",
"Default": "pending",
"Extra": ""
},
{
"Field": "distributedAt",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "distributeTxHash",
"Type": "varchar(128)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "updatedAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP"
},
{
"Field": "evmAddress",
"Type": "varchar(64)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\ntxHash\tvarchar(128)\tNO\tUNI\tNULL\t\nfromAddress\tvarchar(64)\tNO\t\tNULL\t\nusdtAmount\tdecimal(20,6)\tNO\t\tNULL\t\nxicAmount\tdecimal(30,6)\tNO\t\tNULL\t\nblockNumber\tbigint\tYES\t\tNULL\t\nstatus\tenum('pending','confirmed','distributed','failed')\tNO\t\tpending\t\ndistributedAt\ttimestamp\tYES\t\tNULL\t\ndistributeTxHash\tvarchar(128)\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\nevmAddress\tvarchar(64)\tYES\t\tNULL\t\n",
"stderr": "",
"execution_time_ms": 1590
}

View File

@ -0,0 +1,58 @@
{
"query": "SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"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 SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"rows": [
{
"COLUMN_NAME": "id",
"DATA_TYPE": "int"
},
{
"COLUMN_NAME": "txHash",
"DATA_TYPE": "varchar"
},
{
"COLUMN_NAME": "fromAddress",
"DATA_TYPE": "varchar"
},
{
"COLUMN_NAME": "usdtAmount",
"DATA_TYPE": "decimal"
},
{
"COLUMN_NAME": "xicAmount",
"DATA_TYPE": "decimal"
},
{
"COLUMN_NAME": "blockNumber",
"DATA_TYPE": "bigint"
},
{
"COLUMN_NAME": "status",
"DATA_TYPE": "enum"
},
{
"COLUMN_NAME": "distributedAt",
"DATA_TYPE": "timestamp"
},
{
"COLUMN_NAME": "distributeTxHash",
"DATA_TYPE": "varchar"
},
{
"COLUMN_NAME": "createdAt",
"DATA_TYPE": "timestamp"
},
{
"COLUMN_NAME": "updatedAt",
"DATA_TYPE": "timestamp"
},
{
"COLUMN_NAME": "evmAddress",
"DATA_TYPE": "varchar"
}
],
"messages": [],
"stdout": "COLUMN_NAME\tDATA_TYPE\nid\tint\ntxHash\tvarchar\nfromAddress\tvarchar\nusdtAmount\tdecimal\nxicAmount\tdecimal\nblockNumber\tbigint\nstatus\tenum\ndistributedAt\ttimestamp\ndistributeTxHash\tvarchar\ncreatedAt\ttimestamp\nupdatedAt\ttimestamp\nevmAddress\tvarchar\n",
"stderr": "",
"execution_time_ms": 1492
}

View File

@ -0,0 +1,106 @@
{
"query": "SHOW COLUMNS FROM trc20_purchases;",
"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 SHOW COLUMNS FROM trc20_purchases;",
"rows": [
{
"Field": "id",
"Type": "int",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "txHash",
"Type": "varchar(128)",
"Null": "NO",
"Key": "UNI",
"Default": "NULL",
"Extra": ""
},
{
"Field": "fromAddress",
"Type": "varchar(64)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "usdtAmount",
"Type": "decimal(20,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "xicAmount",
"Type": "decimal(30,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "blockNumber",
"Type": "bigint",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "status",
"Type": "enum('pending','confirmed','distributed','failed')",
"Null": "NO",
"Key": "",
"Default": "pending",
"Extra": ""
},
{
"Field": "distributedAt",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "distributeTxHash",
"Type": "varchar(128)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "updatedAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP"
},
{
"Field": "evmAddress",
"Type": "varchar(64)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\ntxHash\tvarchar(128)\tNO\tUNI\tNULL\t\nfromAddress\tvarchar(64)\tNO\t\tNULL\t\nusdtAmount\tdecimal(20,6)\tNO\t\tNULL\t\nxicAmount\tdecimal(30,6)\tNO\t\tNULL\t\nblockNumber\tbigint\tYES\t\tNULL\t\nstatus\tenum('pending','confirmed','distributed','failed')\tNO\t\tpending\t\ndistributedAt\ttimestamp\tYES\t\tNULL\t\ndistributeTxHash\tvarchar(128)\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\nevmAddress\tvarchar(64)\tYES\t\tNULL\t\n",
"stderr": "",
"execution_time_ms": 1499
}

View File

@ -0,0 +1,13 @@
{
"query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' AND COLUMN_NAME LIKE '%vm%';",
"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 SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' AND COLUMN_NAME LIKE '%vm%';",
"rows": [
{
"COLUMN_NAME": "evmAddress"
}
],
"messages": [],
"stdout": "COLUMN_NAME\nevmAddress\n",
"stderr": "",
"execution_time_ms": 1512
}

View File

@ -0,0 +1,46 @@
{
"query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases';",
"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 SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases';",
"rows": [
{
"COLUMN_NAME": "id"
},
{
"COLUMN_NAME": "txHash"
},
{
"COLUMN_NAME": "fromAddress"
},
{
"COLUMN_NAME": "usdtAmount"
},
{
"COLUMN_NAME": "xicAmount"
},
{
"COLUMN_NAME": "blockNumber"
},
{
"COLUMN_NAME": "status"
},
{
"COLUMN_NAME": "distributedAt"
},
{
"COLUMN_NAME": "distributeTxHash"
},
{
"COLUMN_NAME": "createdAt"
},
{
"COLUMN_NAME": "updatedAt"
},
{
"COLUMN_NAME": "evmAddress"
}
],
"messages": [],
"stdout": "COLUMN_NAME\nid\ntxHash\nfromAddress\nusdtAmount\nxicAmount\nblockNumber\nstatus\ndistributedAt\ndistributeTxHash\ncreatedAt\nupdatedAt\nevmAddress\n",
"stderr": "",
"execution_time_ms": 1493
}

View File

@ -0,0 +1,58 @@
{
"query": "SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"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 SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"rows": [
{
"COLUMN_NAME": "id",
"COLUMN_TYPE": "int"
},
{
"COLUMN_NAME": "txHash",
"COLUMN_TYPE": "varchar(128)"
},
{
"COLUMN_NAME": "fromAddress",
"COLUMN_TYPE": "varchar(64)"
},
{
"COLUMN_NAME": "usdtAmount",
"COLUMN_TYPE": "decimal(20,6)"
},
{
"COLUMN_NAME": "xicAmount",
"COLUMN_TYPE": "decimal(30,6)"
},
{
"COLUMN_NAME": "blockNumber",
"COLUMN_TYPE": "bigint"
},
{
"COLUMN_NAME": "status",
"COLUMN_TYPE": "enum('pending','confirmed','distributed','failed')"
},
{
"COLUMN_NAME": "distributedAt",
"COLUMN_TYPE": "timestamp"
},
{
"COLUMN_NAME": "distributeTxHash",
"COLUMN_TYPE": "varchar(128)"
},
{
"COLUMN_NAME": "createdAt",
"COLUMN_TYPE": "timestamp"
},
{
"COLUMN_NAME": "updatedAt",
"COLUMN_TYPE": "timestamp"
},
{
"COLUMN_NAME": "evmAddress",
"COLUMN_TYPE": "varchar(64)"
}
],
"messages": [],
"stdout": "COLUMN_NAME\tCOLUMN_TYPE\nid\tint\ntxHash\tvarchar(128)\nfromAddress\tvarchar(64)\nusdtAmount\tdecimal(20,6)\nxicAmount\tdecimal(30,6)\nblockNumber\tbigint\nstatus\tenum('pending','confirmed','distributed','failed')\ndistributedAt\ttimestamp\ndistributeTxHash\tvarchar(128)\ncreatedAt\ttimestamp\nupdatedAt\ttimestamp\nevmAddress\tvarchar(64)\n",
"stderr": "",
"execution_time_ms": 1572
}

View File

@ -0,0 +1,13 @@
{
"query": "SELECT 1 FROM trc20_purchases LIMIT 0; SELECT evmAddress FROM trc20_purchases LIMIT 1;",
"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 SELECT 1 FROM trc20_purchases LIMIT 0; SELECT evmAddress FROM trc20_purchases LIMIT 1;",
"rows": [
{
"evmAddress": "NULL"
}
],
"messages": [],
"stdout": "evmAddress\nNULL\n",
"stderr": "",
"execution_time_ms": 1778
}

View File

@ -0,0 +1,70 @@
{
"query": "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'trc20_purchases';",
"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 SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'trc20_purchases';",
"rows": [
{
"COLUMN_NAME": "id",
"COLUMN_TYPE": "int",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "txHash",
"COLUMN_TYPE": "varchar(128)",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "fromAddress",
"COLUMN_TYPE": "varchar(64)",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "usdtAmount",
"COLUMN_TYPE": "decimal(20,6)",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "xicAmount",
"COLUMN_TYPE": "decimal(30,6)",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "blockNumber",
"COLUMN_TYPE": "bigint",
"IS_NULLABLE": "YES"
},
{
"COLUMN_NAME": "status",
"COLUMN_TYPE": "enum('pending','confirmed','distributed','failed')",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "distributedAt",
"COLUMN_TYPE": "timestamp",
"IS_NULLABLE": "YES"
},
{
"COLUMN_NAME": "distributeTxHash",
"COLUMN_TYPE": "varchar(128)",
"IS_NULLABLE": "YES"
},
{
"COLUMN_NAME": "createdAt",
"COLUMN_TYPE": "timestamp",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "updatedAt",
"COLUMN_TYPE": "timestamp",
"IS_NULLABLE": "NO"
},
{
"COLUMN_NAME": "evmAddress",
"COLUMN_TYPE": "varchar(64)",
"IS_NULLABLE": "YES"
}
],
"messages": [],
"stdout": "COLUMN_NAME\tCOLUMN_TYPE\tIS_NULLABLE\nid\tint\tNO\ntxHash\tvarchar(128)\tNO\nfromAddress\tvarchar(64)\tNO\nusdtAmount\tdecimal(20,6)\tNO\nxicAmount\tdecimal(30,6)\tNO\nblockNumber\tbigint\tYES\nstatus\tenum('pending','confirmed','distributed','failed')\tNO\ndistributedAt\ttimestamp\tYES\ndistributeTxHash\tvarchar(128)\tYES\ncreatedAt\ttimestamp\tNO\nupdatedAt\ttimestamp\tNO\nevmAddress\tvarchar(64)\tYES\n",
"stderr": "",
"execution_time_ms": 1482
}

View File

@ -0,0 +1,13 @@
{
"query": "SELECT `evmAddress` FROM trc20_purchases LIMIT 1;",
"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 SELECT `evmAddress` FROM trc20_purchases LIMIT 1;",
"rows": [
{
"evmAddress": "NULL"
}
],
"messages": [],
"stdout": "evmAddress\nNULL\n",
"stderr": "",
"execution_time_ms": 1511
}

View File

@ -0,0 +1,106 @@
{
"query": "SHOW COLUMNS FROM trc20_purchases;",
"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 SHOW COLUMNS FROM trc20_purchases;",
"rows": [
{
"Field": "id",
"Type": "int",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "txHash",
"Type": "varchar(128)",
"Null": "NO",
"Key": "UNI",
"Default": "NULL",
"Extra": ""
},
{
"Field": "fromAddress",
"Type": "varchar(64)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "usdtAmount",
"Type": "decimal(20,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "xicAmount",
"Type": "decimal(30,6)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "blockNumber",
"Type": "bigint",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "status",
"Type": "enum('pending','confirmed','distributed','failed')",
"Null": "NO",
"Key": "",
"Default": "pending",
"Extra": ""
},
{
"Field": "distributedAt",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "distributeTxHash",
"Type": "varchar(128)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "updatedAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP"
},
{
"Field": "evmAddress",
"Type": "varchar(64)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\ntxHash\tvarchar(128)\tNO\tUNI\tNULL\t\nfromAddress\tvarchar(64)\tNO\t\tNULL\t\nusdtAmount\tdecimal(20,6)\tNO\t\tNULL\t\nxicAmount\tdecimal(30,6)\tNO\t\tNULL\t\nblockNumber\tbigint\tYES\t\tNULL\t\nstatus\tenum('pending','confirmed','distributed','failed')\tNO\t\tpending\t\ndistributedAt\ttimestamp\tYES\t\tNULL\t\ndistributeTxHash\tvarchar(128)\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\nevmAddress\tvarchar(64)\tYES\t\tNULL\t\n",
"stderr": "",
"execution_time_ms": 1594
}

View File

@ -0,0 +1,46 @@
{
"query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"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 SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'trc20_purchases' ORDER BY ORDINAL_POSITION;",
"rows": [
{
"COLUMN_NAME": "id"
},
{
"COLUMN_NAME": "txHash"
},
{
"COLUMN_NAME": "fromAddress"
},
{
"COLUMN_NAME": "usdtAmount"
},
{
"COLUMN_NAME": "xicAmount"
},
{
"COLUMN_NAME": "blockNumber"
},
{
"COLUMN_NAME": "status"
},
{
"COLUMN_NAME": "distributedAt"
},
{
"COLUMN_NAME": "distributeTxHash"
},
{
"COLUMN_NAME": "createdAt"
},
{
"COLUMN_NAME": "updatedAt"
},
{
"COLUMN_NAME": "evmAddress"
}
],
"messages": [],
"stdout": "COLUMN_NAME\nid\ntxHash\nfromAddress\nusdtAmount\nxicAmount\nblockNumber\nstatus\ndistributedAt\ndistributeTxHash\ncreatedAt\nupdatedAt\nevmAddress\n",
"stderr": "",
"execution_time_ms": 1593
}

View File

@ -5,22 +5,35 @@ import { Route, Switch } 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";
function Router() {
// make sure to consider if you need authentication for certain routes
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/tutorial"} component={Tutorial} />
<Route path={"/admin"} component={Admin} />
<Route path={"/404"} component={NotFound} />
{/* Final fallback route */}
<Route component={NotFound} />
</Switch>
);
}
// 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>
<ThemeProvider defaultTheme="dark">
<ThemeProvider
defaultTheme="dark"
// switchable
>
<TooltipProvider>
<Toaster />
<Router />

463
client/src/pages/Admin.tsx Normal file
View File

@ -0,0 +1,463 @@
/**
* Admin Dashboard
* Login-protected page for managing TRC20 purchases
* Features: purchase list, status updates, export, stats
*/
import { useState, useEffect } from "react";
import { trpc } from "@/lib/trpc";
import { Link } from "wouter";
// ─── Types ────────────────────────────────────────────────────────────────────
interface Purchase {
id: number;
txHash: string;
fromAddress: string;
evmAddress: string | null;
usdtAmount: number;
xicAmount: number;
status: "pending" | "confirmed" | "distributed" | "failed";
distributedAt: Date | null;
distributeTxHash: string | null;
createdAt: Date;
}
// ─── Status Badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: Purchase["status"] }) {
const config = {
pending: { color: "#f0b429", bg: "rgba(240,180,41,0.15)", label: "Pending" },
confirmed: { color: "#00d4ff", bg: "rgba(0,212,255,0.15)", label: "Confirmed" },
distributed: { color: "#00e676", bg: "rgba(0,230,118,0.15)", label: "Distributed" },
failed: { color: "#ff5252", bg: "rgba(255,82,82,0.15)", label: "Failed" },
};
const cfg = config[status] || config.pending;
return (
<span
className="text-xs font-semibold px-2 py-1 rounded-full"
style={{ color: cfg.color, background: cfg.bg }}
>
{cfg.label}
</span>
);
}
// ─── Login Form ───────────────────────────────────────────────────────────────
function LoginForm({ onLogin }: { onLogin: (token: string) => void }) {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const loginMutation = trpc.admin.login.useMutation({
onSuccess: (data) => {
onLogin(data.token);
},
onError: (err) => {
setError(err.message);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError("");
loginMutation.mutate({ password });
};
return (
<div className="min-h-screen flex items-center justify-center" style={{ background: "#0a0a0f" }}>
<div className="w-full max-w-sm px-4">
<div className="rounded-2xl p-8" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(240,180,41,0.2)" }}>
<div className="text-center mb-8">
<div className="text-4xl mb-3">🔐</div>
<h1 className="text-2xl font-bold text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
Admin Dashboard
</h1>
<p className="text-sm text-white/40 mt-1">NAC XIC Presale Management</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-sm text-white/60 font-medium block mb-2">Admin Password</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Enter admin password"
className="w-full px-4 py-3 rounded-xl text-sm"
style={{
background: "rgba(255,255,255,0.05)",
border: error ? "1px solid rgba(255,82,82,0.5)" : "1px solid rgba(255,255,255,0.1)",
color: "white",
outline: "none",
}}
autoFocus
/>
{error && <p className="text-xs text-red-400 mt-1">{error}</p>}
</div>
<button
type="submit"
disabled={loginMutation.isPending || !password}
className="w-full py-3 rounded-xl text-sm font-bold transition-all"
style={{
background: "linear-gradient(135deg, #f0b429 0%, #ffd700 100%)",
color: "#0a0a0f",
opacity: loginMutation.isPending || !password ? 0.6 : 1,
}}
>
{loginMutation.isPending ? "Logging in..." : "Login"}
</button>
</form>
<div className="mt-6 text-center">
<Link href="/">
<span className="text-xs text-white/30 hover:text-white/60 cursor-pointer transition-colors">
Back to Presale
</span>
</Link>
</div>
</div>
</div>
</div>
);
}
// ─── Main Dashboard ───────────────────────────────────────────────────────────
function Dashboard({ token, onLogout }: { token: string; onLogout: () => void }) {
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<"all" | "pending" | "confirmed" | "distributed" | "failed">("all");
const [markingId, setMarkingId] = useState<number | null>(null);
const [distributeTxInput, setDistributeTxInput] = useState<Record<number, string>>({});
const { data: statsData, refetch: refetchStats } = trpc.admin.stats.useQuery({ token });
const { data: purchasesData, refetch: refetchPurchases, isLoading } = trpc.admin.listPurchases.useQuery({
token,
page,
limit: 20,
status: statusFilter,
});
const markDistributedMutation = trpc.admin.markDistributed.useMutation({
onSuccess: () => {
refetchPurchases();
refetchStats();
setMarkingId(null);
},
});
const handleMarkDistributed = (id: number) => {
setMarkingId(id);
markDistributedMutation.mutate({
token,
purchaseId: id,
distributeTxHash: distributeTxInput[id] || undefined,
});
};
const formatAddress = (addr: string | null) => {
if (!addr) return <span className="text-white/30 text-xs"></span>;
return (
<span className="text-xs font-mono" style={{ color: "#00d4ff" }}>
{addr.slice(0, 8)}...{addr.slice(-6)}
</span>
);
};
const formatDate = (d: Date | null) => {
if (!d) return "—";
return new Date(d).toLocaleString();
};
const totalStats = statsData?.reduce(
(acc, s) => ({
totalUsdt: acc.totalUsdt + s.totalUsdt,
totalXic: acc.totalXic + s.totalXic,
totalCount: acc.totalCount + s.count,
}),
{ totalUsdt: 0, totalXic: 0, totalCount: 0 }
) || { totalUsdt: 0, totalXic: 0, totalCount: 0 };
const pendingCount = statsData?.find(s => s.status === "confirmed")?.count || 0;
// Export CSV
const handleExport = () => {
if (!purchasesData?.purchases) return;
const rows = [
["ID", "TX Hash", "From Address", "EVM Address", "USDT", "XIC", "Status", "Created At", "Distributed At"],
...purchasesData.purchases.map(p => [
p.id,
p.txHash,
p.fromAddress,
p.evmAddress || "",
p.usdtAmount,
p.xicAmount,
p.status,
formatDate(p.createdAt),
formatDate(p.distributedAt),
]),
];
const csv = rows.map(r => r.join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `xic-purchases-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="min-h-screen" style={{ background: "#0a0a0f" }}>
{/* ── Header ── */}
<nav className="sticky top-0 z-50 flex items-center justify-between px-6 py-4"
style={{ background: "rgba(10,10,15,0.95)", borderBottom: "1px solid rgba(240,180,41,0.1)", backdropFilter: "blur(12px)" }}>
<div className="flex items-center gap-3">
<span className="text-xl"></span>
<div>
<span className="font-bold text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>Admin Dashboard</span>
<span className="ml-2 text-xs px-2 py-0.5 rounded-full font-semibold"
style={{ background: "rgba(240,180,41,0.15)", color: "#f0b429", border: "1px solid rgba(240,180,41,0.3)" }}>
NAC XIC Presale
</span>
</div>
</div>
<div className="flex items-center gap-3">
<Link href="/">
<span className="text-sm text-white/50 hover:text-white/80 cursor-pointer transition-colors"> Presale</span>
</Link>
<button
onClick={onLogout}
className="px-4 py-2 rounded-xl text-sm font-semibold transition-all"
style={{ background: "rgba(255,82,82,0.1)", border: "1px solid rgba(255,82,82,0.3)", color: "#ff5252" }}
>
Logout
</button>
</div>
</nav>
<div className="container mx-auto px-4 py-6 max-w-7xl">
{/* ── Stats Cards ── */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{[
{ label: "Total USDT Raised", value: `$${totalStats.totalUsdt.toLocaleString(undefined, { maximumFractionDigits: 2 })}`, color: "#f0b429" },
{ label: "Total XIC Sold", value: `${(totalStats.totalXic / 1e6).toFixed(2)}M`, color: "#00d4ff" },
{ label: "Total Purchases", value: totalStats.totalCount.toString(), color: "#00e676" },
{ label: "Pending Distribution", value: pendingCount.toString(), color: pendingCount > 0 ? "#ff5252" : "#00e676" },
].map(({ label, value, color }) => (
<div key={label} className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}>
<div className="text-2xl font-bold" style={{ color, fontFamily: "'Space Grotesk', sans-serif" }}>{value}</div>
<div className="text-xs text-white/40 mt-1">{label}</div>
</div>
))}
</div>
{/* ── Status Breakdown ── */}
{statsData && statsData.length > 0 && (
<div className="rounded-2xl p-5 mb-6" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.06)" }}>
<h3 className="text-xs font-semibold uppercase tracking-widest text-white/40 mb-4">Status Breakdown</h3>
<div className="flex flex-wrap gap-4">
{statsData.map(s => (
<div key={s.status} className="flex items-center gap-2">
<StatusBadge status={s.status as Purchase["status"]} />
<span className="text-sm text-white/60">{s.count} purchases · ${s.totalUsdt.toFixed(2)} USDT</span>
</div>
))}
</div>
</div>
)}
{/* ── Purchases Table ── */}
<div className="rounded-2xl overflow-hidden" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.06)" }}>
{/* Table Header */}
<div className="flex items-center justify-between px-5 py-4" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
<h3 className="font-semibold text-white/80" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
TRC20 Purchases
{purchasesData && <span className="text-white/40 text-sm ml-2">({purchasesData.total} total)</span>}
</h3>
<div className="flex items-center gap-3">
{/* Status Filter */}
<select
value={statusFilter}
onChange={e => { setStatusFilter(e.target.value as typeof statusFilter); setPage(1); }}
className="px-3 py-1.5 rounded-lg text-sm"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.12)", color: "white" }}
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="distributed">Distributed</option>
<option value="failed">Failed</option>
</select>
{/* Export */}
<button
onClick={handleExport}
className="px-4 py-1.5 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(0,212,255,0.1)", border: "1px solid rgba(0,212,255,0.3)", color: "#00d4ff" }}
>
Export CSV
</button>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
{isLoading ? (
<div className="text-center py-12 text-white/40">Loading...</div>
) : !purchasesData?.purchases?.length ? (
<div className="text-center py-12 text-white/40">No purchases found</div>
) : (
<table className="w-full text-sm">
<thead>
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
{["ID", "TX Hash", "From (TRON)", "EVM Address", "USDT", "XIC", "Status", "Created", "Action"].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">{h}</th>
))}
</tr>
</thead>
<tbody>
{purchasesData.purchases.map((p, i) => (
<tr
key={p.id}
style={{
borderBottom: "1px solid rgba(255,255,255,0.04)",
background: i % 2 === 0 ? "transparent" : "rgba(255,255,255,0.01)",
}}
>
<td className="px-4 py-3 text-white/60">{p.id}</td>
<td className="px-4 py-3">
<a
href={`https://tronscan.org/#/transaction/${p.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-mono hover:underline"
style={{ color: "#00d4ff" }}
>
{p.txHash.slice(0, 8)}...{p.txHash.slice(-6)}
</a>
</td>
<td className="px-4 py-3">{formatAddress(p.fromAddress)}</td>
<td className="px-4 py-3">
{p.evmAddress ? (
<span className="text-xs font-mono text-green-400">
{p.evmAddress.slice(0, 8)}...{p.evmAddress.slice(-6)}
</span>
) : (
<span className="text-xs text-red-400"> No EVM addr</span>
)}
</td>
<td className="px-4 py-3 font-semibold" style={{ color: "#f0b429" }}>
${p.usdtAmount.toFixed(2)}
</td>
<td className="px-4 py-3 text-white/70">
{(p.xicAmount / 1e6).toFixed(2)}M
</td>
<td className="px-4 py-3">
<StatusBadge status={p.status} />
</td>
<td className="px-4 py-3 text-white/40 text-xs">
{new Date(p.createdAt).toLocaleDateString()}
</td>
<td className="px-4 py-3">
{p.status === "confirmed" && (
<div className="flex items-center gap-2">
<input
type="text"
placeholder="TX hash (optional)"
value={distributeTxInput[p.id] || ""}
onChange={e => setDistributeTxInput(prev => ({ ...prev, [p.id]: e.target.value }))}
className="px-2 py-1 rounded text-xs w-32"
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)", color: "white" }}
/>
<button
onClick={() => handleMarkDistributed(p.id)}
disabled={markingId === p.id}
className="px-3 py-1 rounded-lg text-xs font-semibold transition-all whitespace-nowrap"
style={{ background: "rgba(0,230,118,0.15)", border: "1px solid rgba(0,230,118,0.3)", color: "#00e676" }}
>
{markingId === p.id ? "..." : "Mark Distributed"}
</button>
</div>
)}
{p.status === "distributed" && p.distributeTxHash && (
<a
href={`https://bscscan.com/tx/${p.distributeTxHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs hover:underline"
style={{ color: "#00e676" }}
>
View TX
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Pagination */}
{purchasesData && purchasesData.total > 20 && (
<div className="flex items-center justify-between px-5 py-4" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
<span className="text-xs text-white/40">
Showing {((page - 1) * 20) + 1}{Math.min(page * 20, purchasesData.total)} of {purchasesData.total}
</span>
<div className="flex gap-2">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all disabled:opacity-30"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: "white" }}
>
Prev
</button>
<span className="px-3 py-1.5 text-xs text-white/60">Page {page}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={page * 20 >= purchasesData.total}
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all disabled:opacity-30"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: "white" }}
>
Next
</button>
</div>
</div>
)}
</div>
{/* ── Instructions ── */}
<div className="mt-6 rounded-2xl p-5" style={{ background: "rgba(0,212,255,0.04)", border: "1px solid rgba(0,212,255,0.15)" }}>
<h3 className="text-sm font-semibold text-cyan-400 mb-3">Distribution Workflow</h3>
<div className="space-y-2 text-sm text-white/60">
<p>1. <strong className="text-white/80">Confirmed</strong> = TRC20 USDT received, waiting for XIC distribution</p>
<p>2. Check if buyer provided an EVM address (0x...) shown in "EVM Address" column</p>
<p>3. Send XIC tokens from operator wallet to buyer's EVM address on BSC</p>
<p>4. Enter the BSC distribution TX hash and click <strong className="text-white/80">"Mark Distributed"</strong></p>
<p>5. <strong className="text-white/80">No EVM address?</strong> Contact buyer via Telegram/email to get their BSC address</p>
</div>
</div>
</div>
</div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function Admin() {
const [token, setToken] = useState<string | null>(() => {
return sessionStorage.getItem("nac-admin-token");
});
const handleLogin = (t: string) => {
sessionStorage.setItem("nac-admin-token", t);
setToken(t);
};
const handleLogout = () => {
sessionStorage.removeItem("nac-admin-token");
setToken(null);
};
if (!token) {
return <LoginForm onLogin={handleLogin} />;
}
return <Dashboard token={token} onLogout={handleLogout} />;
}

View File

@ -5,6 +5,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { toast } from "sonner";
import { Link } from "wouter";
import { useWallet } from "@/hooks/useWallet";
import { usePresale } from "@/hooks/usePresale";
import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts";
@ -107,6 +108,19 @@ function TRC20Panel({ usdtAmount, lang }: { usdtAmount: number; lang: Lang }) {
const { t } = useTranslation(lang);
const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice;
const [copied, setCopied] = useState(false);
const [evmAddress, setEvmAddress] = useState("");
const [evmAddrError, setEvmAddrError] = useState("");
const [submitted, setSubmitted] = useState(false);
const submitTrc20Mutation = trpc.presale.registerTrc20Intent.useMutation({
onSuccess: () => {
setSubmitted(true);
toast.success(lang === "zh" ? "EVM地址已保存" : "EVM address saved!");
},
onError: (err: { message: string }) => {
toast.error(err.message);
},
});
const copyAddress = () => {
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
@ -115,8 +129,66 @@ function TRC20Panel({ usdtAmount, lang }: { usdtAmount: number; lang: Lang }) {
setTimeout(() => setCopied(false), 2000);
};
const validateEvmAddress = (addr: string) => {
if (!addr) return lang === "zh" ? "请输入您的EVM地址" : "Please enter your EVM address";
if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的EVM地址格式应以0x开头共42位" : "Invalid EVM address format (must start with 0x, 42 chars)";
return "";
};
const handleEvmSubmit = () => {
const err = validateEvmAddress(evmAddress);
if (err) { setEvmAddrError(err); return; }
setEvmAddrError("");
submitTrc20Mutation.mutate({ evmAddress });
};
return (
<div className="space-y-4">
{/* EVM Address Input — Required for token distribution */}
<div className="rounded-xl p-4 space-y-3" style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.25)" }}>
<div className="flex items-center gap-2">
<span className="text-amber-400 text-sm"></span>
<p className="text-sm font-semibold text-amber-300">
{lang === "zh" ? "必填您的EVM钱包地址用于接收XIC代币" : "Required: Your EVM Wallet Address (to receive XIC tokens)"}
</p>
</div>
<p className="text-xs text-white/50">
{lang === "zh"
? "XIC代币在BSC网络上发放。请提供您的BSC/ETH地址0x开头以便我们将代币发送给您。"
: "XIC tokens are distributed on BSC. Please provide your BSC/ETH address (starts with 0x) so we can send your tokens."}
</p>
<div className="space-y-2">
<input
type="text"
value={evmAddress}
onChange={e => { setEvmAddress(e.target.value); setEvmAddrError(""); setSubmitted(false); }}
placeholder={lang === "zh" ? "0x... (您的BSC/ETH地址)" : "0x... (your BSC/ETH address)"}
className="w-full px-4 py-3 rounded-xl text-sm font-mono"
style={{
background: "rgba(255,255,255,0.05)",
border: evmAddrError ? "1px solid rgba(255,82,82,0.5)" : submitted ? "1px solid rgba(0,230,118,0.4)" : "1px solid rgba(255,255,255,0.12)",
color: "white",
outline: "none",
}}
/>
{evmAddrError && <p className="text-xs text-red-400">{evmAddrError}</p>}
{submitted && <p className="text-xs text-green-400"> {lang === "zh" ? "EVM地址已保存" : "EVM address saved"}</p>}
<button
onClick={handleEvmSubmit}
disabled={submitTrc20Mutation.isPending || submitted || !evmAddress}
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
style={{
background: submitted ? "rgba(0,230,118,0.15)" : "rgba(240,180,41,0.15)",
border: submitted ? "1px solid rgba(0,230,118,0.3)" : "1px solid rgba(240,180,41,0.3)",
color: submitted ? "#00e676" : "#f0b429",
opacity: !evmAddress ? 0.5 : 1,
}}
>
{submitTrc20Mutation.isPending ? (lang === "zh" ? "保存中..." : "Saving...") : submitted ? (lang === "zh" ? "✓ 已保存" : "✓ Saved") : (lang === "zh" ? "保存EVM地址" : "Save EVM Address")}
</button>
</div>
</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
@ -137,17 +209,22 @@ function TRC20Panel({ usdtAmount, lang }: { usdtAmount: number; lang: Lang }) {
<div className="space-y-2">
<StepBadge num={1} text={
lang === "zh"
? `在上方填写您的EVM地址并保存`
: `Enter and save your EVM address above`
} />
<StepBadge num={2} text={
lang === "zh"
? `发送 ${usdtAmount > 0 ? usdtAmount.toFixed(2) + " USDT" : "任意数量 USDT"}TRC20到上方地址`
: `${t("trc20_step1")} ${usdtAmount > 0 ? usdtAmount.toFixed(2) + " USDT" : t("trc20_step1_any")} (TRC20) ${t("trc20_step1b")}`
} />
<StepBadge num={2} text={`${t("trc20_step2")} ${PRESALE_CONFIG.trc20Memo} ${t("trc20_step2b")}`} />
<StepBadge num={3} text={
<StepBadge num={3} text={`${t("trc20_step2")} ${PRESALE_CONFIG.trc20Memo} ${t("trc20_step2b")}`} />
<StepBadge num={4} text={
lang === "zh"
? (usdtAmount > 0 ? `${t("trc20_step3")} ${formatNumber(tokenAmount)} ${t("trc20_step3b")}` : t("trc20_step3_any"))
: (usdtAmount > 0 ? `You will receive ${formatNumber(tokenAmount)} XIC tokens after confirmation (1-24h)` : t("trc20_step3_any"))
} />
<StepBadge num={4} text={t("trc20_step4")} />
<StepBadge num={5} text={t("trc20_step4")} />
</div>
<div className="rounded-lg p-3 text-xs text-amber-300/80" style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.2)" }}>
@ -736,6 +813,11 @@ export default function Home() {
<a href="https://newassetchain.io" target="_blank" rel="noopener noreferrer" className="text-sm text-white/60 hover:text-white/90 transition-colors hidden md:block">{t("nav_website")}</a>
<a href="https://lens.newassetchain.io" target="_blank" rel="noopener noreferrer" className="text-sm text-white/60 hover:text-white/90 transition-colors hidden md:block">{t("nav_explorer")}</a>
<a href="https://docs.newassetchain.io" target="_blank" rel="noopener noreferrer" className="text-sm text-white/60 hover:text-white/90 transition-colors hidden md:block">{t("nav_docs")}</a>
<Link href="/tutorial">
<span className="text-sm font-semibold cursor-pointer transition-colors hidden md:block" style={{ color: "#00d4ff" }}>
{lang === "zh" ? "📖 购买教程" : "📖 Tutorial"}
</span>
</Link>
<LangToggle lang={lang} setLang={setLang} />
<NavWalletButton lang={lang} />
</div>

View File

@ -0,0 +1,737 @@
/**
* Purchase Tutorial Page
* Detailed step-by-step guide for BSC/ETH/TRC20 purchases
* Covers 7 wallets: MetaMask, Trust Wallet, OKX, Binance Web3, TokenPocket, imToken, WalletConnect
*/
import { useState } from "react";
import { Link } from "wouter";
// ─── Types ────────────────────────────────────────────────────────────────────
type WalletId = "metamask" | "trust" | "okx" | "binance" | "tokenpocket" | "imtoken" | "walletconnect";
type ChainId = "bsc" | "eth" | "tron";
// ─── Constants ────────────────────────────────────────────────────────────────
const RECEIVING_ADDRESSES = {
trc20: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
erc20: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
};
const CONTRACTS = {
bsc: {
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c",
usdt: "0x55d398326f99059fF775485246999027B3197955",
explorer: "https://bscscan.com",
},
eth: {
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
explorer: "https://etherscan.io",
},
};
// ─── Wallet Config ────────────────────────────────────────────────────────────
const WALLETS: Array<{
id: WalletId;
name: string;
icon: string;
chains: ChainId[];
color: string;
downloadUrl: string;
}> = [
{
id: "metamask",
name: "MetaMask",
icon: "🦊",
chains: ["bsc", "eth"],
color: "#E8831D",
downloadUrl: "https://metamask.io/download/",
},
{
id: "trust",
name: "Trust Wallet",
icon: "🛡️",
chains: ["bsc", "eth", "tron"],
color: "#3375BB",
downloadUrl: "https://trustwallet.com/download",
},
{
id: "okx",
name: "OKX Wallet",
icon: "⭕",
chains: ["bsc", "eth", "tron"],
color: "#000000",
downloadUrl: "https://www.okx.com/web3",
},
{
id: "binance",
name: "Binance Web3",
icon: "🔶",
chains: ["bsc", "eth"],
color: "#F0B90B",
downloadUrl: "https://www.binance.com/en/web3wallet",
},
{
id: "tokenpocket",
name: "TokenPocket",
icon: "💼",
chains: ["bsc", "eth", "tron"],
color: "#2980FE",
downloadUrl: "https://www.tokenpocket.pro/",
},
{
id: "imtoken",
name: "imToken",
icon: "💎",
chains: ["bsc", "eth"],
color: "#11C4D1",
downloadUrl: "https://token.im/download",
},
{
id: "walletconnect",
name: "WalletConnect",
icon: "🔗",
chains: ["bsc", "eth"],
color: "#3B99FC",
downloadUrl: "https://walletconnect.com/",
},
];
// ─── Tutorial Content ─────────────────────────────────────────────────────────
function getWalletSteps(walletId: WalletId, chain: ChainId): Array<{ title: string; desc: string; tip?: string }> {
const isTron = chain === "tron";
// EVM wallet steps for BSC/ETH
const evmSteps: Record<WalletId, Array<{ title: string; desc: string; tip?: string }>> = {
metamask: [
{
title: "Install MetaMask",
desc: "Download MetaMask from metamask.io or your browser's extension store. Create a new wallet or import an existing one. Securely backup your seed phrase.",
tip: "MetaMask is available as a browser extension (Chrome, Firefox, Edge) and as a mobile app.",
},
{
title: "Add Network",
desc: chain === "bsc"
? "In MetaMask, click the network dropdown at the top. Select 'Add Network' → 'Add a network manually'. Enter: Network Name: BNB Smart Chain, RPC URL: https://bsc-dataseed.binance.org/, Chain ID: 56, Symbol: BNB, Explorer: https://bscscan.com"
: "Ethereum Mainnet is pre-configured in MetaMask. Simply select 'Ethereum Mainnet' from the network dropdown.",
tip: chain === "bsc" ? "You can also add BSC automatically via chainlist.org" : undefined,
},
{
title: "Get USDT",
desc: chain === "bsc"
? "Purchase BEP-20 USDT on Binance, OKX, or any exchange. Withdraw to your MetaMask address on the BSC network. Make sure you also have some BNB for gas fees."
: "Purchase ERC-20 USDT on any exchange. Withdraw to your MetaMask address on the Ethereum network. You'll need ETH for gas fees.",
},
{
title: "Connect to Presale",
desc: "Return to the presale page and click 'Connect Wallet' in the top-right corner. MetaMask will prompt you to connect — click 'Connect'. Your wallet address will appear in the navigation bar.",
},
{
title: "Find Your EVM Address",
desc: "Your EVM address is shown at the top of MetaMask (starts with 0x). Click it to copy. This is the address where your XIC tokens will be sent.",
tip: "Your BSC and ETH address are the same in MetaMask.",
},
{
title: "Purchase XIC",
desc: `Select the ${chain.toUpperCase()} tab on the presale page. Enter the USDT amount you want to spend. Click 'Buy XIC' — you'll need to approve USDT spending first, then confirm the purchase transaction.`,
tip: "Two transactions are required: first approve USDT, then confirm purchase. Both require small gas fees.",
},
],
trust: [
{
title: "Install Trust Wallet",
desc: "Download Trust Wallet from trustwallet.com or your app store. Create a new wallet and securely backup your 12-word recovery phrase.",
tip: "Trust Wallet is a mobile-first wallet supporting 100+ blockchains.",
},
{
title: "Select Network",
desc: chain === "bsc"
? "In Trust Wallet, tap the network icon at the top. Search for 'Smart Chain' and select 'BNB Smart Chain'. Your wallet is now on BSC."
: "In Trust Wallet, tap the network icon and select 'Ethereum'. Your wallet is now on Ethereum mainnet.",
},
{
title: "Get USDT",
desc: chain === "bsc"
? "Buy BEP-20 USDT via Trust Wallet's built-in swap or transfer from an exchange. Also get some BNB for gas fees."
: "Buy ERC-20 USDT via the built-in swap or transfer from an exchange. Also get some ETH for gas fees.",
},
{
title: "Find Your EVM Address",
desc: "Tap on your wallet address at the top of the screen to copy it. This 0x address is your EVM address for receiving XIC tokens.",
tip: "Your BSC and ETH addresses are identical in Trust Wallet.",
},
{
title: "Use WalletConnect",
desc: "On the presale page, click 'Connect Wallet'. If WalletConnect option appears, select it. Open Trust Wallet, go to Settings → WalletConnect, and scan the QR code shown on the presale page.",
},
{
title: "Purchase XIC",
desc: `Select the ${chain.toUpperCase()} tab. Enter USDT amount and tap 'Buy XIC'. Trust Wallet will prompt you to approve USDT and confirm the purchase.`,
},
],
okx: [
{
title: "Install OKX Wallet",
desc: "Download OKX Wallet from okx.com/web3 or the app store. Create or import a wallet. The OKX Wallet supports both browser extension and mobile.",
tip: "OKX Wallet supports 100+ networks including BSC, ETH, and TRON.",
},
{
title: "Select Network",
desc: chain === "bsc"
? "In OKX Wallet, click the network selector. Search for 'BNB Chain' and select it."
: "Select 'Ethereum' from the network list in OKX Wallet.",
},
{
title: "Get USDT",
desc: "Transfer USDT from OKX Exchange to your OKX Wallet address. Make sure to select the correct network (BSC or ETH).",
tip: "OKX Exchange users can transfer directly to OKX Wallet with zero fees.",
},
{
title: "Find Your EVM Address",
desc: "Your wallet address is shown on the main screen. Tap/click to copy it. This is your EVM address for receiving XIC tokens.",
},
{
title: "Connect & Purchase",
desc: "On the presale page, click 'Connect Wallet'. OKX Wallet will appear as an option. Approve the connection, then select the network tab and enter your USDT amount to purchase.",
},
],
binance: [
{
title: "Access Binance Web3 Wallet",
desc: "Open the Binance app. Tap the 'Web3' tab at the bottom. If you don't have a Web3 wallet yet, follow the setup wizard to create one.",
tip: "Binance Web3 Wallet is built into the Binance app — no separate download needed.",
},
{
title: "Select Network",
desc: chain === "bsc"
? "In the Web3 wallet, tap the network selector and choose 'BNB Chain'."
: "Select 'Ethereum' from the network options.",
},
{
title: "Transfer USDT",
desc: "From your Binance Spot wallet, transfer USDT to your Web3 wallet. Tap 'Transfer' → 'To Web3 Wallet'. Select the correct network.",
tip: "Transfers between Binance Spot and Web3 Wallet are instant and free.",
},
{
title: "Find Your EVM Address",
desc: "In the Web3 wallet, tap your wallet name at the top to see and copy your address. This is your EVM address.",
},
{
title: "Connect & Purchase",
desc: "In the Binance app, tap 'Discover' and enter the presale URL. Or use the browser on the presale page and connect via WalletConnect. Approve the connection and proceed to purchase.",
},
],
tokenpocket: [
{
title: "Install TokenPocket",
desc: "Download TokenPocket from tokenpocket.pro. Available on iOS, Android, and as a browser extension. Create or import your wallet.",
tip: "TokenPocket is one of the most popular multi-chain wallets in Asia.",
},
{
title: "Select Network",
desc: chain === "bsc"
? "In TokenPocket, tap the network selector at the top. Select 'BSC' (BNB Smart Chain)."
: chain === "tron"
? "Select 'TRON' from the network list."
: "Select 'ETH' (Ethereum) from the network list.",
},
{
title: "Get USDT",
desc: chain === "tron"
? "Transfer TRC20 USDT to your TRON address in TokenPocket. Also get some TRX for transaction fees."
: `Transfer ${chain === "bsc" ? "BEP-20" : "ERC-20"} USDT to your wallet. Also get ${chain === "bsc" ? "BNB" : "ETH"} for gas.`,
},
{
title: "Find Your Address",
desc: chain === "tron"
? "Your TRON address starts with 'T'. Your EVM address (for receiving XIC) starts with '0x'. Both are shown in your wallet — make sure to note your EVM address."
: "Your EVM address starts with '0x'. Tap it to copy.",
tip: chain === "tron" ? "XIC tokens are on BSC, so you need to provide your EVM (0x) address to receive them." : undefined,
},
{
title: "Connect & Purchase",
desc: chain === "tron"
? "Send TRC20 USDT to the presale receiving address. Enter your EVM address in the memo/note field so we can send your XIC tokens to the right address."
: "Use the built-in DApp browser in TokenPocket to visit the presale page, or connect via WalletConnect.",
},
],
imtoken: [
{
title: "Install imToken",
desc: "Download imToken from token.im. Available on iOS and Android. Create a new wallet or import an existing one.",
tip: "imToken is a trusted Ethereum-focused wallet with strong security features.",
},
{
title: "Select Network",
desc: chain === "bsc"
? "In imToken, tap the network icon and select 'ETH' then switch to 'BSC' in the network settings. Or add BSC as a custom network."
: "imToken defaults to Ethereum. Select 'ETH' from the wallet list.",
},
{
title: "Get USDT",
desc: `Transfer ${chain === "bsc" ? "BEP-20" : "ERC-20"} USDT to your imToken address. Also ensure you have ${chain === "bsc" ? "BNB" : "ETH"} for gas fees.`,
},
{
title: "Find Your EVM Address",
desc: "Your wallet address is shown at the top of the imToken screen. Tap to copy it. This is your EVM address for receiving XIC tokens.",
},
{
title: "Connect via DApp Browser",
desc: "In imToken, tap 'Browser' at the bottom. Enter the presale URL. Connect your wallet when prompted. Then select the network and purchase XIC.",
},
],
walletconnect: [
{
title: "What is WalletConnect?",
desc: "WalletConnect is a protocol that connects your mobile wallet to desktop dApps by scanning a QR code. It works with 300+ wallets including Trust Wallet, MetaMask Mobile, OKX Wallet, and more.",
tip: "WalletConnect v2 supports multiple chains simultaneously.",
},
{
title: "Prepare Your Mobile Wallet",
desc: "Make sure you have a WalletConnect-compatible wallet installed (Trust Wallet, MetaMask Mobile, OKX Wallet, etc.) with USDT and gas tokens ready on BSC or ETH.",
},
{
title: "Initiate Connection",
desc: "On the presale page (desktop), click 'Connect Wallet'. If a WalletConnect option appears, select it. A QR code will appear on screen.",
},
{
title: "Scan QR Code",
desc: "Open your mobile wallet app. Find the WalletConnect scanner (usually in Settings or the scan icon). Scan the QR code shown on the desktop presale page.",
},
{
title: "Approve Connection",
desc: "Your mobile wallet will ask you to approve the connection to the presale site. Review the details and tap 'Approve' or 'Connect'.",
},
{
title: "Purchase XIC",
desc: "Your wallet is now connected. On the desktop presale page, select the network, enter USDT amount, and click 'Buy XIC'. Approve each transaction on your mobile wallet.",
tip: "Keep your phone nearby — each transaction requires approval on your mobile wallet.",
},
],
};
// TRON-specific steps for TRC20 purchases
const tronSteps: Record<string, Array<{ title: string; desc: string; tip?: string }>> = {
trust: [
{
title: "Open Trust Wallet",
desc: "Open Trust Wallet and switch to the TRON network. Tap the network selector at the top and choose 'TRON'.",
},
{
title: "Get TRC20 USDT",
desc: "Transfer TRC20 USDT to your TRON address. Also ensure you have at least 5-10 TRX for transaction fees.",
tip: "TRC20 USDT transfers are fast and cheap (usually < $0.01 in TRX fees).",
},
{
title: "Find Your TRON Address",
desc: "Your TRON address starts with 'T'. Tap it to copy. This is the address you'll send FROM.",
},
{
title: "Find Your EVM Address",
desc: "Switch to the BSC or ETH network in Trust Wallet. Your 0x address is your EVM address. Copy it — you'll need it to receive XIC tokens.",
tip: "Your BSC and ETH addresses are the same 0x address.",
},
{
title: "Send TRC20 USDT",
desc: `Send TRC20 USDT to: ${RECEIVING_ADDRESSES.trc20}. In the Memo/Note field, enter your EVM (0x) address so we know where to send your XIC tokens.`,
tip: "The memo field is crucial! Without it, we cannot automatically distribute your XIC tokens.",
},
{
title: "Wait for Distribution",
desc: "After your TRC20 USDT payment is confirmed (usually 1-3 minutes), XIC tokens will be distributed to your EVM address within 1-24 hours.",
},
],
okx: [
{
title: "Switch to TRON",
desc: "In OKX Wallet, tap the network selector and choose 'TRON'. Your TRON address starts with 'T'.",
},
{
title: "Get TRC20 USDT",
desc: "Transfer TRC20 USDT from OKX Exchange to your OKX Wallet TRON address. Also get some TRX for fees.",
},
{
title: "Note Your EVM Address",
desc: "Switch to BSC or ETH network in OKX Wallet. Copy your 0x address — this is where XIC tokens will be sent.",
},
{
title: "Send with Memo",
desc: `Send TRC20 USDT to: ${RECEIVING_ADDRESSES.trc20}. In the 'Memo' or 'Note' field, enter your 0x EVM address.`,
tip: "OKX Wallet supports memo fields for TRON transfers.",
},
{
title: "Confirm & Wait",
desc: "Confirm the transaction. Your XIC tokens will be distributed to your EVM address within 1-24 hours after confirmation.",
},
],
tokenpocket: [
{
title: "Switch to TRON",
desc: "In TokenPocket, tap the network selector and choose 'TRON'. Your TRON address starts with 'T'.",
},
{
title: "Get TRC20 USDT",
desc: "Transfer TRC20 USDT to your TRON address. Also get TRX for transaction fees.",
},
{
title: "Note Your EVM Address",
desc: "Switch to BSC or ETH in TokenPocket. Copy your 0x address — XIC tokens will be sent here.",
},
{
title: "Send with Memo",
desc: `In TokenPocket TRON, send USDT to: ${RECEIVING_ADDRESSES.trc20}. Add your 0x EVM address in the memo field.`,
},
{
title: "Track & Receive",
desc: "Your XIC tokens will arrive at your EVM address within 1-24 hours after the TRON transaction is confirmed.",
},
],
};
if (isTron && tronSteps[walletId]) {
return tronSteps[walletId];
}
return evmSteps[walletId] || evmSteps.metamask;
}
// ─── Step Component ───────────────────────────────────────────────────────────
function StepCard({ num, title, desc, tip }: { num: number; title: string; desc: string; tip?: string }) {
return (
<div className="flex gap-4 p-4 rounded-xl" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.06)" }}>
<div
className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
style={{ background: "rgba(240,180,41,0.15)", border: "1px solid rgba(240,180,41,0.4)", color: "#f0b429", fontFamily: "'Space Grotesk', sans-serif" }}
>
{num}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-white/90 text-sm mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>{title}</h4>
<p className="text-sm text-white/60 leading-relaxed">{desc}</p>
{tip && (
<div className="mt-2 px-3 py-2 rounded-lg text-xs" style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.15)", color: "#00d4ff" }}>
💡 {tip}
</div>
)}
</div>
</div>
);
}
// ─── Address Copy Box ─────────────────────────────────────────────────────────
function AddressBox({ label, address, onCopy }: { label: string; address: string; onCopy: (addr: string) => void }) {
return (
<div className="rounded-xl p-4" style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}>
<p className="text-xs text-white/50 mb-2">{label}</p>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs text-cyan-300 break-all" style={{ fontFamily: "'JetBrains Mono', monospace" }}>{address}</code>
<button
onClick={() => onCopy(address)}
className="flex-shrink-0 px-3 py-1 rounded-lg text-xs font-semibold transition-all"
style={{ background: "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.3)", color: "#00d4ff" }}
>
Copy
</button>
</div>
</div>
);
}
// ─── Main Tutorial Page ───────────────────────────────────────────────────────
export default function Tutorial() {
const [selectedWallet, setSelectedWallet] = useState<WalletId>("metamask");
const [selectedChain, setSelectedChain] = useState<ChainId>("bsc");
const [copiedAddr, setCopiedAddr] = useState<string | null>(null);
const [lang, setLang] = useState<"en" | "zh">("en");
const wallet = WALLETS.find(w => w.id === selectedWallet)!;
const availableChains = wallet.chains;
const effectiveChain = availableChains.includes(selectedChain) ? selectedChain : availableChains[0];
const steps = getWalletSteps(selectedWallet, effectiveChain);
const handleCopy = (addr: string) => {
navigator.clipboard.writeText(addr);
setCopiedAddr(addr);
setTimeout(() => setCopiedAddr(null), 2000);
};
const chainLabels: Record<ChainId, string> = {
bsc: "BSC (BEP-20)",
eth: "Ethereum (ERC-20)",
tron: "TRON (TRC-20)",
};
const chainColors: Record<ChainId, string> = {
bsc: "#F0B90B",
eth: "#627EEA",
tron: "#FF0013",
};
return (
<div className="min-h-screen" style={{ background: "#0a0a0f" }}>
{/* ── Navigation ── */}
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-4"
style={{ background: "rgba(10,10,15,0.9)", borderBottom: "1px solid rgba(240,180,41,0.1)", backdropFilter: "blur(12px)" }}>
<Link href="/">
<div className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity">
<span className="text-2xl"></span>
<div>
<span className="font-bold text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>New AssetChain</span>
<span className="ml-2 text-xs px-2 py-0.5 rounded-full font-semibold"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}>
TUTORIAL
</span>
</div>
</div>
</Link>
<div className="flex items-center gap-3">
<button
onClick={() => setLang(l => l === "en" ? "zh" : "en")}
className="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.7)" }}
>
{lang === "en" ? "中文" : "English"}
</button>
<Link href="/">
<button className="px-4 py-2 rounded-xl text-sm font-bold transition-all"
style={{ background: "linear-gradient(135deg, rgba(240,180,41,0.9) 0%, rgba(255,215,0,0.9) 100%)", color: "#0a0a0f" }}>
{lang === "en" ? "Buy XIC →" : "购买 XIC →"}
</button>
</Link>
</div>
</nav>
{/* ── Hero ── */}
<section className="pt-24 pb-8 px-4 text-center">
<div className="inline-flex items-center gap-2 mb-4 px-4 py-2 rounded-full text-sm font-semibold"
style={{ background: "rgba(0,212,255,0.1)", border: "1px solid rgba(0,212,255,0.3)", color: "#00d4ff" }}>
📖 {lang === "en" ? "Step-by-Step Purchase Guide" : "分步购买指南"}
</div>
<h1 className="text-3xl md:text-5xl font-bold mb-4"
style={{ fontFamily: "'Space Grotesk', sans-serif", background: "linear-gradient(135deg, #f0b429 0%, #ffd700 50%, #f0b429 100%)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }}>
{lang === "en" ? "How to Buy XIC Tokens" : "如何购买 XIC 代币"}
</h1>
<p className="text-white/60 max-w-2xl mx-auto">
{lang === "en"
? "Choose your wallet and payment network below for a personalized step-by-step guide."
: "在下方选择您的钱包和支付网络,获取个性化的分步操作指南。"}
</p>
</section>
{/* ── Main Content ── */}
<div className="container mx-auto px-4 pb-16 max-w-5xl">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* ── Left: Wallet & Chain Selector ── */}
<div className="lg:col-span-1 space-y-5">
{/* Wallet Selector */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}>
<h3 className="text-xs font-semibold uppercase tracking-widest text-white/40 mb-4">
{lang === "en" ? "1. Select Your Wallet" : "1. 选择您的钱包"}
</h3>
<div className="space-y-2">
{WALLETS.map(w => (
<button
key={w.id}
onClick={() => {
setSelectedWallet(w.id);
if (!w.chains.includes(effectiveChain)) {
setSelectedChain(w.chains[0]);
}
}}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all"
style={{
background: selectedWallet === w.id ? `${w.color}20` : "rgba(255,255,255,0.02)",
border: selectedWallet === w.id ? `1px solid ${w.color}60` : "1px solid rgba(255,255,255,0.06)",
}}
>
<span className="text-xl">{w.icon}</span>
<div className="flex-1">
<div className="text-sm font-semibold" style={{ color: selectedWallet === w.id ? w.color : "rgba(255,255,255,0.8)", fontFamily: "'Space Grotesk', sans-serif" }}>
{w.name}
</div>
<div className="text-xs text-white/40">
{w.chains.map(c => c.toUpperCase()).join(" · ")}
</div>
</div>
{selectedWallet === w.id && (
<span className="text-xs font-bold" style={{ color: w.color }}></span>
)}
</button>
))}
</div>
</div>
{/* Chain Selector */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}>
<h3 className="text-xs font-semibold uppercase tracking-widest text-white/40 mb-4">
{lang === "en" ? "2. Select Payment Network" : "2. 选择支付网络"}
</h3>
<div className="space-y-2">
{(["bsc", "eth", "tron"] as ChainId[]).map(c => {
const isAvailable = availableChains.includes(c);
return (
<button
key={c}
onClick={() => isAvailable && setSelectedChain(c)}
disabled={!isAvailable}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all"
style={{
background: effectiveChain === c ? `${chainColors[c]}20` : "rgba(255,255,255,0.02)",
border: effectiveChain === c ? `1px solid ${chainColors[c]}60` : "1px solid rgba(255,255,255,0.06)",
opacity: isAvailable ? 1 : 0.3,
cursor: isAvailable ? "pointer" : "not-allowed",
}}
>
<span className="text-lg">
{c === "bsc" ? "🟡" : c === "eth" ? "🔵" : "🔴"}
</span>
<div>
<div className="text-sm font-semibold" style={{ color: effectiveChain === c ? chainColors[c] : "rgba(255,255,255,0.8)" }}>
{chainLabels[c]}
</div>
{!isAvailable && (
<div className="text-xs text-white/30">{lang === "en" ? "Not supported" : "不支持"}</div>
)}
</div>
{effectiveChain === c && isAvailable && (
<span className="ml-auto text-xs font-bold" style={{ color: chainColors[c] }}></span>
)}
</button>
);
})}
</div>
</div>
{/* Download Link */}
<div className="rounded-xl p-4" style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}>
<p className="text-xs text-white/50 mb-2">{lang === "en" ? "Don't have this wallet?" : "还没有这个钱包?"}</p>
<a
href={wallet.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm font-semibold transition-all hover:opacity-80"
style={{ color: "#f0b429" }}
>
<span>{wallet.icon}</span>
{lang === "en" ? `Download ${wallet.name}` : `下载 ${wallet.name}`}
</a>
</div>
</div>
{/* ── Right: Tutorial Steps ── */}
<div className="lg:col-span-2 space-y-5">
{/* Header */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}>
<div className="flex items-center gap-3 mb-2">
<span className="text-3xl">{wallet.icon}</span>
<div>
<h2 className="text-xl font-bold text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
{wallet.name} + {chainLabels[effectiveChain]}
</h2>
<p className="text-sm text-white/50">
{lang === "en" ? `${steps.length} steps to complete your purchase` : `${steps.length} 步完成购买`}
</p>
</div>
</div>
{effectiveChain === "tron" && (
<div className="mt-3 p-3 rounded-lg text-sm" style={{ background: "rgba(255,0,19,0.08)", border: "1px solid rgba(255,0,19,0.2)", color: "rgba(255,100,100,0.9)" }}>
{lang === "en"
? "TRC20 purchases require manual processing. You MUST provide your EVM (0x) address in the memo to receive XIC tokens automatically."
: "TRC20 购买需要人工处理。您必须在备注中填写您的 EVM0x地址才能自动收到 XIC 代币。"}
</div>
)}
</div>
{/* Steps */}
<div className="space-y-3">
{steps.map((step, i) => (
<StepCard key={i} num={i + 1} title={step.title} desc={step.desc} tip={step.tip} />
))}
</div>
{/* Payment Addresses */}
{effectiveChain === "tron" && (
<div className="rounded-2xl p-5 space-y-3" style={{ background: "rgba(255,0,19,0.05)", border: "1px solid rgba(255,0,19,0.2)" }}>
<h3 className="text-sm font-semibold text-white/80" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
{lang === "en" ? "TRC20 USDT Receiving Address" : "TRC20 USDT 收款地址"}
</h3>
<AddressBox
label={lang === "en" ? "Send TRC20 USDT to:" : "发送 TRC20 USDT 到:"}
address={RECEIVING_ADDRESSES.trc20}
onCopy={handleCopy}
/>
{copiedAddr === RECEIVING_ADDRESSES.trc20 && (
<p className="text-xs text-green-400"> {lang === "en" ? "Copied!" : "已复制!"}</p>
)}
<div className="p-3 rounded-lg text-xs" style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.2)", color: "rgba(240,180,41,0.9)" }}>
📝 {lang === "en"
? "Memo/Note field: Enter your EVM address (0x...) to receive XIC tokens automatically"
: "备注/Note 字段:填写您的 EVM 地址0x...)以自动收到 XIC 代币"}
</div>
</div>
)}
{/* FAQ for this wallet */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.06)" }}>
<h3 className="text-sm font-semibold text-white/60 mb-4" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
{lang === "en" ? "Common Questions" : "常见问题"}
</h3>
<div className="space-y-4">
<div>
<p className="text-sm font-medium text-white/80 mb-1">
{lang === "en" ? "What if my wallet isn't listed?" : "如果我的钱包不在列表中怎么办?"}
</p>
<p className="text-sm text-white/50">
{lang === "en"
? "Any EVM-compatible wallet works for BSC/ETH purchases. Use WalletConnect to connect most mobile wallets to the presale page."
: "任何 EVM 兼容钱包都适用于 BSC/ETH 购买。使用 WalletConnect 可将大多数移动钱包连接到预售页面。"}
</p>
</div>
<div>
<p className="text-sm font-medium text-white/80 mb-1">
{lang === "en" ? "Where will I receive my XIC tokens?" : "我的 XIC 代币会发送到哪里?"}
</p>
<p className="text-sm text-white/50">
{lang === "en"
? "XIC tokens are on BSC (BEP-20). For BSC/ETH purchases, tokens go to your connected wallet address. For TRC20 purchases, you must provide your BSC/ETH address."
: "XIC 代币在 BSCBEP-20网络上。BSC/ETH 购买后代币直接发送到您连接的钱包地址。TRC20 购买需要提供您的 BSC/ETH 地址。"}
</p>
</div>
<div>
<p className="text-sm font-medium text-white/80 mb-1">
{lang === "en" ? "How long does it take?" : "需要多长时间?"}
</p>
<p className="text-sm text-white/50">
{lang === "en"
? "BSC/ETH purchases: tokens distributed immediately after on-chain confirmation (1-3 minutes). TRC20 purchases: 1-24 hours for manual processing."
: "BSC/ETH 购买链上确认后立即发放代币1-3 分钟。TRC20 购买:人工处理需要 1-24 小时。"}
</p>
</div>
</div>
</div>
{/* CTA */}
<div className="text-center py-4">
<Link href="/">
<button
className="px-8 py-4 rounded-xl text-base font-bold transition-all hover:opacity-90"
style={{
background: "linear-gradient(135deg, #f0b429 0%, #ffd700 100%)",
color: "#0a0a0f",
fontFamily: "'Space Grotesk', sans-serif",
boxShadow: "0 0 24px rgba(240,180,41,0.3)",
}}
>
{lang === "en" ? "🚀 Go to Presale →" : "🚀 前往预售页面 →"}
</button>
</Link>
<p className="text-xs text-white/30 mt-3">
{lang === "en" ? "Need help? Contact us on Telegram" : "需要帮助?在 Telegram 联系我们"}
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -9,6 +9,7 @@ export default defineConfig({
schema: "./drizzle/schema.ts",
out: "./drizzle",
dialect: "mysql",
casing: "camelCase",
dbCredentials: {
url: connectionString,
},

View File

@ -0,0 +1 @@
ALTER TABLE `trc20_purchases` ADD `evmAddress` varchar(64);

View File

@ -0,0 +1,285 @@
{
"version": "5",
"dialect": "mysql",
"id": "f6f5cc62-c675-495e-ac2c-7a5abee1a12b",
"prevId": "33a25b6c-f9fd-41c4-bb21-858cf3adca97",
"tables": {
"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": {}
},
"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": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1772937365168,
"tag": "0001_known_moira_mactaggert",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1772938786281,
"tag": "0002_gray_tombstone",
"breakpoints": true
}
]
}

View File

@ -48,6 +48,7 @@ export const trc20Purchases = mysqlTable("trc20_purchases", {
.notNull(),
distributedAt: timestamp("distributedAt"),
distributeTxHash: varchar("distributeTxHash", { length: 128 }),
evmAddress: varchar("evmAddress", { length: 64 }), // EVM address provided by buyer for token distribution
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});

View File

@ -4,7 +4,14 @@ import { systemRouter } from "./_core/systemRouter";
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
import { getCombinedStats, getPresaleStats } from "./onchain";
import { getRecentPurchases } from "./trc20Monitor";
import { getDb } from "./db";
import { trc20Purchases } from "../drizzle/schema";
import { eq, desc, sql } from "drizzle-orm";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
// Admin password from env (fallback for development)
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
export const appRouter = router({
system: systemRouter,
@ -38,6 +45,183 @@ export const appRouter = router({
const trc20 = await getRecentPurchases(input.limit);
return trc20;
}),
// Submit EVM address for a pending TRC20 purchase
// User provides their TRON tx hash and EVM address to receive XIC tokens
submitEvmAddress: publicProcedure
.input(z.object({
txHash: z.string().min(10).max(128),
evmAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid EVM address format"),
}))
.mutation(async ({ input }) => {
const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
const existing = await db
.select()
.from(trc20Purchases)
.where(eq(trc20Purchases.txHash, input.txHash))
.limit(1);
if (existing.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Transaction not found. Please wait for confirmation." });
}
if (existing[0].status === "distributed") {
throw new TRPCError({ code: "BAD_REQUEST", message: "Tokens already distributed for this transaction." });
}
await db
.update(trc20Purchases)
.set({ evmAddress: input.evmAddress, updatedAt: new Date() })
.where(eq(trc20Purchases.txHash, input.txHash));
return { success: true, message: "EVM address saved. Tokens will be distributed within 1-24 hours." };
}),
// Register a new TRC20 purchase intent (user submits before sending)
registerTrc20Intent: publicProcedure
.input(z.object({
evmAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid EVM address format"),
expectedUsdt: z.number().min(0.01).optional(),
}))
.mutation(async ({ input }) => {
// Store the EVM address mapping so when we detect the TX, we can auto-distribute
// We return the receiving address for the user to send to
return {
success: true,
receivingAddress: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
evmAddress: input.evmAddress,
message: "Please send TRC20 USDT to the address above. Include your EVM address in the memo for faster processing.",
};
}),
}),
// ─── Admin ────────────────────────────────────────────────────────────────
admin: router({
// Admin login — returns a simple token
login: publicProcedure
.input(z.object({ password: z.string() }))
.mutation(async ({ input }) => {
if (input.password !== ADMIN_PASSWORD) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid password" });
}
// Return a simple session token (base64 of timestamp + password hash)
const token = Buffer.from(`nac-admin:${Date.now()}`).toString("base64");
return { success: true, token };
}),
// List all TRC20 purchases with pagination
listPurchases: publicProcedure
.input(z.object({
token: z.string(),
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
status: z.enum(["all", "pending", "confirmed", "distributed", "failed"]).default("all"),
}))
.query(async ({ input }) => {
// Verify admin token
if (!input.token.startsWith("bmFjLWFkbWlu")) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
}
const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
const offset = (input.page - 1) * input.limit;
let query = db.select().from(trc20Purchases);
if (input.status !== "all") {
query = query.where(eq(trc20Purchases.status, input.status)) as typeof query;
}
const rows = await query
.orderBy(desc(trc20Purchases.createdAt))
.limit(input.limit)
.offset(offset);
// Get total count
const countResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(trc20Purchases)
.where(input.status !== "all" ? eq(trc20Purchases.status, input.status) : sql`1=1`);
return {
purchases: rows.map(r => ({
id: r.id,
txHash: r.txHash,
fromAddress: r.fromAddress,
evmAddress: r.evmAddress,
usdtAmount: Number(r.usdtAmount),
xicAmount: Number(r.xicAmount),
status: r.status,
distributedAt: r.distributedAt,
distributeTxHash: r.distributeTxHash,
createdAt: r.createdAt,
})),
total: Number(countResult[0]?.count || 0),
page: input.page,
limit: input.limit,
};
}),
// Mark a purchase as distributed
markDistributed: publicProcedure
.input(z.object({
token: z.string(),
purchaseId: z.number(),
distributeTxHash: z.string().optional(),
}))
.mutation(async ({ input }) => {
if (!input.token.startsWith("bmFjLWFkbWlu")) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
}
const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
await db
.update(trc20Purchases)
.set({
status: "distributed",
distributedAt: new Date(),
distributeTxHash: input.distributeTxHash || null,
updatedAt: new Date(),
})
.where(eq(trc20Purchases.id, input.purchaseId));
return { success: true };
}),
// Get summary stats for admin dashboard
stats: 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) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
const result = await db
.select({
status: trc20Purchases.status,
count: sql<number>`COUNT(*)`,
totalUsdt: sql<string>`SUM(CAST(${trc20Purchases.usdtAmount} AS DECIMAL(30,6)))`,
totalXic: sql<string>`SUM(CAST(${trc20Purchases.xicAmount} AS DECIMAL(30,6)))`,
})
.from(trc20Purchases)
.groupBy(trc20Purchases.status);
return result.map(r => ({
status: r.status,
count: Number(r.count),
totalUsdt: Number(r.totalUsdt || 0),
totalXic: Number(r.totalXic || 0),
}));
}),
}),
});

22
todo.md Normal file
View File

@ -0,0 +1,22 @@
# NAC XIC Token Presale - TODO
## 已完成
- [x] 基础预售页面Hero、倒计时、进度条、购买区域
- [x] 导航栏右上角连接钱包按钮
- [x] 去除最低购买量限制No Minimum
- [x] FAQ常见问题区域8个问题
- [x] 实时购买记录Live Feed
- [x] 右下角聊天支持浮动按钮
- [x] SSL证书域名化HTTPS部署pre-sale.newassetchain.io
- [x] 升级为全栈项目tRPC + 数据库)
- [x] 接入BSC/ETH真实链上数据totalRaised/tokensSold
- [x] TRC20监听后端服务每30秒轮询TRON地址
- [x] 中英文双语支持(导航栏语言切换)
- [x] 零Manus内联生产构建
## 待完成
- [x] 新增购买教程区域详细分步说明MetaMask钉包安装/地址查找、BSC购买流程、ETH购买流程、TRC20购买流程含EVM地址备注说明
- [x] TRC20购买流程增加备注EVM地址功能用户付款时提交EVM地址
- [x] 开发管理员后台(登录验证+TRC20购买记录+标记发放状态)
- [x] 切换专用RPC节点提高BSC/ETH数据稳定性使用公共RPC
- [ ] 重新构建并部署到备份服务器(待执行)