Checkpoint: 新增功能:1) 购买教程页面(/tutorial) - 支持7种钱包+3种网络的分步指南,中英双语;2) TRC20面板EVM地址输入 - 用户提交BSC/ETH地址用于接收XIC代币;3) 管理员后台(/admin) - 密码登录、TRC20购买记录查看、标记发放状态、CSV导出;4) 导航栏添加Tutorial链接;5) 数据库新增evmAddress列
This commit is contained in:
parent
80444bfdc6
commit
7acc5d4a0f
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -5,22 +5,35 @@ import { Route, Switch } from "wouter";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
|
import Tutorial from "./pages/Tutorial";
|
||||||
|
import Admin from "./pages/Admin";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
// make sure to consider if you need authentication for certain routes
|
// make sure to consider if you need authentication for certain routes
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
|
<Route path={"/tutorial"} component={Tutorial} />
|
||||||
|
<Route path={"/admin"} component={Admin} />
|
||||||
<Route path={"/404"} component={NotFound} />
|
<Route path={"/404"} component={NotFound} />
|
||||||
|
{/* Final fallback route */}
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ThemeProvider defaultTheme="dark">
|
<ThemeProvider
|
||||||
|
defaultTheme="dark"
|
||||||
|
// switchable
|
||||||
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Router />
|
<Router />
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Link } from "wouter";
|
||||||
import { useWallet } from "@/hooks/useWallet";
|
import { useWallet } from "@/hooks/useWallet";
|
||||||
import { usePresale } from "@/hooks/usePresale";
|
import { usePresale } from "@/hooks/usePresale";
|
||||||
import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts";
|
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 { t } = useTranslation(lang);
|
||||||
const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice;
|
const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice;
|
||||||
const [copied, setCopied] = useState(false);
|
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 = () => {
|
const copyAddress = () => {
|
||||||
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
|
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
|
||||||
|
|
@ -115,8 +129,66 @@ function TRC20Panel({ usdtAmount, lang }: { usdtAmount: number; lang: Lang }) {
|
||||||
setTimeout(() => setCopied(false), 2000);
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
<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>
|
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
|
||||||
<div
|
<div
|
||||||
|
|
@ -137,17 +209,22 @@ function TRC20Panel({ usdtAmount, lang }: { usdtAmount: number; lang: Lang }) {
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<StepBadge num={1} text={
|
<StepBadge num={1} text={
|
||||||
|
lang === "zh"
|
||||||
|
? `在上方填写您的EVM地址并保存`
|
||||||
|
: `Enter and save your EVM address above`
|
||||||
|
} />
|
||||||
|
<StepBadge num={2} text={
|
||||||
lang === "zh"
|
lang === "zh"
|
||||||
? `发送 ${usdtAmount > 0 ? usdtAmount.toFixed(2) + " USDT" : "任意数量 USDT"}(TRC20)到上方地址`
|
? `发送 ${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")}`
|
: `${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={`${t("trc20_step2")} ${PRESALE_CONFIG.trc20Memo} ${t("trc20_step2b")}`} />
|
||||||
<StepBadge num={3} text={
|
<StepBadge num={4} text={
|
||||||
lang === "zh"
|
lang === "zh"
|
||||||
? (usdtAmount > 0 ? `${t("trc20_step3")} ${formatNumber(tokenAmount)} ${t("trc20_step3b")}` : t("trc20_step3_any"))
|
? (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"))
|
: (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>
|
||||||
|
|
||||||
<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)" }}>
|
<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://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://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>
|
<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} />
|
<LangToggle lang={lang} setLang={setLang} />
|
||||||
<NavWalletButton lang={lang} />
|
<NavWalletButton lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 购买需要人工处理。您必须在备注中填写您的 EVM(0x)地址才能自动收到 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 代币在 BSC(BEP-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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ export default defineConfig({
|
||||||
schema: "./drizzle/schema.ts",
|
schema: "./drizzle/schema.ts",
|
||||||
out: "./drizzle",
|
out: "./drizzle",
|
||||||
dialect: "mysql",
|
dialect: "mysql",
|
||||||
|
casing: "camelCase",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: connectionString,
|
url: connectionString,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `trc20_purchases` ADD `evmAddress` varchar(64);
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,13 @@
|
||||||
"when": 1772937365168,
|
"when": 1772937365168,
|
||||||
"tag": "0001_known_moira_mactaggert",
|
"tag": "0001_known_moira_mactaggert",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1772938786281,
|
||||||
|
"tag": "0002_gray_tombstone",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ export const trc20Purchases = mysqlTable("trc20_purchases", {
|
||||||
.notNull(),
|
.notNull(),
|
||||||
distributedAt: timestamp("distributedAt"),
|
distributedAt: timestamp("distributedAt"),
|
||||||
distributeTxHash: varchar("distributeTxHash", { length: 128 }),
|
distributeTxHash: varchar("distributeTxHash", { length: 128 }),
|
||||||
|
evmAddress: varchar("evmAddress", { length: 64 }), // EVM address provided by buyer for token distribution
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@ import { systemRouter } from "./_core/systemRouter";
|
||||||
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
|
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
|
||||||
import { getCombinedStats, getPresaleStats } from "./onchain";
|
import { getCombinedStats, getPresaleStats } from "./onchain";
|
||||||
import { getRecentPurchases } from "./trc20Monitor";
|
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 { 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({
|
export const appRouter = router({
|
||||||
system: systemRouter,
|
system: systemRouter,
|
||||||
|
|
@ -38,6 +45,183 @@ export const appRouter = router({
|
||||||
const trc20 = await getRecentPurchases(input.limit);
|
const trc20 = await getRecentPurchases(input.limit);
|
||||||
return trc20;
|
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),
|
||||||
|
}));
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
- [ ] 重新构建并部署到备份服务器(待执行)
|
||||||
Loading…
Reference in New Issue