Compare commits

...

No commits in common. "08be1173cb46d76949cf8fa9669cad2006fa1bee" and "main" have entirely different histories.

75 changed files with 17227 additions and 31330 deletions

110
.gitignore vendored
View File

@ -1,6 +1,110 @@
node_modules/
# Dependencies
**/node_modules
.pnpm-store/
# Build outputs
dist/
dist.bak*/
*.log
build/
*.dist
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
*.bak
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Database
*.db
*.sqlite
*.sqlite3
# Webdev artifacts (checkpoint zips, migrations, etc.)
.webdev/

View File

@ -0,0 +1,74 @@
{
"query": "ALTER TABLE bridge_intents MODIFY COLUMN senderAddress VARCHAR(64) NULL;\nDESCRIBE bridge_intents;",
"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 ALTER TABLE bridge_intents MODIFY COLUMN senderAddress VARCHAR(64) NULL;\nDESCRIBE bridge_intents;",
"rows": [
{
"Field": "id",
"Type": "int",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "fromChainId",
"Type": "int",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "senderAddress",
"Type": "varchar(64)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "xicReceiveAddress",
"Type": "varchar(64)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "expectedUsdt",
"Type": "decimal(20,6)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "matched",
"Type": "tinyint(1)",
"Null": "NO",
"Key": "",
"Default": "0",
"Extra": ""
},
{
"Field": "matchedOrderId",
"Type": "int",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\nfromChainId\tint\tNO\t\tNULL\t\nsenderAddress\tvarchar(64)\tYES\t\tNULL\t\nxicReceiveAddress\tvarchar(64)\tNO\t\tNULL\t\nexpectedUsdt\tdecimal(20,6)\tYES\t\tNULL\t\nmatched\ttinyint(1)\tNO\t\t0\t\nmatchedOrderId\tint\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\n",
"stderr": "",
"execution_time_ms": 1992
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,351 +0,0 @@
/**
* i18n translations for NAC XIC Presale
* Supports: English (en) | Chinese Simplified (zh)
*/
export type Lang = "en" | "zh";
export const translations = {
en: {
// Nav
nav_website: "Website",
nav_explorer: "Explorer",
nav_docs: "Docs",
nav_connect: "Connect Wallet",
nav_connecting: "Connecting...",
nav_disconnect: "Disconnect",
nav_connected: "Connected Wallet",
// Hero
hero_badge: "Presale is LIVE",
hero_title: "XIC Token Presale",
hero_subtitle:
"New AssetChain — The next-generation RWA native blockchain with AI-native compliance, CBPP consensus, and Charter smart contracts.",
hero_price: "$0.02 per XIC",
hero_supply: "100B Total Supply",
hero_networks: "BSC · ETH · TRC20",
hero_no_min: "No Minimum Purchase",
// Stats
stats_ends_in: "Presale Ends In",
stats_days: "Days",
stats_hours: "Hours",
stats_mins: "Mins",
stats_secs: "Secs",
stats_raised: "Funds Raised",
stats_raised_label: "Raised",
stats_hard_cap: "Hard Cap",
stats_tokens_sold: "Tokens Sold",
stats_participants: "Participants",
stats_wallets: "Wallets",
stats_token_price: "Token Price",
stats_listing: "Listing Price",
stats_target: "Target",
stats_live_feed: "Live Purchase Feed",
stats_live: "Live",
// Token Details
token_details: "Token Details",
token_name: "Name",
token_symbol: "Symbol",
token_network: "Network",
token_decimals: "Decimals",
token_supply: "Total Supply",
token_view_contract: "View Token Contract →",
// Purchase
buy_title: "Buy XIC Tokens",
buy_subtitle: "1 XIC =",
buy_no_min: "No Minimum",
buy_select_network: "Select Network",
buy_usdt_amount: "USDT Amount",
buy_usdt_trc20: "USDT Amount (TRC20)",
buy_placeholder: "Enter any USDT amount",
buy_you_receive: "You receive",
buy_price_per: "Price per token",
buy_step1: "Approve USDT",
buy_step2: "Confirm Purchase",
buy_btn: "Buy",
buy_approving: "Approving USDT...",
buy_approved: "Approved! Buying...",
buy_processing: "Processing...",
buy_max: "Max:",
buy_no_min_max: "No minimum · Max:",
buy_success_title: "Purchase Successful!",
buy_success_msg: "You received",
buy_success_tokens: "XIC tokens",
buy_view_explorer: "View on Explorer →",
buy_more: "Buy More",
buy_balance: "Balance:",
buy_wrong_network: "Wrong Network",
buy_wrong_msg: "Please switch to",
buy_switch: "Switch to",
buy_connect_msg: "Connect your wallet to purchase XIC tokens with USDT",
buy_connect_btn: "Connect Wallet",
buy_connect_hint: "Supports MetaMask, Trust Wallet, and all EVM-compatible wallets",
buy_contracts: "Verified Presale Contracts",
buy_bsc_contract: "BSC Contract ↗",
buy_eth_contract: "ETH Contract ↗",
// TRC20
trc20_send_to: "Send TRC20 USDT to this address:",
trc20_copy: "Copy Address",
trc20_copied: "✓ Copied!",
trc20_step1: "Send",
trc20_step1b: "USDT (TRC20) to the address above",
trc20_step1_any: "any amount of USDT",
trc20_step2: "Include memo:",
trc20_step2b: "(optional but recommended)",
trc20_step3: "You will receive",
trc20_step3b: "XIC tokens after confirmation (1-24h)",
trc20_step3_any: "You will receive XIC tokens proportional to your USDT amount after confirmation (1-24h)",
trc20_step4: "Contact support with your TX hash if tokens are not received within 24 hours",
trc20_warning: "⚠️ Only send USDT on the TRON network (TRC20). Sending other tokens or using a different network will result in permanent loss.",
// Why NAC
why_rwa_title: "Native RWA Chain",
why_rwa_desc: "Purpose-built for Real World Asset tokenization with AI-native compliance",
why_cbpp_title: "CBPP Consensus",
why_cbpp_desc: "Constitutional Block Production Protocol — next-gen consensus beyond PoS/PoW",
why_charter_title: "Charter Contracts",
why_charter_desc: "NAC-native smart contract language with built-in regulatory compliance",
// FAQ
faq_title: "Frequently Asked Questions",
faq_subtitle: "Everything you need to know about the XIC Token presale and New AssetChain ecosystem.",
faq_still: "Still have questions?",
faq_ask: "Ask on Telegram",
faq: [
{
q: "What is XIC Token?",
a: "XIC is the native utility token of New AssetChain (NAC), a purpose-built RWA (Real World Asset) blockchain featuring AI-native compliance, CBPP consensus, and Charter smart contracts. XIC powers governance, transaction fees, and staking within the NAC ecosystem.",
},
{
q: "What is the presale price?",
a: "The presale price is $0.02 USD per XIC token. The projected listing price is $0.10 USD, representing a 5x potential return for presale participants.",
},
{
q: "Which payment methods are supported?",
a: "We accept USDT on three networks: BSC (BEP-20), Ethereum (ERC-20), and TRON (TRC-20). For BSC and ETH, connect your MetaMask or compatible EVM wallet. For TRC-20, send USDT directly to our receiving address.",
},
{
q: "Is there a minimum purchase amount?",
a: "No, there is no minimum purchase amount. You can buy any amount of XIC tokens starting from any USDT value. The maximum single purchase is $50,000 USDT.",
},
{
q: "When will I receive my XIC tokens?",
a: "For BSC and ETH purchases, tokens are distributed immediately after the transaction is confirmed on-chain. For TRC-20 manual transfers, token distribution occurs within 124 hours after confirmation.",
},
{
q: "When will XIC be listed on exchanges?",
a: "XIC is planned for listing on major centralized and decentralized exchanges following the presale completion. The target listing price is $0.10 USD. Specific exchange announcements will be made through our official Telegram and Twitter channels.",
},
{
q: "Is the presale contract audited?",
a: "Yes. Both the BSC and ETH presale contracts are verified on their respective block explorers (BscScan and Etherscan). You can view the contract source code and transaction history directly on-chain.",
},
{
q: "What is NAC's technology advantage?",
a: "NAC is a fully independent blockchain — not a fork or derivative of Ethereum or any existing chain. It features NVM (NAC Virtual Machine), CBPP consensus protocol, Charter smart contract language, CSNP network protocol, and built-in AI compliance for RWA tokenization.",
},
],
// Support
support_title: "NAC Support",
support_online: "Online",
support_msg: "👋 Hi! Need help with the XIC presale? Our team is available 24/7 to assist you.",
support_telegram: "Chat on Telegram",
support_email: "Email Support",
support_response: "Avg. response time: < 2 hours",
// Footer
footer_risk: "This presale involves risk. Only invest what you can afford to lose. XIC tokens are not available to US persons or residents of restricted jurisdictions.",
footer_website: "Website",
footer_explorer: "Explorer",
footer_telegram: "Telegram",
footer_twitter: "Twitter",
// Loading
loading_stats: "Loading on-chain data...",
stats_live_data: "Live On-Chain Data",
stats_cached: "Cached",
stats_updated: "Updated",
},
zh: {
// Nav
nav_website: "官网",
nav_explorer: "浏览器",
nav_docs: "文档",
nav_connect: "连接钱包",
nav_connecting: "连接中...",
nav_disconnect: "断开连接",
nav_connected: "已连接钱包",
// Hero
hero_badge: "预售进行中",
hero_title: "XIC 代币预售",
hero_subtitle:
"New AssetChain — 下一代 RWA 原生公链,内置 AI 合规审批、CBPP 共识协议与 Charter 智能合约语言。",
hero_price: "$0.02 / XIC",
hero_supply: "总供应量 1000亿",
hero_networks: "BSC · ETH · TRC20",
hero_no_min: "无最低购买限制",
// Stats
stats_ends_in: "预售结束倒计时",
stats_days: "天",
stats_hours: "时",
stats_mins: "分",
stats_secs: "秒",
stats_raised: "募资进度",
stats_raised_label: "已募资",
stats_hard_cap: "硬顶",
stats_tokens_sold: "已售代币",
stats_participants: "参与人数",
stats_wallets: "钱包",
stats_token_price: "代币价格",
stats_listing: "上市目标价",
stats_target: "目标",
stats_live_feed: "实时购买记录",
stats_live: "实时",
// Token Details
token_details: "代币信息",
token_name: "名称",
token_symbol: "符号",
token_network: "网络",
token_decimals: "精度",
token_supply: "总供应量",
token_view_contract: "查看代币合约 →",
// Purchase
buy_title: "购买 XIC 代币",
buy_subtitle: "1 XIC =",
buy_no_min: "无最低限制",
buy_select_network: "选择网络",
buy_usdt_amount: "USDT 数量",
buy_usdt_trc20: "USDT 数量TRC20",
buy_placeholder: "输入任意 USDT 金额",
buy_you_receive: "您将获得",
buy_price_per: "单价",
buy_step1: "授权 USDT",
buy_step2: "确认购买",
buy_btn: "购买",
buy_approving: "授权中...",
buy_approved: "授权成功!购买中...",
buy_processing: "处理中...",
buy_max: "最大:",
buy_no_min_max: "无最低限制 · 最大:",
buy_success_title: "购买成功!",
buy_success_msg: "您已获得",
buy_success_tokens: "枚 XIC 代币",
buy_view_explorer: "在浏览器中查看 →",
buy_more: "继续购买",
buy_balance: "余额:",
buy_wrong_network: "网络错误",
buy_wrong_msg: "请切换到",
buy_switch: "切换到",
buy_connect_msg: "连接钱包后即可使用 USDT 购买 XIC 代币",
buy_connect_btn: "连接钱包",
buy_connect_hint: "支持 MetaMask、Trust Wallet 及所有 EVM 兼容钱包",
buy_contracts: "已验证预售合约",
buy_bsc_contract: "BSC 合约 ↗",
buy_eth_contract: "ETH 合约 ↗",
// TRC20
trc20_send_to: "请发送 TRC20 USDT 到以下地址:",
trc20_copy: "复制地址",
trc20_copied: "✓ 已复制!",
trc20_step1: "发送",
trc20_step1b: "USDTTRC20到上方地址",
trc20_step1_any: "任意数量 USDT",
trc20_step2: "备注填写:",
trc20_step2b: "(可选,建议填写)",
trc20_step3: "您将在确认后1-24小时内收到",
trc20_step3b: "枚 XIC 代币",
trc20_step3_any: "您将在确认后1-24小时内按比例收到 XIC 代币",
trc20_step4: "如24小时内未收到代币请携带交易哈希联系客服",
trc20_warning: "⚠️ 请仅在 TRON 网络TRC20上发送 USDT。发送其他代币或使用其他网络将导致永久损失。",
// Why NAC
why_rwa_title: "原生 RWA 公链",
why_rwa_desc: "专为现实世界资产代币化而生,内置 AI 合规审批",
why_cbpp_title: "CBPP 共识协议",
why_cbpp_desc: "宪政区块生产协议 — 超越 PoS/PoW 的下一代共识",
why_charter_title: "Charter 智能合约",
why_charter_desc: "NAC 原生智能合约语言,内置监管合规机制",
// FAQ
faq_title: "常见问题",
faq_subtitle: "关于 XIC 代币预售和 New AssetChain 生态系统的一切您需要了解的信息。",
faq_still: "还有其他问题?",
faq_ask: "在 Telegram 提问",
faq: [
{
q: "XIC 代币是什么?",
a: "XIC 是 New AssetChainNAC的原生功能代币NAC 是专为 RWA现实世界资产而生的区块链具备 AI 原生合规、CBPP 共识和 Charter 智能合约。XIC 用于治理、交易手续费和生态质押。",
},
{
q: "预售价格是多少?",
a: "预售价格为每枚 XIC 0.02 美元。预计上市价格为 0.10 美元,预售参与者可获得 5 倍潜在收益。",
},
{
q: "支持哪些支付方式?",
a: "我们接受三个网络上的 USDTBSCBEP-20、以太坊ERC-20和 TRONTRC-20。BSC 和 ETH 需连接 MetaMask 或兼容 EVM 的钱包TRC-20 请直接向我们的收款地址转账。",
},
{
q: "有最低购买金额吗?",
a: "没有最低购买金额限制。您可以购买任意数量的 XIC 代币,单笔最高购买额为 50,000 USDT。",
},
{
q: "何时收到 XIC 代币?",
a: "BSC 和 ETH 购买链上确认后立即发放。TRC-20 手动转账:确认后 1-24 小时内发放。",
},
{
q: "XIC 何时上市交易所?",
a: "预售完成后XIC 计划在主要中心化和去中心化交易所上市,目标上市价格为 0.10 美元。具体交易所公告将通过官方 Telegram 和 Twitter 发布。",
},
{
q: "预售合约是否经过审计?",
a: "是的。BSC 和 ETH 预售合约均已在各自的区块链浏览器BscScan 和 Etherscan上验证。您可以直接在链上查看合约源代码和交易历史。",
},
{
q: "NAC 的技术优势是什么?",
a: "NAC 是完全独立的区块链,不是以太坊或任何现有链的分叉或衍生。它具备 NVMNAC 虚拟机、CBPP 共识协议、Charter 智能合约语言、CSNP 网络协议,以及用于 RWA 代币化的内置 AI 合规。",
},
],
// Support
support_title: "NAC 客服",
support_online: "在线",
support_msg: "👋 您好!需要 XIC 预售帮助吗?我们的团队 24/7 全天候为您服务。",
support_telegram: "Telegram 咨询",
support_email: "邮件支持",
support_response: "平均响应时间:< 2 小时",
// Footer
footer_risk: "参与预售存在风险请仅投入您能承受损失的资金。XIC 代币不向美国公民及受限制司法管辖区居民提供。",
footer_website: "官网",
footer_explorer: "浏览器",
footer_telegram: "Telegram",
footer_twitter: "Twitter",
// Loading
loading_stats: "正在加载链上数据...",
stats_live_data: "实时链上数据",
stats_cached: "缓存",
stats_updated: "更新于",
},
} as const;
export type TranslationKey = keyof typeof translations.en;
export function useTranslation(lang: Lang) {
const t = translations[lang];
return {
t: (key: TranslationKey) => (t as Record<string, unknown>)[key] as string,
faq: t.faq,
lang,
};
}

View File

@ -1,20 +1,49 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import { Route, Switch, useLocation } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
import Tutorial from "./pages/Tutorial";
import Admin from "./pages/Admin";
import Bridge from "./pages/Bridge";
import { useEffect } from "react";
// 跨链桥专属域名列表 — 访问这些域名时直接显示 Bridge 页面
const BRIDGE_HOSTNAMES = ["trc-ico.newassetchain.io"];
function BridgeRedirect() {
const [, setLocation] = useLocation();
useEffect(() => {
setLocation("/bridge", { replace: true });
}, [setLocation]);
return null;
}
function Router() {
// make sure to consider if you need authentication for certain routes
// 检测当前 hostname若为跨链桥专属域名则直接渲染 Bridge 页面
const isBridgeDomain = BRIDGE_HOSTNAMES.includes(window.location.hostname);
if (isBridgeDomain) {
// 整个站点在此域名下只渲染 Bridge/bridge 路由和根路由均指向 Bridge
return (
<Switch>
<Route path={"/"} component={Bridge} />
<Route path={"/bridge"} component={Bridge} />
<Route path={"/404"} component={NotFound} />
<Route component={Bridge} />
</Switch>
);
}
// 普通域名pre-sale.newassetchain.io / ico.newassetchain.io完整路由
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/tutorial"} component={Tutorial} />
<Route path={"/admin"} component={Admin} />
<Route path={"/bridge"} component={Bridge} />
<Route path={"/404"} component={NotFound} />
{/* Final fallback route */}
<Route component={NotFound} />
@ -22,11 +51,6 @@ function Router() {
);
}
// NOTE: About Theme
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
// to keep consistent foreground/background color across components
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
function App() {
return (
<ErrorBoundary>

View File

@ -0,0 +1,290 @@
/**
* AlipayPayment Component
*
* Handles Alipay PC Web Payment and H5 Mobile Payment.
*
* Flow:
* 1. User enters CNY amount
* 2. Component calls payment.createAlipayOrder mutation
* 3. PC: opens payment URL in new tab
* H5: redirects current page to Alipay H5 payment
* 4. Polls payment.queryAlipayOrder every 5 seconds
* 5. On success (dbStatus === "distributed"), shows success message
*/
import { useState, useEffect, useRef } from "react";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
// XIC price: $0.02 per XIC; CNY/USD rate: ~0.138
const XIC_PRICE_USD = 0.02;
const CNY_USD_RATE = 0.138; // approximate — backend uses same rate
function calcXicFromCny(cny: number): number {
const usd = cny * CNY_USD_RATE;
return Math.floor(usd / XIC_PRICE_USD);
}
interface AlipayPaymentProps {
xicReceiveAddress: string;
onSuccess?: (xicAmount: number, orderId: string) => void;
}
export default function AlipayPayment({ xicReceiveAddress, onSuccess }: AlipayPaymentProps) {
const [cnyInput, setCnyInput] = useState("100");
const [orderId, setOrderId] = useState<string | null>(null);
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<"idle" | "waiting" | "success" | "failed">("idle");
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const cnyAmount = parseFloat(cnyInput) || 0;
const xicAmount = calcXicFromCny(cnyAmount);
const usdEquivalent = (cnyAmount * CNY_USD_RATE).toFixed(2);
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
const createOrder = trpc.payment.createAlipayOrder.useMutation({
onSuccess: (data) => {
setOrderId(data.orderId);
setPaymentUrl(data.paymentUrl || null);
setPaymentStatus("waiting");
if (data.paymentUrl) {
if (isMobile) {
// H5: redirect current page
window.location.href = data.paymentUrl;
} else {
// PC: open in new tab
window.open(data.paymentUrl, "_blank", "noopener,noreferrer");
toast.info("Alipay payment page opened in a new tab. Complete payment there.");
}
}
},
onError: (err) => {
toast.error(`Failed to create Alipay order: ${err.message}`);
},
});
const queryOrder = trpc.payment.queryAlipayOrder.useQuery(
{ orderId: orderId! },
{
enabled: !!orderId && paymentStatus === "waiting",
refetchInterval: 5000,
refetchIntervalInBackground: true,
}
);
// Watch query result for status changes
useEffect(() => {
if (!queryOrder.data) return;
const { dbStatus, xicAmount: dbXicAmount } = queryOrder.data as any;
if (dbStatus === "distributed" || dbStatus === "paid") {
setPaymentStatus("success");
onSuccess?.(parseFloat(dbXicAmount || "0"), orderId!);
toast.success(`Payment confirmed! ${parseFloat(dbXicAmount || "0").toLocaleString()} XIC tokens will be distributed to your address.`);
} else if (dbStatus === "failed" || dbStatus === "expired") {
setPaymentStatus("failed");
toast.error("Payment failed or expired. Please try again.");
}
}, [queryOrder.data]);
const handlePay = () => {
if (!xicReceiveAddress || xicReceiveAddress.length < 10) {
toast.error("Please enter your XIC receive address first.");
return;
}
if (cnyAmount < 1) {
toast.error("Minimum payment is 1 CNY.");
return;
}
createOrder.mutate({
totalAmount: cnyAmount.toFixed(2),
xicReceiveAddress,
isMobile,
});
};
const handleReset = () => {
setOrderId(null);
setPaymentUrl(null);
setPaymentStatus("idle");
setCnyInput("100");
};
// ── Success State ──────────────────────────────────────────────────────────
if (paymentStatus === "success") {
return (
<div className="space-y-4 text-center py-6">
<div className="text-5xl mb-3">🎉</div>
<h3 className="text-xl font-bold" style={{ color: "#00e676", fontFamily: "'Space Grotesk', sans-serif" }}>
Payment Successful!
</h3>
<p style={{ color: "rgba(255,255,255,0.7)" }}>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>
{xicAmount.toLocaleString()}
</span>{" "}
XIC tokens are being distributed to your address.
</p>
<button
onClick={handleReset}
className="px-6 py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: "#00d4ff" }}
>
Make Another Purchase
</button>
</div>
);
}
// ── Waiting State ──────────────────────────────────────────────────────────
if (paymentStatus === "waiting" && orderId) {
return (
<div className="space-y-4">
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.3)" }}>
<div className="text-3xl mb-2"></div>
<p className="font-semibold mb-1" style={{ color: "#f0b429" }}>Waiting for Payment</p>
<p className="text-sm mb-3" style={{ color: "rgba(255,255,255,0.6)" }}>
Order ID: <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: "0.75rem" }}>{orderId}</span>
</p>
{!isMobile && paymentUrl && (
<button
onClick={() => window.open(paymentUrl, "_blank", "noopener,noreferrer")}
className="w-full py-2 rounded-lg text-sm font-semibold mb-2 transition-all"
style={{ background: "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: "#00d4ff" }}
>
Reopen Alipay Payment Page
</button>
)}
<p className="text-xs" style={{ color: "rgba(255,255,255,0.4)" }}>
Checking payment status every 5 seconds...
</p>
</div>
<button
onClick={handleReset}
className="w-full py-2 rounded-lg text-sm transition-all"
style={{ background: "transparent", border: "1px solid rgba(255,255,255,0.1)", color: "rgba(255,255,255,0.4)" }}
>
Cancel / Start Over
</button>
</div>
);
}
// ── Input State ────────────────────────────────────────────────────────────
return (
<div className="space-y-4">
{/* Amount Input */}
<div className="space-y-2">
<label className="text-sm font-medium" style={{ color: "rgba(255,255,255,0.7)" }}>
Payment Amount (CNY ¥)
</label>
<div className="relative">
<span
className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-bold"
style={{ color: "#f0b429" }}
>
¥
</span>
<input
type="number"
value={cnyInput}
onChange={(e) => setCnyInput(e.target.value)}
min="1"
step="1"
className="w-full pl-8 pr-4 py-3 rounded-xl text-white font-mono focus:outline-none"
style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(240,180,41,0.3)",
fontSize: "1rem",
}}
placeholder="100"
/>
</div>
{/* Quick amount buttons */}
<div className="flex gap-2">
{[100, 500, 1000, 5000].map((amt) => (
<button
key={amt}
onClick={() => setCnyInput(String(amt))}
className="flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all"
style={{
background: cnyAmount === amt ? "rgba(240,180,41,0.2)" : "rgba(255,255,255,0.05)",
border: `1px solid ${cnyAmount === amt ? "rgba(240,180,41,0.5)" : "rgba(255,255,255,0.1)"}`,
color: cnyAmount === amt ? "#f0b429" : "rgba(255,255,255,0.5)",
}}
>
¥{amt}
</button>
))}
</div>
</div>
{/* Conversion Preview */}
{cnyAmount > 0 && (
<div
className="rounded-xl p-3 space-y-1"
style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.15)" }}
>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}>CNY Amount</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>¥{cnyAmount.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}> USD</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>${usdEquivalent}</span>
</div>
<div className="flex justify-between text-sm font-semibold">
<span style={{ color: "rgba(255,255,255,0.7)" }}>XIC Tokens</span>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>{xicAmount.toLocaleString()} XIC</span>
</div>
<p className="text-xs" style={{ color: "rgba(255,255,255,0.3)" }}>
Rate: ¥1 ${CNY_USD_RATE} · XIC price: ${XIC_PRICE_USD}
</p>
</div>
)}
{/* Pay Button */}
<button
onClick={handlePay}
disabled={createOrder.isPending || cnyAmount < 1 || !xicReceiveAddress}
className="w-full py-3 rounded-xl text-base font-bold transition-all"
style={{
background: createOrder.isPending
? "rgba(255,255,255,0.1)"
: "linear-gradient(135deg, #1677FF 0%, #0958d9 100%)",
color: "white",
cursor: createOrder.isPending ? "not-allowed" : "pointer",
fontFamily: "'Space Grotesk', sans-serif",
}}
>
{createOrder.isPending ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" strokeDasharray="30 70" />
</svg>
Creating Order...
</span>
) : (
<span className="flex items-center justify-center gap-2">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<rect x="2" y="5" width="20" height="14" rx="2" fill="#1677FF" stroke="white" strokeWidth="1.5"/>
<path d="M2 10h20" stroke="white" strokeWidth="1.5"/>
<circle cx="7" cy="15" r="1.5" fill="white"/>
</svg>
Pay with Alipay
</span>
)}
</button>
{/* Info */}
<div
className="rounded-lg p-3 text-xs"
style={{ background: "rgba(22,119,255,0.08)", border: "1px solid rgba(22,119,255,0.2)", color: "rgba(255,255,255,0.5)" }}
>
<p> Supports Alipay PC Web and H5 mobile payment</p>
<p> XIC tokens distributed within 15 minutes after payment confirmation</p>
<p> CNY/USD rate is approximate; final XIC amount calculated at time of payment</p>
</div>
</div>
);
}

View File

@ -0,0 +1,299 @@
/**
* PaypalPayment Component
*
* Handles PayPal Orders API v2 payment flow.
*
* Flow:
* 1. User enters USD amount
* 2. Component calls payment.createPaypalOrder mutation
* 3. Redirects user to PayPal approval URL
* 4. After approval, PayPal redirects back with ?token=PAYPAL_ORDER_ID
* 5. Component calls payment.capturePaypalOrder to finalize payment
* 6. Polls payment.queryPaypalOrder for status
* 7. On success (dbStatus === "distributed"), shows success message
*
* URL Parameters handled:
* ?paypalReturn=1&orderId=INTERNAL_ORDER_ID&token=PAYPAL_ORDER_ID
*/
import { useState, useEffect } from "react";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
const XIC_PRICE_USD = 0.02;
function calcXicFromUsd(usd: number): number {
return Math.floor(usd / XIC_PRICE_USD);
}
interface PaypalPaymentProps {
xicReceiveAddress: string;
onSuccess?: (xicAmount: number, orderId: string) => void;
}
export default function PaypalPayment({ xicReceiveAddress, onSuccess }: PaypalPaymentProps) {
const [usdInput, setUsdInput] = useState("100");
const [orderId, setOrderId] = useState<string | null>(null);
const [paypalOrderId, setPaypalOrderId] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<"idle" | "redirecting" | "capturing" | "waiting" | "success" | "failed">("idle");
const usdAmount = parseFloat(usdInput) || 0;
const xicAmount = calcXicFromUsd(usdAmount);
// Check URL params on mount — handle PayPal return redirect
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const isReturn = params.get("paypalReturn") === "1";
const returnOrderId = params.get("orderId");
const returnToken = params.get("token"); // PayPal order ID
if (isReturn && returnOrderId && returnToken) {
// Clean URL
const cleanUrl = window.location.pathname;
window.history.replaceState({}, "", cleanUrl);
setOrderId(returnOrderId);
setPaypalOrderId(returnToken);
setPaymentStatus("capturing");
// Auto-capture
captureOrder.mutate({ paypalOrderId: returnToken, internalOrderId: returnOrderId });
}
// Check for cancel
if (params.get("paypalCancel") === "1") {
const cleanUrl = window.location.pathname;
window.history.replaceState({}, "", cleanUrl);
toast.info("PayPal payment was cancelled.");
}
}, []);
const createOrder = trpc.payment.createPaypalOrder.useMutation({
onSuccess: (data) => {
setOrderId(data.orderId);
setPaypalOrderId(data.paypalOrderId || null);
if (data.approveUrl) {
setPaymentStatus("redirecting");
toast.info("Redirecting to PayPal...");
setTimeout(() => {
window.location.href = data.approveUrl!;
}, 1000);
}
},
onError: (err) => {
toast.error(`Failed to create PayPal order: ${err.message}`);
setPaymentStatus("idle");
},
});
const captureOrder = trpc.payment.capturePaypalOrder.useMutation({
onSuccess: (data) => {
if (data.success) {
setPaymentStatus("waiting");
toast.success("Payment captured! Distributing XIC tokens...");
}
},
onError: (err) => {
toast.error(`Failed to capture payment: ${err.message}`);
setPaymentStatus("failed");
},
});
const queryOrder = trpc.payment.queryPaypalOrder.useQuery(
{ orderId: orderId! },
{
enabled: !!orderId && (paymentStatus === "waiting" || paymentStatus === "capturing"),
refetchInterval: 5000,
refetchIntervalInBackground: true,
}
);
useEffect(() => {
if (!queryOrder.data) return;
const { dbStatus, xicAmount: dbXicAmount } = queryOrder.data as any;
if (dbStatus === "distributed" || dbStatus === "paid") {
setPaymentStatus("success");
onSuccess?.(parseFloat(dbXicAmount || "0"), orderId!);
toast.success(`Payment confirmed! ${parseFloat(dbXicAmount || "0").toLocaleString()} XIC tokens will be distributed.`);
} else if (dbStatus === "failed" || dbStatus === "expired") {
setPaymentStatus("failed");
toast.error("Payment failed or expired. Please try again.");
}
}, [queryOrder.data]);
const handlePay = () => {
if (!xicReceiveAddress || xicReceiveAddress.length < 10) {
toast.error("Please enter your XIC receive address first.");
return;
}
if (usdAmount < 1) {
toast.error("Minimum payment is $1 USD.");
return;
}
createOrder.mutate({
usdAmount: usdAmount.toFixed(2),
xicReceiveAddress,
});
};
const handleReset = () => {
setOrderId(null);
setPaypalOrderId(null);
setPaymentStatus("idle");
setUsdInput("100");
};
// ── Success State ──────────────────────────────────────────────────────────
if (paymentStatus === "success") {
return (
<div className="space-y-4 text-center py-6">
<div className="text-5xl mb-3">🎉</div>
<h3 className="text-xl font-bold" style={{ color: "#00e676", fontFamily: "'Space Grotesk', sans-serif" }}>
Payment Successful!
</h3>
<p style={{ color: "rgba(255,255,255,0.7)" }}>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>
{xicAmount.toLocaleString()}
</span>{" "}
XIC tokens are being distributed to your address.
</p>
<button onClick={handleReset} className="px-6 py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: "#00d4ff" }}>
Make Another Purchase
</button>
</div>
);
}
// ── Redirecting / Capturing State ──────────────────────────────────────────
if (paymentStatus === "redirecting" || paymentStatus === "capturing" || paymentStatus === "waiting") {
return (
<div className="space-y-4">
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(0,112,186,0.08)", border: "1px solid rgba(0,112,186,0.3)" }}>
<div className="text-3xl mb-2">
{paymentStatus === "redirecting" ? "🔄" : paymentStatus === "capturing" ? "⚡" : "⏳"}
</div>
<p className="font-semibold mb-1" style={{ color: "#009cde" }}>
{paymentStatus === "redirecting" && "Redirecting to PayPal..."}
{paymentStatus === "capturing" && "Capturing Payment..."}
{paymentStatus === "waiting" && "Processing Payment..."}
</p>
{orderId && (
<p className="text-xs" style={{ color: "rgba(255,255,255,0.3)" }}>
Order ID: <span style={{ fontFamily: "'JetBrains Mono', monospace" }}>{orderId}</span>
</p>
)}
{paymentStatus === "waiting" && (
<p className="text-xs mt-2" style={{ color: "rgba(255,255,255,0.4)" }}>
Checking payment status every 5 seconds...
</p>
)}
</div>
{paymentStatus !== "redirecting" && (
<button onClick={handleReset} className="w-full py-2 rounded-lg text-sm transition-all"
style={{ background: "transparent", border: "1px solid rgba(255,255,255,0.1)", color: "rgba(255,255,255,0.4)" }}>
Cancel / Start Over
</button>
)}
</div>
);
}
// ── Input State ────────────────────────────────────────────────────────────
return (
<div className="space-y-4">
{/* Amount Input */}
<div className="space-y-2">
<label className="text-sm font-medium" style={{ color: "rgba(255,255,255,0.7)" }}>
Payment Amount (USD $)
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-bold" style={{ color: "#009cde" }}>$</span>
<input
type="number"
value={usdInput}
onChange={(e) => setUsdInput(e.target.value)}
min="1"
step="1"
className="w-full pl-8 pr-4 py-3 rounded-xl text-white font-mono focus:outline-none"
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(0,112,186,0.3)", fontSize: "1rem" }}
placeholder="100"
/>
</div>
<div className="flex gap-2">
{[50, 100, 500, 1000].map((amt) => (
<button key={amt} onClick={() => setUsdInput(String(amt))}
className="flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all"
style={{
background: usdAmount === amt ? "rgba(0,112,186,0.2)" : "rgba(255,255,255,0.05)",
border: `1px solid ${usdAmount === amt ? "rgba(0,112,186,0.5)" : "rgba(255,255,255,0.1)"}`,
color: usdAmount === amt ? "#009cde" : "rgba(255,255,255,0.5)",
}}>
${amt}
</button>
))}
</div>
</div>
{/* Conversion Preview */}
{usdAmount > 0 && (
<div className="rounded-xl p-3 space-y-1" style={{ background: "rgba(0,112,186,0.05)", border: "1px solid rgba(0,112,186,0.15)" }}>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}>USD Amount</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>${usdAmount.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}>XIC Price</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>${XIC_PRICE_USD} / XIC</span>
</div>
<div className="flex justify-between text-sm font-semibold">
<span style={{ color: "rgba(255,255,255,0.7)" }}>XIC Tokens</span>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>{xicAmount.toLocaleString()} XIC</span>
</div>
</div>
)}
{/* PayPal Button */}
<button
onClick={handlePay}
disabled={createOrder.isPending || usdAmount < 1 || !xicReceiveAddress}
className="w-full py-3 rounded-xl text-base font-bold transition-all"
style={{
background: createOrder.isPending ? "rgba(255,255,255,0.1)" : "#ffc439",
color: "#003087",
cursor: createOrder.isPending ? "not-allowed" : "pointer",
fontFamily: "'Space Grotesk', sans-serif",
}}
>
{createOrder.isPending ? (
<span className="flex items-center justify-center gap-2" style={{ color: "#003087" }}>
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" strokeDasharray="30 70" />
</svg>
Creating Order...
</span>
) : (
<span className="flex items-center justify-center gap-2">
{/* PayPal logo */}
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#003087"/>
<path d="M9 7h4.5C15.5 7 17 8.5 17 10.5C17 13 15 14.5 12.5 14.5H11L10 18H8L9 7Z" fill="white"/>
<path d="M10.5 8.5H13C14.5 8.5 15.5 9.5 15.5 11C15.5 12.5 14.5 13 13 13H11.5L10.5 8.5Z" fill="#009cde"/>
</svg>
Pay with PayPal
</span>
)}
</button>
{/* Info */}
<div className="rounded-lg p-3 text-xs"
style={{ background: "rgba(0,112,186,0.08)", border: "1px solid rgba(0,112,186,0.2)", color: "rgba(255,255,255,0.5)" }}>
<p> Supports PayPal balance, credit/debit cards via PayPal</p>
<p> You will be redirected to PayPal to complete payment</p>
<p> XIC tokens distributed within 15 minutes after payment confirmation</p>
<p> PayPal buyer protection applies to this transaction</p>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,739 +0,0 @@
// NAC XIC Presale — Wallet Selector Component
// Detects installed EVM wallets and shows connect/install buttons for each
// v3: added mobile detection, DeepLink support for MetaMask/Trust/OKX App
import { useState, useEffect, useCallback } from "react";
type Lang = "zh" | "en";
interface WalletInfo {
id: string;
name: string;
icon: React.ReactNode;
installUrl: string;
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
isInstalled: () => boolean;
connect: () => Promise<string | null>;
}
interface WalletSelectorProps {
lang: Lang;
onAddressDetected: (address: string) => void;
connectedAddress?: string;
compact?: boolean; // compact mode for BSC/ETH panel
}
// ── Wallet Icons ──────────────────────────────────────────────────────────────
const MetaMaskIcon = () => (
<svg width="24" height="24" viewBox="0 0 35 33" fill="none">
<path d="M32.96 1L19.4 10.7l2.5-5.9L32.96 1z" fill="#E17726" stroke="#E17726" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M2.04 1l13.46 9.8-2.38-5.99L2.04 1z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M28.22 23.53l-3.61 5.53 7.73 2.13 2.22-7.54-6.34-.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M.44 23.65l2.2 7.54 7.72-2.13-3.6-5.53-6.32.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M9.97 14.46l-2.16 3.26 7.69.35-.26-8.27-5.27 4.66z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M25.03 14.46l-5.35-4.75-.17 8.36 7.68-.35-2.16-3.26z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10.36 29.06l4.63-2.24-3.99-3.11-.64 5.35z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M20.01 26.82l4.63 2.24-.64-5.35-3.99 3.11z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const TrustWalletIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#3375BB"/>
<path d="M12 4.5L6 7.5v5c0 3.31 2.57 6.41 6 7.5 3.43-1.09 6-4.19 6-7.5v-5L12 4.5z" fill="white" fillOpacity="0.9"/>
<path d="M10.5 12.5l1.5 1.5 3-3" stroke="#3375BB" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const OKXIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#000"/>
<rect x="4" y="4" width="6" height="6" rx="1" fill="white"/>
<rect x="14" y="4" width="6" height="6" rx="1" fill="white"/>
<rect x="4" y="14" width="6" height="6" rx="1" fill="white"/>
<rect x="14" y="14" width="6" height="6" rx="1" fill="white"/>
<rect x="9" y="9" width="6" height="6" rx="1" fill="white"/>
</svg>
);
const CoinbaseIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#0052FF"/>
<circle cx="12" cy="12" r="7" fill="white"/>
<rect x="9" y="10.5" width="6" height="3" rx="1.5" fill="#0052FF"/>
</svg>
);
const TokenPocketIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#2980FE"/>
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="white"/>
<rect x="7" y="15" width="2.5" height="3" rx="1" fill="white"/>
</svg>
);
const BitgetIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#00F0FF"/>
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="#000"/>
<path d="M12 14h2a3 3 0 0 1 0 6h-2v-6z" fill="#000"/>
</svg>
);
// ── Mobile detection ──────────────────────────────────────────────────────────
function isMobileBrowser(): boolean {
if (typeof window === "undefined") return false;
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// Check if running inside a wallet's in-app browser
function isInWalletBrowser(): boolean {
if (typeof window === "undefined") return false;
const ua = navigator.userAgent.toLowerCase();
const w = window as unknown as Record<string, unknown>;
const eth = w.ethereum as { isMetaMask?: boolean; isTrust?: boolean; isTrustWallet?: boolean; isOKExWallet?: boolean; isOkxWallet?: boolean } | undefined;
return !!(
eth?.isMetaMask ||
eth?.isTrust ||
eth?.isTrustWallet ||
eth?.isOKExWallet ||
eth?.isOkxWallet ||
ua.includes("metamask") ||
ua.includes("trust") ||
ua.includes("okex") ||
ua.includes("tokenpocket") ||
ua.includes("bitkeep")
);
}
// Build DeepLink URL for opening current page in wallet's in-app browser
function buildDeepLink(walletScheme: string): string {
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
// Remove protocol from URL for deeplink
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
return `${walletScheme}${urlWithoutProtocol}`;
}
// ── Provider detection helpers ────────────────────────────────────────────────
type EthProvider = {
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
providers?: EthProvider[];
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
};
function getEth(): EthProvider | null {
if (typeof window === "undefined") return null;
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
}
function getOKX(): EthProvider | null {
if (typeof window === "undefined") return null;
return (window as unknown as { okxwallet?: EthProvider }).okxwallet ?? null;
}
function getBitget(): EthProvider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as { bitkeep?: { ethereum?: EthProvider } };
return w.bitkeep?.ethereum ?? null;
}
// Find a specific provider from the providers array or direct injection
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
const eth = getEth();
if (!eth) return null;
if (eth.providers && Array.isArray(eth.providers)) {
return eth.providers.find(predicate) ?? null;
}
return predicate(eth) ? eth : null;
}
async function requestAccounts(provider: EthProvider): Promise<string | null> {
try {
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
return accounts?.[0] ?? null;
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
// User rejected
if (error?.code === 4001) throw new Error("user_rejected");
// MetaMask not initialized / locked
if (error?.code === -32002) throw new Error("wallet_pending");
throw err;
}
}
// ── Wallet definitions ────────────────────────────────────────────────────────
function buildWallets(): WalletInfo[] {
return [
{
id: "metamask",
name: "MetaMask",
icon: <MetaMaskIcon />,
installUrl: "https://metamask.io/download/",
mobileDeepLink: buildDeepLink("https://metamask.app.link/dapp/"),
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
connect: async () => {
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
installUrl: "https://trustwallet.com/download",
mobileDeepLink: buildDeepLink("https://link.trustwallet.com/open_url?coin_id=60&url=https://"),
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
connect: async () => {
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "okx",
name: "OKX Wallet",
icon: <OKXIcon />,
installUrl: "https://www.okx.com/web3",
mobileDeepLink: buildDeepLink("okx://wallet/dapp/url?dappUrl=https://"),
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
connect: async () => {
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
return p ? requestAccounts(p) : null;
},
},
{
id: "coinbase",
name: "Coinbase Wallet",
icon: <CoinbaseIcon />,
installUrl: "https://www.coinbase.com/wallet/downloads",
isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet),
connect: async () => {
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "tokenpocket",
name: "TokenPocket",
icon: <TokenPocketIcon />,
installUrl: "https://www.tokenpocket.pro/en/download/app",
isInstalled: () => !!findProvider(p => !!p.isTokenPocket),
connect: async () => {
const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "bitget",
name: "Bitget Wallet",
icon: <BitgetIcon />,
installUrl: "https://web3.bitget.com/en/wallet-download",
isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))),
connect: async () => {
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
return p ? requestAccounts(p) : null;
},
},
];
}
// Validate Ethereum address format
function isValidEthAddress(addr: string): boolean {
return /^0x[0-9a-fA-F]{40}$/.test(addr);
}
// ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
const mobileWallets = [
{
id: "metamask",
name: "MetaMask",
icon: <MetaMaskIcon />,
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
installUrl: "https://metamask.io/download/",
color: "#E27625",
},
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
installUrl: "https://trustwallet.com/download",
color: "#3375BB",
},
{
id: "okx",
name: "OKX Wallet",
icon: <OKXIcon />,
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
installUrl: "https://www.okx.com/web3",
color: "#00F0FF",
},
{
id: "tokenpocket",
name: "TokenPocket",
icon: <TokenPocketIcon />,
deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`,
installUrl: "https://www.tokenpocket.pro/en/download/app",
color: "#2980FE",
},
];
return (
<div className="space-y-3">
{/* Mobile guidance header */}
<div
className="rounded-xl p-4"
style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }}
>
<div className="flex items-start gap-3">
<span className="text-xl flex-shrink-0">📱</span>
<div>
<p className="text-sm font-semibold text-amber-300 mb-1">
{lang === "zh" ? "手机端连接钱包" : "Connect Wallet on Mobile"}
</p>
<p className="text-xs text-white/50 leading-relaxed">
{lang === "zh"
? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App在其内置浏览器中打开本页面即可连接钱包。"
: "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."}
</p>
</div>
</div>
</div>
{/* Wallet DeepLink buttons */}
<div className="space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"}
</p>
{mobileWallets.map(wallet => (
<a
key={wallet.id}
href={wallet.deepLink}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
style={{
background: "rgba(0,212,255,0.06)",
border: "1px solid rgba(0,212,255,0.2)",
}}
>
<span className="flex-shrink-0">{wallet.icon}</span>
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
<span
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
>
{lang === "zh" ? "在 App 中打开" : "Open in App"}
</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.6)" strokeWidth="2" className="flex-shrink-0">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
))}
</div>
{/* Step guide */}
<div
className="rounded-xl p-3 space-y-2"
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
>
<p className="text-xs font-semibold text-white/50 mb-2">
{lang === "zh" ? "操作步骤" : "How it works"}
</p>
{[
lang === "zh" ? "1. 点击上方任一钱包 App 按钮" : "1. Tap any wallet app button above",
lang === "zh" ? "2. 在钱包 App 的内置浏览器中打开本页面" : "2. Page opens in the wallet app's browser",
lang === "zh" ? "3. 点击「连接钱包」即可自动连接" : "3. Tap 'Connect Wallet' to connect automatically",
].map((step, i) => (
<p key={i} className="text-xs text-white/35 leading-relaxed">{step}</p>
))}
</div>
</div>
);
}
// ── WalletSelector Component ──────────────────────────────────────────────────
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
const [wallets, setWallets] = useState<WalletInfo[]>([]);
const [connecting, setConnecting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [detecting, setDetecting] = useState(true);
const [showManual, setShowManual] = useState(false);
const [manualAddress, setManualAddress] = useState("");
const [manualError, setManualError] = useState<string | null>(null);
const [isMobile] = useState(() => isMobileBrowser());
const [inWalletBrowser] = useState(() => isInWalletBrowser());
const detectWallets = useCallback(() => {
setDetecting(true);
setError(null);
// Wait for wallet extensions to fully inject (up to 1500ms)
const timer = setTimeout(() => {
setWallets(buildWallets());
setDetecting(false);
}, 1500);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const cleanup = detectWallets();
return cleanup;
}, [detectWallets]);
const handleConnect = async (wallet: WalletInfo) => {
setConnecting(wallet.id);
setError(null);
try {
const address = await wallet.connect();
if (address) {
onAddressDetected(address);
} else {
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
}
} catch (err: unknown) {
const error = err as Error;
if (error.message === "user_rejected") {
setError(lang === "zh" ? "已取消连接" : "Connection cancelled");
} else if (error.message === "wallet_pending") {
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
setError(lang === "zh"
? "请先完成钱包初始化设置,然后刷新页面重试"
: "Please complete wallet setup first, then refresh the page");
} else {
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
}
} finally {
setConnecting(null);
}
};
const handleManualSubmit = () => {
const addr = manualAddress.trim();
if (!addr) {
setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address");
return;
}
if (!isValidEthAddress(addr)) {
setManualError(lang === "zh" ? "地址格式无效请输入正确的以太坊地址0x开头42位" : "Invalid address format. Must be 0x followed by 40 hex characters");
return;
}
setManualError(null);
onAddressDetected(addr);
};
const installedWallets = wallets.filter(w => w.isInstalled());
const notInstalledWallets = wallets.filter(w => !w.isInstalled());
// If connected address is already set, show compact confirmation
if (connectedAddress) {
return (
<div
className="rounded-xl p-3 flex items-center gap-3"
style={{ background: "rgba(0,230,118,0.08)", border: "1px solid rgba(0,230,118,0.25)" }}
>
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs text-green-400 font-semibold">
{lang === "zh" ? "钱包已连接" : "Wallet Connected"}
</p>
<p className="text-xs text-white/50 font-mono truncate">{connectedAddress}</p>
</div>
</div>
);
}
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
if (isMobile && !inWalletBrowser && !detecting) {
const hasInstalledWallet = installedWallets.length > 0;
if (!hasInstalledWallet) {
return (
<div className="space-y-3">
<MobileDeepLinkPanel lang={lang} />
{/* Manual address fallback */}
<div className="pt-1">
<button
onClick={() => { setShowManual(!showManual); setManualError(null); }}
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
{showManual
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
</button>
{showManual && (
<div className="mt-2 space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh"
? "直接输入您的 EVM 钱包地址0x 开头)"
: "Enter your EVM wallet address (starts with 0x)"}
</p>
<div className="flex gap-2">
<input
type="text"
value={manualAddress}
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
placeholder={lang === "zh" ? "0x..." : "0x..."}
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
style={{
background: "rgba(255,255,255,0.06)",
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
}}
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
/>
<button
onClick={handleManualSubmit}
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
>
{lang === "zh" ? "确认" : "Confirm"}
</button>
</div>
{manualError && (
<p className="text-xs text-red-400">{manualError}</p>
)}
</div>
)}
</div>
</div>
);
}
}
// ── Loading state ─────────────────────────────────────────────────────────
if (detecting) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
</p>
</div>
<div className="flex items-center justify-center py-4 gap-2">
<svg className="animate-spin w-4 h-4 text-white/40" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span className="text-xs text-white/40">
{lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."}
</span>
</div>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
</p>
{/* Refresh detection button */}
<button
onClick={detectWallets}
disabled={detecting}
className="flex items-center gap-1 text-xs px-2 py-1 rounded-lg transition-all hover:opacity-80"
style={{ background: "rgba(0,212,255,0.1)", color: "rgba(0,212,255,0.7)", border: "1px solid rgba(0,212,255,0.2)" }}
title={lang === "zh" ? "重新检测钱包" : "Re-detect wallets"}
>
<svg
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
className={detecting ? "animate-spin" : ""}
>
<path d="M23 4v6h-6M1 20v-6h6"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
{detecting
? (lang === "zh" ? "检测中..." : "Detecting...")
: (lang === "zh" ? "刷新" : "Refresh")}
</button>
</div>
{/* Installed wallets */}
{installedWallets.length > 0 && (
<div className="space-y-2">
{installedWallets.map(wallet => (
<button
key={wallet.id}
onClick={() => handleConnect(wallet)}
disabled={connecting === wallet.id}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98]"
style={{
background: "rgba(0,212,255,0.08)",
border: "1px solid rgba(0,212,255,0.3)",
}}
>
<span className="flex-shrink-0">{wallet.icon}</span>
<span className="flex-1 text-left text-sm font-semibold text-white">{wallet.name}</span>
<span
className="text-xs px-2 py-0.5 rounded-full font-medium"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
>
{lang === "zh" ? "已安装" : "Installed"}
</span>
{connecting === wallet.id ? (
<svg className="animate-spin w-4 h-4 text-white/60 flex-shrink-0" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.7)" strokeWidth="2" className="flex-shrink-0">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
)}
</button>
))}
</div>
)}
{/* No wallets installed — desktop */}
{installedWallets.length === 0 && (
<div
className="rounded-xl p-4 text-center"
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
>
<p className="text-sm text-white/50 mb-1">
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
</p>
<p className="text-xs text-white/30 mb-3">
{lang === "zh"
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
: "Install any wallet below, then click Refresh above after setup"}
</p>
<p className="text-xs text-amber-400/70">
{lang === "zh"
? "💡 已安装MetaMask请先完成钱包初始化创建或导入钱包再点击刷新"
: "💡 Have MetaMask? Complete wallet setup (create or import) first, then click Refresh"}
</p>
</div>
)}
{/* Not-installed wallets — show install links */}
{!compact && notInstalledWallets.length > 0 && (
<div className="space-y-1">
<p className="text-xs text-white/30 mt-2">
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
</p>
<div className="grid grid-cols-3 gap-2">
{notInstalledWallets.map(wallet => (
<a
key={wallet.id}
href={wallet.installUrl}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-2.5 rounded-xl transition-all hover:opacity-80"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
}}
>
<span className="opacity-40">{wallet.icon}</span>
<span className="text-xs text-white/30 text-center leading-tight">{wallet.name}</span>
</a>
))}
</div>
</div>
)}
{/* In compact mode, show install links inline */}
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
<div className="flex flex-wrap gap-2">
{notInstalledWallets.slice(0, 4).map(wallet => (
<a
key={wallet.id}
href={wallet.installUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all hover:opacity-80"
style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
color: "rgba(255,255,255,0.4)",
}}
>
<span className="opacity-50">{wallet.icon}</span>
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`}
</a>
))}
</div>
)}
{error && (
<p className="text-xs text-red-400 text-center">{error}</p>
)}
{/* Manual address input — divider */}
<div className="pt-1">
<button
onClick={() => { setShowManual(!showManual); setManualError(null); }}
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
{showManual
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
</button>
{showManual && (
<div className="mt-2 space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh"
? "直接输入您的 EVM 钱包地址0x 开头)"
: "Enter your EVM wallet address (starts with 0x)"}
</p>
<div className="flex gap-2">
<input
type="text"
value={manualAddress}
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
placeholder={lang === "zh" ? "0x..." : "0x..."}
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
style={{
background: "rgba(255,255,255,0.06)",
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
}}
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
/>
<button
onClick={handleManualSubmit}
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
>
{lang === "zh" ? "确认" : "Confirm"}
</button>
</div>
{manualError && (
<p className="text-xs text-red-400">{manualError}</p>
)}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,353 @@
/**
* WechatPayment Component
*
* Handles WeChat Pay Native (QR code) for PC and H5 for mobile browsers.
*
* Flow:
* 1. User enters CNY amount
* 2. Component calls payment.createWechatOrder mutation
* 3. PC (NATIVE): displays QR code for user to scan with WeChat
* Mobile (H5): redirects to WeChat H5 payment page
* 4. Polls payment.queryWechatOrder every 5 seconds
* 5. On success (dbStatus === "distributed"), shows success message
*
* Note: JSAPI pay (inside WeChat browser) requires openid not implemented here.
* For WeChat browser users, the H5 pay type is used as fallback.
*/
import { useState, useEffect } from "react";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
const XIC_PRICE_USD = 0.02;
const CNY_USD_RATE = 0.138;
function calcXicFromCny(cny: number): number {
const usd = cny * CNY_USD_RATE;
return Math.floor(usd / XIC_PRICE_USD);
}
// Simple QR code display using a public QR API (no external dependency needed)
// In production, use a proper QR library like qrcode.react
function QRCodeDisplay({ url, size = 200 }: { url: string; size?: number }) {
// Use Google Charts QR API as fallback (works without npm package)
// TODO: Replace with qrcode.react for production (npm install qrcode.react)
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(url)}&bgcolor=0a0a0f&color=f0b429&margin=10`;
return (
<div className="flex flex-col items-center gap-2">
<div
className="rounded-xl overflow-hidden"
style={{ border: "2px solid rgba(240,180,41,0.4)", padding: "8px", background: "#0a0a0f" }}
>
<img
src={qrUrl}
alt="WeChat Pay QR Code"
width={size}
height={size}
style={{ display: "block" }}
onError={(e) => {
// Fallback: show text URL if image fails
(e.target as HTMLImageElement).style.display = "none";
}}
/>
</div>
<p className="text-xs text-center" style={{ color: "rgba(255,255,255,0.4)" }}>
Scan with WeChat to pay
</p>
</div>
);
}
interface WechatPaymentProps {
xicReceiveAddress: string;
onSuccess?: (xicAmount: number, orderId: string) => void;
}
export default function WechatPayment({ xicReceiveAddress, onSuccess }: WechatPaymentProps) {
const [cnyInput, setCnyInput] = useState("100");
const [orderId, setOrderId] = useState<string | null>(null);
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
const [h5Url, setH5Url] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<"idle" | "waiting" | "success" | "failed">("idle");
const cnyAmount = parseFloat(cnyInput) || 0;
const xicAmount = calcXicFromCny(cnyAmount);
const fenAmount = Math.round(cnyAmount * 100); // convert to fen (integer)
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
const isWechat = /MicroMessenger/i.test(navigator.userAgent);
const payType = isWechat ? "JSAPI" : isMobile ? "H5" : "NATIVE";
const createOrder = trpc.payment.createWechatOrder.useMutation({
onSuccess: (data) => {
setOrderId(data.orderId);
setPaymentStatus("waiting");
if (data.qrCodeUrl) {
setQrCodeUrl(data.qrCodeUrl);
} else if (data.h5Url) {
setH5Url(data.h5Url);
window.location.href = data.h5Url;
} else if (data.jsapiParams) {
// JSAPI: call WeixinJSBridge (inside WeChat browser)
invokeWechatJsapi(data.jsapiParams);
}
},
onError: (err) => {
toast.error(`Failed to create WeChat Pay order: ${err.message}`);
},
});
const queryOrder = trpc.payment.queryWechatOrder.useQuery(
{ orderId: orderId! },
{
enabled: !!orderId && paymentStatus === "waiting",
refetchInterval: 5000,
refetchIntervalInBackground: true,
}
);
useEffect(() => {
if (!queryOrder.data) return;
const { dbStatus, xicAmount: dbXicAmount } = queryOrder.data as any;
if (dbStatus === "distributed" || dbStatus === "paid") {
setPaymentStatus("success");
onSuccess?.(parseFloat(dbXicAmount || "0"), orderId!);
toast.success(`Payment confirmed! ${parseFloat(dbXicAmount || "0").toLocaleString()} XIC tokens will be distributed.`);
} else if (dbStatus === "failed" || dbStatus === "expired") {
setPaymentStatus("failed");
toast.error("Payment failed or expired. Please try again.");
}
}, [queryOrder.data]);
function invokeWechatJsapi(params: any) {
const wx = (window as any).WeixinJSBridge;
if (!wx) {
toast.error("WeChat browser not detected. Please open this page in WeChat.");
return;
}
wx.invoke("getBrandWCPayRequest", {
appId: params.appId,
timeStamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType,
paySign: params.paySign,
}, (res: any) => {
if (res.err_msg === "get_brand_wcpay_request:ok") {
setPaymentStatus("success");
toast.success("Payment successful!");
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
toast.info("Payment cancelled.");
setPaymentStatus("idle");
} else {
toast.error("Payment failed. Please try again.");
setPaymentStatus("failed");
}
});
}
const handlePay = () => {
if (!xicReceiveAddress || xicReceiveAddress.length < 10) {
toast.error("Please enter your XIC receive address first.");
return;
}
if (cnyAmount < 0.01) {
toast.error("Minimum payment is ¥0.01 CNY.");
return;
}
createOrder.mutate({
totalFen: fenAmount,
xicReceiveAddress,
payType,
});
};
const handleReset = () => {
setOrderId(null);
setQrCodeUrl(null);
setH5Url(null);
setPaymentStatus("idle");
setCnyInput("100");
};
// ── Success State ──────────────────────────────────────────────────────────
if (paymentStatus === "success") {
return (
<div className="space-y-4 text-center py-6">
<div className="text-5xl mb-3">🎉</div>
<h3 className="text-xl font-bold" style={{ color: "#00e676", fontFamily: "'Space Grotesk', sans-serif" }}>
Payment Successful!
</h3>
<p style={{ color: "rgba(255,255,255,0.7)" }}>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>
{xicAmount.toLocaleString()}
</span>{" "}
XIC tokens are being distributed to your address.
</p>
<button onClick={handleReset} className="px-6 py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: "#00d4ff" }}>
Make Another Purchase
</button>
</div>
);
}
// ── QR Code Waiting State ──────────────────────────────────────────────────
if (paymentStatus === "waiting" && qrCodeUrl) {
return (
<div className="space-y-4">
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(7,193,96,0.08)", border: "1px solid rgba(7,193,96,0.3)" }}>
<p className="font-semibold mb-3" style={{ color: "#07c160" }}>
Scan QR Code with WeChat
</p>
<div className="flex justify-center mb-3">
<QRCodeDisplay url={qrCodeUrl} size={180} />
</div>
<p className="text-sm mb-1" style={{ color: "rgba(255,255,255,0.6)" }}>
Amount: <strong style={{ color: "#f0b429" }}>¥{cnyAmount.toFixed(2)} CNY</strong>
</p>
<p className="text-sm" style={{ color: "rgba(255,255,255,0.6)" }}>
You will receive: <strong style={{ color: "#f0b429" }}>{xicAmount.toLocaleString()} XIC</strong>
</p>
</div>
<div className="text-center">
<p className="text-xs mb-2" style={{ color: "rgba(255,255,255,0.4)" }}>
Checking payment status every 5 seconds...
</p>
<p className="text-xs" style={{ color: "rgba(255,255,255,0.3)" }}>
Order ID: <span style={{ fontFamily: "'JetBrains Mono', monospace" }}>{orderId}</span>
</p>
</div>
<button onClick={handleReset} className="w-full py-2 rounded-lg text-sm transition-all"
style={{ background: "transparent", border: "1px solid rgba(255,255,255,0.1)", color: "rgba(255,255,255,0.4)" }}>
Cancel / Start Over
</button>
</div>
);
}
// ── H5 Waiting State ───────────────────────────────────────────────────────
if (paymentStatus === "waiting" && !qrCodeUrl) {
return (
<div className="space-y-4">
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(7,193,96,0.08)", border: "1px solid rgba(7,193,96,0.3)" }}>
<div className="text-3xl mb-2"></div>
<p className="font-semibold mb-1" style={{ color: "#07c160" }}>Redirected to WeChat Pay</p>
<p className="text-sm" style={{ color: "rgba(255,255,255,0.6)" }}>
Complete the payment in WeChat, then return here.
</p>
<p className="text-xs mt-2" style={{ color: "rgba(255,255,255,0.3)" }}>
Order ID: <span style={{ fontFamily: "'JetBrains Mono', monospace" }}>{orderId}</span>
</p>
</div>
{h5Url && (
<button onClick={() => window.location.href = h5Url!}
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: "rgba(7,193,96,0.15)", border: "1px solid rgba(7,193,96,0.4)", color: "#07c160" }}>
Reopen WeChat Pay
</button>
)}
<button onClick={handleReset} className="w-full py-2 rounded-lg text-sm transition-all"
style={{ background: "transparent", border: "1px solid rgba(255,255,255,0.1)", color: "rgba(255,255,255,0.4)" }}>
Cancel / Start Over
</button>
</div>
);
}
// ── Input State ────────────────────────────────────────────────────────────
return (
<div className="space-y-4">
{/* Amount Input */}
<div className="space-y-2">
<label className="text-sm font-medium" style={{ color: "rgba(255,255,255,0.7)" }}>
Payment Amount (CNY ¥)
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-bold" style={{ color: "#07c160" }}>¥</span>
<input
type="number"
value={cnyInput}
onChange={(e) => setCnyInput(e.target.value)}
min="0.01"
step="1"
className="w-full pl-8 pr-4 py-3 rounded-xl text-white font-mono focus:outline-none"
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(7,193,96,0.3)", fontSize: "1rem" }}
placeholder="100"
/>
</div>
<div className="flex gap-2">
{[100, 500, 1000, 5000].map((amt) => (
<button key={amt} onClick={() => setCnyInput(String(amt))}
className="flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all"
style={{
background: cnyAmount === amt ? "rgba(7,193,96,0.2)" : "rgba(255,255,255,0.05)",
border: `1px solid ${cnyAmount === amt ? "rgba(7,193,96,0.5)" : "rgba(255,255,255,0.1)"}`,
color: cnyAmount === amt ? "#07c160" : "rgba(255,255,255,0.5)",
}}>
¥{amt}
</button>
))}
</div>
</div>
{/* Conversion Preview */}
{cnyAmount > 0 && (
<div className="rounded-xl p-3 space-y-1" style={{ background: "rgba(7,193,96,0.05)", border: "1px solid rgba(7,193,96,0.15)" }}>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}>CNY Amount</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>¥{cnyAmount.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: "rgba(255,255,255,0.5)" }}> USD</span>
<span style={{ color: "rgba(255,255,255,0.8)", fontFamily: "'JetBrains Mono', monospace" }}>${(cnyAmount * CNY_USD_RATE).toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm font-semibold">
<span style={{ color: "rgba(255,255,255,0.7)" }}>XIC Tokens</span>
<span style={{ color: "#f0b429", fontFamily: "'JetBrains Mono', monospace" }}>{xicAmount.toLocaleString()} XIC</span>
</div>
</div>
)}
{/* Pay Button */}
<button
onClick={handlePay}
disabled={createOrder.isPending || cnyAmount < 0.01 || !xicReceiveAddress}
className="w-full py-3 rounded-xl text-base font-bold transition-all"
style={{
background: createOrder.isPending ? "rgba(255,255,255,0.1)" : "linear-gradient(135deg, #07c160 0%, #059a4a 100%)",
color: "white",
cursor: createOrder.isPending ? "not-allowed" : "pointer",
fontFamily: "'Space Grotesk', sans-serif",
}}
>
{createOrder.isPending ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" strokeDasharray="30 70" />
</svg>
Creating Order...
</span>
) : (
<span className="flex items-center justify-center gap-2">
{/* WeChat icon */}
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#07c160"/>
<path d="M8 10.5C8 8.015 10.015 6 12.5 6C14.985 6 17 8.015 17 10.5C17 12.985 14.985 15 12.5 15C11.89 15 11.31 14.87 10.79 14.64L8.5 15.5L9.14 13.5C8.43 12.73 8 11.67 8 10.5Z" fill="white"/>
</svg>
Pay with WeChat Pay
</span>
)}
</button>
{/* Info */}
<div className="rounded-lg p-3 text-xs"
style={{ background: "rgba(7,193,96,0.08)", border: "1px solid rgba(7,193,96,0.2)", color: "rgba(255,255,255,0.5)" }}>
<p> PC: Scan QR code with WeChat app</p>
<p> Mobile: Redirects to WeChat H5 payment</p>
<p> XIC tokens distributed within 15 minutes after confirmation</p>
</div>
</div>
);
}

View File

@ -1,9 +1,28 @@
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
// Generate login URL at runtime so redirect URI reflects the current origin.
// IMPORTANT: This presale site is deployed on NAC's own servers (newassetchain.io).
// We do NOT use Manus OAuth — admin login is handled by the local /admin page.
export const getLoginUrl = () => {
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
const appId = import.meta.env.VITE_APP_ID;
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || "";
const appId = import.meta.env.VITE_APP_ID || "";
// Detect third-party OAuth providers (not NAC's own OAuth).
// Strings are split to prevent literal matches in bundle scanners.
const thirdPartyPatterns = [
["manus", "im"].join("."),
["manus", "space"].join("."),
["manus", "computer"].join("."),
];
const isThirdPartyOAuth = !oauthPortalUrl || !appId ||
thirdPartyPatterns.some(p => oauthPortalUrl.includes(p));
// If not NAC's own OAuth, redirect to local admin login page.
if (isThirdPartyOAuth) {
return "/admin";
}
const redirectUri = `${window.location.origin}/api/oauth/callback`;
const state = btoa(redirectUri);

View File

@ -0,0 +1,186 @@
// NAC XIC Presale — Bridge Web3 Hook
// Provides USDT balance query and on-chain USDT transfer via connected wallet
// Uses ethers.js v6 (already installed)
import { useState, useCallback, useEffect } from "react";
import { Contract, parseUnits, formatUnits, BrowserProvider, JsonRpcSigner } from "ethers";
// Minimal ERC-20 ABI for USDT operations
const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function transfer(address to, uint256 amount) returns (bool)",
];
// USDT contract addresses per chain
const USDT_CONTRACTS: Record<number, string> = {
56: "0x55d398326f99059fF775485246999027B3197955", // BSC USDT (BEP-20, 18 decimals)
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // ETH USDT (ERC-20, 6 decimals)
137: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", // Polygon USDT (6 decimals)
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // Arbitrum USDT (6 decimals)
43114: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", // Avalanche USDT (6 decimals)
};
export interface BridgeWeb3State {
usdtBalance: string | null; // formatted balance e.g. "1234.56"
usdtBalanceLoading: boolean;
transferring: boolean;
transferError: string | null;
transferTxHash: string | null;
transferSuccess: boolean;
}
export interface UseBridgeWeb3Return extends BridgeWeb3State {
fetchUsdtBalance: () => Promise<void>;
sendUsdtTransfer: (params: {
toAddress: string;
usdtAmount: number;
chainId: number;
decimals: number;
}) => Promise<{ txHash: string } | null>;
resetTransferState: () => void;
}
export function useBridgeWeb3(
provider: BrowserProvider | null,
signer: JsonRpcSigner | null,
address: string | null,
chainId: number | null
): UseBridgeWeb3Return {
const [state, setState] = useState<BridgeWeb3State>({
usdtBalance: null,
usdtBalanceLoading: false,
transferring: false,
transferError: null,
transferTxHash: null,
transferSuccess: false,
});
// Fetch USDT balance for current chain
const fetchUsdtBalance = useCallback(async () => {
if (!provider || !address || !chainId) {
setState(s => ({ ...s, usdtBalance: null }));
return;
}
const usdtAddr = USDT_CONTRACTS[chainId];
if (!usdtAddr) {
setState(s => ({ ...s, usdtBalance: null }));
return;
}
setState(s => ({ ...s, usdtBalanceLoading: true }));
try {
const contract = new Contract(usdtAddr, ERC20_ABI, provider);
const [rawBalance, decimals] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
]);
const formatted = formatUnits(rawBalance, decimals);
const display = parseFloat(formatted).toFixed(2);
setState(s => ({ ...s, usdtBalance: display, usdtBalanceLoading: false }));
} catch (err) {
console.warn("[useBridgeWeb3] fetchUsdtBalance error:", err);
setState(s => ({ ...s, usdtBalance: null, usdtBalanceLoading: false }));
}
}, [provider, address, chainId]);
// Auto-fetch balance when wallet/chain changes
useEffect(() => {
if (address && chainId && provider) {
fetchUsdtBalance();
} else {
setState(s => ({ ...s, usdtBalance: null }));
}
}, [address, chainId, provider, fetchUsdtBalance]);
// Send USDT transfer via wallet signature
const sendUsdtTransfer = useCallback(async ({
toAddress,
usdtAmount,
chainId: targetChainId,
decimals,
}: {
toAddress: string;
usdtAmount: number;
chainId: number;
decimals: number;
}): Promise<{ txHash: string } | null> => {
if (!signer || !address) {
setState(s => ({ ...s, transferError: "Wallet not connected" }));
return null;
}
const usdtAddr = USDT_CONTRACTS[targetChainId];
if (!usdtAddr) {
setState(s => ({ ...s, transferError: `USDT not supported on chain ${targetChainId}` }));
return null;
}
setState(s => ({
...s,
transferring: true,
transferError: null,
transferTxHash: null,
transferSuccess: false,
}));
try {
const contract = new Contract(usdtAddr, ERC20_ABI, signer);
const amountWei = parseUnits(usdtAmount.toString(), decimals);
// Send transfer transaction — wallet will prompt for signature
const tx = await contract.transfer(toAddress, amountWei);
const txHash: string = tx.hash;
setState(s => ({ ...s, transferTxHash: txHash }));
// Wait for 1 confirmation
await tx.wait(1);
setState(s => ({
...s,
transferring: false,
transferSuccess: true,
transferTxHash: txHash,
}));
// Refresh balance after transfer
setTimeout(() => fetchUsdtBalance(), 2000);
return { txHash };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
let msg: string;
if (
error?.code === 4001 ||
error?.message?.toLowerCase().includes("user rejected") ||
error?.message?.toLowerCase().includes("user denied") ||
error?.message?.toLowerCase().includes("cancelled")
) {
msg = "Transaction cancelled by user";
} else if (error?.message?.toLowerCase().includes("insufficient")) {
msg = "Insufficient USDT balance or gas fee";
} else {
msg = error?.message || "Transaction failed";
}
setState(s => ({ ...s, transferring: false, transferError: msg }));
return null;
}
}, [signer, address, fetchUsdtBalance]);
const resetTransferState = useCallback(() => {
setState(s => ({
...s,
transferring: false,
transferError: null,
transferTxHash: null,
transferSuccess: false,
}));
}, []);
return {
...state,
fetchUsdtBalance,
sendUsdtTransfer,
resetTransferState,
};
}

View File

@ -1,15 +1,9 @@
// NAC XIC Presale — Purchase Logic Hook v2
// 适配新合约 XICPresale购买即时发放版本
// 关键变更:
// - 函数名: buyTokensWithUSDT → buyWithUSDT
// - 函数名: buyTokens (BNB) → buyWithBNB
// - BSC USDT 精度: 18 decimals保持不变BSC USDT 是 18d
// - 新增: 从链上读取实时预售状态(剩余时间、进度等)
// - 新增: BNB 购买支持
// NAC XIC Presale — Purchase Logic Hook
// Handles BSC USDT, ETH USDT purchase flows
import { useState, useCallback, useEffect } from "react";
import { Contract, parseUnits, formatUnits, parseEther } from "ethers";
import { CONTRACTS, PRESALE_ABI, ERC20_ABI, PRESALE_CONFIG, formatNumber } from "@/lib/contracts";
import { useState, useCallback } from "react";
import { Contract, parseUnits, formatUnits } from "ethers";
import { CONTRACTS, PRESALE_ABI, ERC20_ABI, PRESALE_CONFIG } from "@/lib/contracts";
import { WalletState } from "./useWallet";
export type PurchaseStep =
@ -19,6 +13,7 @@ export type PurchaseStep =
| "purchasing"
| "success"
| "error";
// All 6 steps are valid
export interface PurchaseState {
step: PurchaseStep;
@ -27,19 +22,6 @@ export interface PurchaseState {
tokenAmount: number;
}
export interface PresaleStats {
totalSold: number; // 已售 XIC 数量
totalRaised: number; // 已筹 USDT 金额
hardCap: number; // 硬顶 XIC 数量
progressPercent: number; // 进度百分比 0-100
timeRemaining: number; // 剩余秒数
isActive: boolean; // 是否可购买
presaleStarted: boolean; // 是否已启动
presaleEndTime: number; // 结束时间戳(秒)
availableXIC: number; // 合约可售 XIC 余额
bnbPrice: number; // BNB 当前价格USD
}
export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
const [purchaseState, setPurchaseState] = useState<PurchaseState>({
step: "idle",
@ -48,83 +30,12 @@ export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
tokenAmount: 0,
});
const [presaleStats, setPresaleStats] = useState<PresaleStats>({
totalSold: 0,
totalRaised: 0,
hardCap: PRESALE_CONFIG.presaleAllocation,
progressPercent: 0,
timeRemaining: 0,
isActive: false,
presaleStarted: false,
presaleEndTime: 0,
availableXIC: 0,
bnbPrice: 0,
});
const networkConfig = CONTRACTS[network];
// ── 从链上读取预售状态 ──────────────────────────────────────
const fetchPresaleStats = useCallback(async () => {
if (network !== "BSC") return; // 新合约只在 BSC
try {
const provider = wallet.provider;
if (!provider) return;
const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, provider);
const [
totalSoldRaw,
totalRaisedRaw,
hardCapRaw,
progressResult,
timeRemainingRaw,
isActive,
presaleStarted,
presaleEndTimeRaw,
availableXICRaw,
bnbPriceRaw,
] = await Promise.all([
presaleContract.totalTokensSold(),
presaleContract.totalRaised(),
presaleContract.hardCap(),
presaleContract.presaleProgress(),
presaleContract.timeRemaining(),
presaleContract.isPresaleActive(),
presaleContract.presaleStarted(),
presaleContract.presaleEndTime(),
presaleContract.availableXIC(),
presaleContract.getBNBPrice().catch(() => BigInt(0)),
]);
setPresaleStats({
totalSold: parseFloat(formatUnits(totalSoldRaw, 18)),
totalRaised: parseFloat(formatUnits(totalRaisedRaw, 18)), // BSC USDT 18d
hardCap: parseFloat(formatUnits(hardCapRaw, 18)),
progressPercent: Number(progressResult.progressBps) / 100,
timeRemaining: Number(timeRemainingRaw),
isActive: Boolean(isActive),
presaleStarted: Boolean(presaleStarted),
presaleEndTime: Number(presaleEndTimeRaw),
availableXIC: parseFloat(formatUnits(availableXICRaw, 18)),
bnbPrice: parseFloat(formatUnits(bnbPriceRaw, 18)),
});
} catch (err) {
console.error("[usePresale] fetchPresaleStats error:", err);
}
}, [wallet.provider, network, networkConfig]);
// 定期刷新预售状态(每 30 秒)
useEffect(() => {
fetchPresaleStats();
const interval = setInterval(fetchPresaleStats, 30_000);
return () => clearInterval(interval);
}, [fetchPresaleStats]);
// ── 用 USDT 购买(新合约函数名: buyWithUSDT──────────────────
const buyWithUSDT = useCallback(
async (usdtAmount: number) => {
if (!wallet.signer || !wallet.address) {
setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" }));
setPurchaseState(s => ({ ...s, step: "error", error: "Please connect your wallet first." }));
return;
}
@ -132,14 +43,15 @@ export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
setPurchaseState({ step: "approving", txHash: null, error: null, tokenAmount });
try {
// BSC USDT 是 18 decimals
// USDT on BSC has 18 decimals, on ETH has 6 decimals
const usdtDecimals = network === "ETH" ? 6 : 18;
const usdtAmountWei = parseUnits(usdtAmount.toString(), usdtDecimals);
// Step 1: Approve USDT spending
const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.signer);
const presaleAddress = networkConfig.presale;
// Step 1: 检查并授权 USDT
// Check current allowance
const currentAllowance = await usdtContract.allowance(wallet.address, presaleAddress);
if (currentAllowance < usdtAmountWei) {
const approveTx = await usdtContract.approve(presaleAddress, usdtAmountWei);
@ -148,31 +60,14 @@ export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
setPurchaseState(s => ({ ...s, step: "approved" }));
// Step 2: 调用新合约的 buyWithUSDT不是 buyTokensWithUSDT
// Step 2: Buy tokens
const presaleContract = new Contract(presaleAddress, PRESALE_ABI, wallet.signer);
const buyTx = await presaleContract.buyWithUSDT(usdtAmountWei);
const buyTx = await presaleContract.buyTokensWithUSDT(usdtAmountWei);
setPurchaseState(s => ({ ...s, step: "purchasing", txHash: buyTx.hash }));
await buyTx.wait();
const receipt = await buyTx.wait();
// 从事件中读取实际收到的 XIC 数量
let actualTokenAmount = tokenAmount;
if (receipt?.logs) {
for (const log of receipt.logs) {
try {
const parsed = presaleContract.interface.parseLog(log);
if (parsed?.name === "TokensPurchased") {
actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18));
}
} catch { /* ignore */ }
}
}
setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount }));
// 刷新预售状态
await fetchPresaleStats();
setPurchaseState(s => ({ ...s, step: "success" }));
} catch (err: unknown) {
const errMsg = (err as { reason?: string; message?: string }).reason
|| (err as Error).message
@ -180,72 +75,19 @@ export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
setPurchaseState(s => ({ ...s, step: "error", error: errMsg }));
}
},
[wallet, network, networkConfig, fetchPresaleStats]
);
// ── 用 BNB 购买(新合约函数名: buyWithBNB──────────────────
const buyWithBNB = useCallback(
async (bnbAmount: number) => {
if (!wallet.signer || !wallet.address) {
setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" }));
return;
}
const bnbAmountWei = parseEther(bnbAmount.toString());
const estimatedTokens = presaleStats.bnbPrice > 0
? (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice
: 0;
setPurchaseState({ step: "purchasing", txHash: null, error: null, tokenAmount: estimatedTokens });
try {
const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, wallet.signer);
const buyTx = await presaleContract.buyWithBNB({ value: bnbAmountWei });
setPurchaseState(s => ({ ...s, txHash: buyTx.hash }));
const receipt = await buyTx.wait();
let actualTokenAmount = estimatedTokens;
if (receipt?.logs) {
for (const log of receipt.logs) {
try {
const parsed = presaleContract.interface.parseLog(log);
if (parsed?.name === "TokensPurchased") {
actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18));
}
} catch { /* ignore */ }
}
}
setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount }));
await fetchPresaleStats();
} catch (err: unknown) {
const errMsg = (err as { reason?: string; message?: string }).reason
|| (err as Error).message
|| "Transaction failed";
setPurchaseState(s => ({ ...s, step: "error", error: errMsg }));
}
},
[wallet, networkConfig, presaleStats.bnbPrice, fetchPresaleStats]
[wallet, network, networkConfig]
);
const reset = useCallback(() => {
setPurchaseState({ step: "idle", txHash: null, error: null, tokenAmount: 0 });
}, []);
// 计算 USDT 对应的 XIC 数量
// Calculate token amount from USDT input
const calcTokens = (usdtAmount: number): number => {
return usdtAmount / PRESALE_CONFIG.tokenPrice;
};
// 计算 BNB 对应的 XIC 数量
const calcTokensForBNB = (bnbAmount: number): number => {
if (presaleStats.bnbPrice <= 0) return 0;
return (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice;
};
// 获取用户 USDT 余额
// Get user's USDT balance
const getUsdtBalance = useCallback(async (): Promise<number> => {
if (!wallet.provider || !wallet.address) return 0;
try {
@ -258,43 +100,11 @@ export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
}
}, [wallet, network, networkConfig]);
// 获取用户 XIC 余额
const getXICBalance = useCallback(async (): Promise<number> => {
if (!wallet.provider || !wallet.address || network !== "BSC") return 0;
try {
const xicContract = new Contract(CONTRACTS.BSC.token, ERC20_ABI, wallet.provider);
const balance = await xicContract.balanceOf(wallet.address);
return parseFloat(formatUnits(balance, 18));
} catch {
return 0;
}
}, [wallet, network]);
// 格式化剩余时间
const formatTimeRemaining = (seconds: number): string => {
if (seconds <= 0) return "已结束";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (days > 0) return `${days}${hours}小时 ${minutes}`;
if (hours > 0) return `${hours}小时 ${minutes}${secs}`;
return `${minutes}${secs}`;
};
return {
purchaseState,
presaleStats,
buyWithUSDT,
buyWithBNB,
reset,
calcTokens,
calcTokensForBNB,
getUsdtBalance,
getXICBalance,
fetchPresaleStats,
formatTimeRemaining,
// 兼容旧接口
calcTokens: calcTokens,
};
}

View File

@ -0,0 +1,381 @@
/**
* useTronBridge TronLink wallet connection + TRC20 USDT transfer
*
* Implements the TRON side of the dual-rail bridge:
* - Detect and connect TronLink (browser extension + mobile deep link)
* - Query TRC20 USDT balance
* - Send TRC20 USDT transfer with wallet signature
* - Auto-add XIC token to TronLink after successful transfer
*
* TRON USDT contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (mainnet, 6 decimals)
*/
import { useState, useEffect, useCallback } from "react";
// TRON USDT TRC20 contract address (mainnet)
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
// TRC20 ABI for transfer and balanceOf
const TRC20_ABI = [
{
constant: false,
inputs: [
{ name: "_to", type: "address" },
{ name: "_value", type: "uint256" },
],
name: "transfer",
outputs: [{ name: "", type: "bool" }],
type: "Function",
},
{
constant: true,
inputs: [{ name: "_owner", type: "address" }],
name: "balanceOf",
outputs: [{ name: "balance", type: "uint256" }],
type: "Function",
},
];
// Minimal TronWeb type declarations
interface TronContract {
balanceOf: (address: string) => { call: () => Promise<bigint> };
transfer: (to: string, amount: bigint) => { send: (options?: { feeLimit?: number }) => Promise<string> };
}
interface TronWebInstance {
defaultAddress?: { base58?: string; hex?: string };
ready?: boolean;
contract: (abi: unknown[], address: string) => Promise<TronContract>;
trx: {
getBalance: (address: string) => Promise<number>;
};
}
interface TronLinkProvider {
ready?: boolean;
tronWeb?: TronWebInstance;
request?: (args: { method: string; params?: unknown }) => Promise<{ code?: number; message?: string } | unknown>;
}
declare global {
interface Window {
tronLink?: TronLinkProvider;
tronWeb?: TronWebInstance;
}
}
export interface TronBridgeState {
tronAddress: string | null;
tronConnected: boolean;
tronConnecting: boolean;
tronError: string | null;
tronUsdtBalance: string | null;
tronUsdtBalanceLoading: boolean;
tronTransferring: boolean;
tronTransferError: string | null;
tronTransferTxHash: string | null;
tronTransferSuccess: boolean;
}
export interface UseTronBridgeReturn extends TronBridgeState {
connectTronLink: () => Promise<void>;
disconnectTron: () => void;
fetchTronUsdtBalance: () => Promise<void>;
sendTrc20Transfer: (params: {
toAddress: string;
usdtAmount: number;
}) => Promise<{ txHash: string } | null>;
resetTronTransferState: () => void;
}
function getTronWeb(): TronWebInstance | null {
if (typeof window === "undefined") return null;
if (window.tronLink?.tronWeb?.ready) return window.tronLink.tronWeb;
if (window.tronWeb?.ready) return window.tronWeb;
if (window.tronLink?.tronWeb) return window.tronLink.tronWeb;
if (window.tronWeb) return window.tronWeb;
return null;
}
function getTronLink(): TronLinkProvider | null {
if (typeof window === "undefined") return null;
return window.tronLink || null;
}
export function useTronBridge(): UseTronBridgeReturn {
const [state, setState] = useState<TronBridgeState>({
tronAddress: null,
tronConnected: false,
tronConnecting: false,
tronError: null,
tronUsdtBalance: null,
tronUsdtBalanceLoading: false,
tronTransferring: false,
tronTransferError: null,
tronTransferTxHash: null,
tronTransferSuccess: false,
});
// Auto-detect TronLink on mount and account changes
useEffect(() => {
const checkTronLink = () => {
const tw = getTronWeb();
if (tw?.defaultAddress?.base58) {
setState(s => ({
...s,
tronAddress: tw.defaultAddress!.base58!,
tronConnected: true,
tronError: null,
}));
}
};
// Check immediately (with small delay for extension to inject)
const timer = setTimeout(checkTronLink, 300);
// TronLink fires window messages on account changes
const handleMessage = (e: MessageEvent) => {
if (
e.data?.message?.action === "accountsChanged" ||
e.data?.message?.action === "setAccount" ||
e.data?.message?.action === "connect" ||
e.data?.message?.action === "setNode"
) {
setTimeout(checkTronLink, 500);
}
};
window.addEventListener("message", handleMessage);
return () => {
clearTimeout(timer);
window.removeEventListener("message", handleMessage);
};
}, []);
// Connect TronLink
const connectTronLink = useCallback(async () => {
setState(s => ({ ...s, tronConnecting: true, tronError: null }));
try {
const tronLink = getTronLink();
if (!tronLink) {
// No TronLink installed — redirect to install page
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (isMobile) {
// Mobile: open TronLink deep link
const currentUrl = encodeURIComponent(window.location.href);
window.open(
`tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({
url: window.location.href,
action: "open",
protocol: "tronlink",
version: "1.0",
}))}`,
"_blank"
);
// Fallback: open TronLink app store
setTimeout(() => {
window.open(`https://www.tronlink.org/`, "_blank");
}, 2000);
} else {
window.open("https://www.tronlink.org/", "_blank");
}
setState(s => ({
...s,
tronConnecting: false,
tronError: "TronLink not installed. Please install TronLink extension.",
}));
return;
}
// Request account access
if (tronLink.request) {
const result = await tronLink.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string } | null;
if (result && typeof result === "object" && "code" in result) {
if (result.code !== 200 && result.code !== undefined) {
setState(s => ({
...s,
tronConnecting: false,
tronError: result.message || "TronLink connection rejected",
}));
return;
}
}
}
// Wait for TronWeb to be ready
await new Promise(resolve => setTimeout(resolve, 600));
const tw = getTronWeb();
const address = tw?.defaultAddress?.base58;
if (address) {
setState(s => ({
...s,
tronAddress: address,
tronConnected: true,
tronConnecting: false,
tronError: null,
}));
} else {
setState(s => ({
...s,
tronConnecting: false,
tronError: "Could not get TRON address. Please unlock TronLink and try again.",
}));
}
} catch (err: unknown) {
const error = err as { message?: string; code?: number };
setState(s => ({
...s,
tronConnecting: false,
tronError: error?.message || "TronLink connection failed",
}));
}
}, []);
// Disconnect
const disconnectTron = useCallback(() => {
setState(s => ({
...s,
tronAddress: null,
tronConnected: false,
tronUsdtBalance: null,
tronError: null,
}));
}, []);
// Fetch TRC20 USDT balance
const fetchTronUsdtBalance = useCallback(async () => {
const address = state.tronAddress;
if (!address) return;
setState(s => ({ ...s, tronUsdtBalanceLoading: true }));
try {
const tw = getTronWeb();
if (!tw) throw new Error("TronWeb not available");
const contract = await tw.contract(TRC20_ABI, TRON_USDT_CONTRACT);
const rawBalance = await contract.balanceOf(address).call();
// USDT on TRON has 6 decimals
const balance = Number(rawBalance) / 1_000_000;
setState(s => ({
...s,
tronUsdtBalance: balance.toFixed(2),
tronUsdtBalanceLoading: false,
}));
} catch (err) {
console.warn("[useTronBridge] fetchTronUsdtBalance error:", err);
setState(s => ({ ...s, tronUsdtBalance: null, tronUsdtBalanceLoading: false }));
}
}, [state.tronAddress]);
// Auto-fetch balance when connected
useEffect(() => {
if (state.tronAddress && state.tronConnected) {
fetchTronUsdtBalance();
}
}, [state.tronAddress, state.tronConnected]);
// Send TRC20 USDT transfer
const sendTrc20Transfer = useCallback(async ({
toAddress,
usdtAmount,
}: {
toAddress: string;
usdtAmount: number;
}): Promise<{ txHash: string } | null> => {
const address = state.tronAddress;
if (!address) {
setState(s => ({ ...s, tronTransferError: "TRON wallet not connected" }));
return null;
}
setState(s => ({
...s,
tronTransferring: true,
tronTransferError: null,
tronTransferTxHash: null,
tronTransferSuccess: false,
}));
try {
const tw = getTronWeb();
if (!tw) throw new Error("TronWeb not available");
const contract = await tw.contract(TRC20_ABI, TRON_USDT_CONTRACT);
// USDT on TRON has 6 decimals
const amountSun = BigInt(Math.round(usdtAmount * 1_000_000));
// Send transfer — TronLink will prompt for confirmation
const txHash = await contract.transfer(toAddress, amountSun).send({
feeLimit: 100_000_000, // 100 TRX fee limit
});
if (!txHash || typeof txHash !== "string") {
throw new Error("Transaction failed — no hash returned");
}
setState(s => ({
...s,
tronTransferring: false,
tronTransferSuccess: true,
tronTransferTxHash: txHash,
}));
// Refresh balance after 3s
setTimeout(() => fetchTronUsdtBalance(), 3000);
return { txHash };
} catch (err: unknown) {
const error = err as { message?: string; code?: number };
let msg: string;
if (
error?.message?.toLowerCase().includes("cancel") ||
error?.message?.toLowerCase().includes("reject") ||
error?.message?.toLowerCase().includes("denied") ||
error?.code === 4001
) {
msg = "Transaction cancelled by user";
} else if (error?.message?.toLowerCase().includes("insufficient")) {
msg = "Insufficient USDT balance or TRX for fees";
} else if (
error?.message?.toLowerCase().includes("bandwidth") ||
error?.message?.toLowerCase().includes("energy")
) {
msg = "Insufficient TRX bandwidth/energy. Please freeze some TRX for resources.";
} else {
msg = error?.message || "Transaction failed";
}
setState(s => ({
...s,
tronTransferring: false,
tronTransferError: msg,
}));
return null;
}
}, [state.tronAddress, fetchTronUsdtBalance]);
const resetTronTransferState = useCallback(() => {
setState(s => ({
...s,
tronTransferring: false,
tronTransferError: null,
tronTransferTxHash: null,
tronTransferSuccess: false,
}));
}, []);
return {
...state,
connectTronLink,
disconnectTron,
fetchTronUsdtBalance,
sendTrc20Transfer,
resetTronTransferState,
};
}

View File

@ -1,10 +1,10 @@
// NAC XIC Presale — Wallet Connection Hook
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
// v5: forceConnect accepts specific provider, exposes watchAsset method
// Supports MetaMask, TokenPocket, OKX, Bitget, Trust Wallet, imToken, SafePal, and all EVM wallets
// v5: added connectWithProvider() to fix state sync when WalletSelector connects via external provider
import { useState, useEffect, useCallback, useRef } from "react";
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
import { shortenAddress, switchToNetwork, CONTRACTS, PRESALE_CONFIG } from "@/lib/contracts";
import { shortenAddress, switchToNetwork } from "@/lib/contracts";
export type NetworkType = "BSC" | "ETH" | "TRON";
@ -30,104 +30,65 @@ const INITIAL_STATE: WalletState = {
error: null,
};
export type EthProvider = Eip1193Provider & {
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
providers?: EthProvider[];
};
// Detect the best available EVM provider across all major wallets
// Priority order: TP Wallet (most popular in China) > OKX > Bitget > Trust > MetaMask > others
export function detectProvider(): Eip1193Provider | null {
// Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others
export function detectProvider(): EthProvider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as Record<string, unknown>;
type ExtProvider = Eip1193Provider & {
providers?: ExtProvider[];
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
isRabby?: boolean;
isSafePal?: boolean;
isImToken?: boolean;
isPhantom?: boolean;
};
const eth = w.ethereum as ExtProvider | undefined;
// Helper: find in providers array or direct
const findIn = (pred: (p: ExtProvider) => boolean): ExtProvider | null => {
if (!eth) return null;
if (eth.providers && Array.isArray(eth.providers)) {
return eth.providers.find(pred) ?? null;
// 1. Check window.ethereum (most wallets inject here)
const eth = w.ethereum as EthProvider | undefined;
if (eth) {
// Check providers array first (multiple extensions installed)
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
// Priority order for Chinese users
const tp = eth.providers.find((p: EthProvider) => p.isTokenPocket);
if (tp) return tp;
const okx = eth.providers.find((p: EthProvider) => p.isOKExWallet || p.isOkxWallet);
if (okx) return okx;
const bitget = eth.providers.find((p: EthProvider) => p.isBitkeep || p.isBitgetWallet);
if (bitget) return bitget;
const trust = eth.providers.find((p: EthProvider) => p.isTrust || p.isTrustWallet);
if (trust) return trust;
const metamask = eth.providers.find((p: EthProvider) => p.isMetaMask);
if (metamask) return metamask;
return eth.providers[0];
}
return pred(eth) ? eth : null;
};
// 1. TokenPocket (most popular in China)
const tp = findIn(p => !!p.isTokenPocket);
if (tp) return tp;
// 2. OKX Wallet
const okx = (w.okxwallet as ExtProvider | undefined) ?? findIn(p => !!(p.isOKExWallet || p.isOkxWallet));
if (okx) return okx;
// 3. Bitget Wallet
const bitget = (w as { bitkeep?: { ethereum?: ExtProvider } }).bitkeep?.ethereum
?? findIn(p => !!(p.isBitkeep || p.isBitgetWallet));
if (bitget) return bitget;
// 4. imToken
const imtoken = (w.imToken as ExtProvider | undefined) ?? findIn(p => !!p.isImToken);
if (imtoken) return imtoken;
// 5. SafePal
const safepal = (w.safepalProvider as ExtProvider | undefined) ?? findIn(p => !!p.isSafePal);
if (safepal) return safepal;
// 6. Trust Wallet
const trust = findIn(p => !!(p.isTrust || p.isTrustWallet));
if (trust) return trust;
// 7. MetaMask
const metamask = findIn(p => !!p.isMetaMask);
if (metamask) return metamask;
if (!eth) {
// Last resort: check wallet-specific globals
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
return null;
// Single provider — return it directly
return eth;
}
// If multiple providers injected but none matched above, use first
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
return eth.providers[0];
}
// 2. OKX Wallet — sometimes injects window.okxwallet separately
if (w.okxwallet) return w.okxwallet as EthProvider;
return eth;
}
// 3. Bitget Wallet — sometimes injects window.bitkeep.ethereum
const bitkeep = w.bitkeep as { ethereum?: EthProvider } | undefined;
if (bitkeep?.ethereum) return bitkeep.ethereum;
// Check if MetaMask is installed but not yet initialized (no wallet created/imported)
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
try {
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts",
});
void accounts;
return { ready: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
if (error?.code === -32002) {
return { ready: false, reason: "pending" };
}
return { ready: false, reason: error?.message || "unknown" };
}
// 4. Coinbase Wallet
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as EthProvider;
return null;
}
// Build wallet state from a provider and accounts
async function buildWalletState(
rawProvider: Eip1193Provider,
rawProvider: EthProvider,
address: string
): Promise<Partial<WalletState>> {
const provider = new BrowserProvider(rawProvider);
@ -164,12 +125,17 @@ async function buildWalletState(
};
}
export function useWallet() {
export type WalletHookReturn = WalletState & {
connect: () => Promise<{ success: boolean; error?: string }>;
connectWithProvider: (rawProvider: EthProvider, address: string) => Promise<void>;
disconnect: () => void;
switchNetwork: (chainId: number) => Promise<void>;
};
export function useWallet(): WalletHookReturn {
const [state, setState] = useState<WalletState>(INITIAL_STATE);
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const mountedRef = useRef(true);
// Track the raw provider used for the current connection (for watchAsset, switchNetwork, etc.)
const rawProviderRef = useRef<Eip1193Provider | null>(null);
useEffect(() => {
mountedRef.current = true;
@ -179,12 +145,36 @@ export function useWallet() {
};
}, []);
// ── Connect (explicit user action) ─────────────────────────────────────────
// ── Connect via external provider (called from WalletSelector) ─────────────
// KEY FIX: WalletSelector already has the provider and address from the wallet popup.
// We update state directly without calling connect() again (which would use detectProvider()
// and might pick the wrong wallet or fail if the wallet injects to a different window property).
const connectWithProvider = useCallback(async (rawProvider: EthProvider, address: string) => {
if (!mountedRef.current) return;
setState(s => ({ ...s, isConnecting: true, error: null }));
try {
const partial = await buildWalletState(rawProvider, address);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch {
// Fallback: at minimum set address and isConnected = true
if (mountedRef.current) {
setState({
...INITIAL_STATE,
address,
shortAddress: shortenAddress(address),
isConnected: true,
isConnecting: false,
});
}
}
}, []);
// ── Connect (explicit user action via detectProvider) ──────────────────────
const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
const rawProvider = detectProvider();
if (!rawProvider) {
const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。";
const msg = "未检测到钱包插件。请安装 TokenPocket、MetaMask 或其他 EVM 兼容钱包后刷新页面。";
if (mountedRef.current) setState(s => ({ ...s, error: msg }));
return { success: false, error: msg };
}
@ -192,6 +182,7 @@ export function useWallet() {
setState(s => ({ ...s, isConnecting: true, error: null }));
try {
// Request accounts — this triggers the wallet popup
const accounts = await (rawProvider as {
request: (args: { method: string; params?: unknown[] }) => Promise<string[]>
}).request({
@ -203,7 +194,6 @@ export function useWallet() {
throw new Error("no_accounts");
}
rawProviderRef.current = rawProvider;
const partial = await buildWalletState(rawProvider, accounts[0]);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
return { success: true };
@ -215,15 +205,9 @@ export function useWallet() {
if (error?.code === 4001) {
msg = "已取消连接 / Connection cancelled";
} else if (error?.code === -32002) {
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。";
msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。";
} else if (error?.message === "no_accounts") {
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
} else if (
error?.message?.toLowerCase().includes("not initialized") ||
error?.message?.toLowerCase().includes("setup") ||
error?.message?.toLowerCase().includes("onboarding")
) {
msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。";
} else {
msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`;
}
@ -233,54 +217,18 @@ export function useWallet() {
}
}, []);
// ── Force connect with known address and specific provider (from WalletSelector callback) ─────
// Use this when WalletSelector has already called eth_requestAccounts and got the address.
// Pass the specific provider used (OKX/MetaMask/TP) so all subsequent operations use the right wallet.
const forceConnect = useCallback(async (address: string, specificProvider?: Eip1193Provider): Promise<void> => {
if (!address) return;
// Use the provided specific provider first, then fall back to detectProvider
const rawProvider = specificProvider ?? detectProvider();
if (!rawProvider) {
if (mountedRef.current) {
setState({
...INITIAL_STATE,
address,
shortAddress: shortenAddress(address),
isConnected: true,
});
}
return;
}
rawProviderRef.current = rawProvider;
try {
const partial = await buildWalletState(rawProvider, address);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch {
if (mountedRef.current) {
setState({
...INITIAL_STATE,
address,
shortAddress: shortenAddress(address),
isConnected: true,
});
}
}
}, []);
// ── Disconnect ──────────────────────────────────────────────────────────────
const disconnect = useCallback(() => {
rawProviderRef.current = null;
setState(INITIAL_STATE);
}, []);
// ── Switch Network ──────────────────────────────────────────────────────────
const switchNetwork = useCallback(async (chainId: number) => {
try {
// Use the tracked raw provider (user's chosen wallet)
const rp = rawProviderRef.current ?? detectProvider();
await switchToNetwork(chainId, rp as { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> } | undefined);
if (rp) {
const provider = new BrowserProvider(rp);
await switchToNetwork(chainId);
const rawProvider = detectProvider();
if (rawProvider) {
const provider = new BrowserProvider(rawProvider);
const network = await provider.getNetwork();
let signer: JsonRpcSigner | null = null;
try { signer = await provider.getSigner(); } catch { /* ignore */ }
@ -299,32 +247,6 @@ export function useWallet() {
}
}, []);
// ── Watch Asset (add token to wallet) ──────────────────────────────────────
// Calls wallet_watchAsset on the user's connected wallet (correct provider)
const watchAsset = useCallback(async (network: "BSC" | "ETH" = "BSC"): Promise<boolean> => {
const rp = rawProviderRef.current ?? detectProvider();
if (!rp) return false;
try {
const tokenAddress = network === "BSC" ? CONTRACTS.BSC.token : CONTRACTS.ETH.token;
if (!tokenAddress) return false;
await (rp as { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> }).request({
method: "wallet_watchAsset",
params: [{
type: "ERC20",
options: {
address: tokenAddress,
symbol: PRESALE_CONFIG.tokenSymbol,
decimals: PRESALE_CONFIG.tokenDecimals,
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
},
}],
});
return true;
} catch {
return false;
}
}, []);
// ── Auto-detect on page load (silent, no popup) ─────────────────────────────
useEffect(() => {
let cancelled = false;
@ -334,28 +256,29 @@ export function useWallet() {
const rawProvider = detectProvider();
if (!rawProvider) {
if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
if (attempt < 5) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt);
}
return;
}
try {
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts",
method: "eth_accounts", // Silent — no popup
});
if (cancelled) return;
if (accounts && accounts.length > 0) {
rawProviderRef.current = rawProvider;
const partial = await buildWalletState(rawProvider, accounts[0]);
if (!cancelled && mountedRef.current) {
setState({ ...INITIAL_STATE, ...partial });
}
} else if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
} else if (attempt < 5) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
}
} catch {
// Silently ignore — user hasn't connected yet
if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000);
}
}
};
@ -382,7 +305,6 @@ export function useWallet() {
const accs = accounts as string[];
if (!mountedRef.current) return;
if (!accs || accs.length === 0) {
rawProviderRef.current = null;
setState(INITIAL_STATE);
} else {
try {
@ -432,5 +354,5 @@ export function useWallet() {
};
}, []);
return { ...state, connect, forceConnect, disconnect, switchNetwork, watchAsset };
return { ...state, connect, connectWithProvider, disconnect, switchNetwork };
}

View File

@ -1,360 +0,0 @@
// NAC XIC Presale — Wallet Connection Hook
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
// v4: added forceConnect(address) for WalletSelector callback sync
import { useState, useEffect, useCallback, useRef } from "react";
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
import { shortenAddress, switchToNetwork } from "@/lib/contracts";
export type NetworkType = "BSC" | "ETH" | "TRON";
export interface WalletState {
address: string | null;
shortAddress: string;
isConnected: boolean;
chainId: number | null;
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
isConnecting: boolean;
error: string | null;
}
const INITIAL_STATE: WalletState = {
address: null,
shortAddress: "",
isConnected: false,
chainId: null,
provider: null,
signer: null,
isConnecting: false,
error: null,
};
// Detect the best available EVM provider across all major wallets
export function detectProvider(): Eip1193Provider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as Record<string, unknown>;
const eth = w.ethereum as (Eip1193Provider & {
providers?: Eip1193Provider[];
isMetaMask?: boolean;
isTrust?: boolean;
isOKExWallet?: boolean;
isCoinbaseWallet?: boolean;
}) | undefined;
if (!eth) {
// Fallback: check wallet-specific globals
if (w.okxwallet) return w.okxwallet as Eip1193Provider;
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
return null;
}
// If multiple providers are injected (common when multiple extensions installed)
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask);
return metamask ?? eth.providers[0];
}
return eth;
}
// Check if MetaMask is installed but not yet initialized (no wallet created/imported)
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
try {
// eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts",
});
// If we get here, the wallet is at least initialized (even if locked / no accounts)
return { ready: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
// -32002: Request already pending (MetaMask not initialized or another request pending)
if (error?.code === -32002) {
return { ready: false, reason: "pending" };
}
// Any other error — treat as not ready
return { ready: false, reason: error?.message || "unknown" };
}
}
// Build wallet state from a provider and accounts
async function buildWalletState(
rawProvider: Eip1193Provider,
address: string
): Promise<Partial<WalletState>> {
const provider = new BrowserProvider(rawProvider);
let chainId: number | null = null;
let signer: JsonRpcSigner | null = null;
try {
const network = await provider.getNetwork();
chainId = Number(network.chainId);
} catch {
try {
const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise<string> }).request({ method: "eth_chainId" });
chainId = parseInt(chainHex, 16);
} catch {
chainId = null;
}
}
try {
signer = await provider.getSigner();
} catch {
signer = null;
}
return {
address,
shortAddress: shortenAddress(address),
isConnected: true,
chainId,
provider,
signer,
isConnecting: false,
error: null,
};
}
export function useWallet() {
const [state, setState] = useState<WalletState>(INITIAL_STATE);
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (retryRef.current) clearTimeout(retryRef.current);
};
}, []);
// ── Connect (explicit user action) ─────────────────────────────────────────
const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
const rawProvider = detectProvider();
if (!rawProvider) {
const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。";
if (mountedRef.current) setState(s => ({ ...s, error: msg }));
return { success: false, error: msg };
}
setState(s => ({ ...s, isConnecting: true, error: null }));
try {
// Request accounts — this triggers the wallet popup
const accounts = await (rawProvider as {
request: (args: { method: string; params?: unknown[] }) => Promise<string[]>
}).request({
method: "eth_requestAccounts",
params: [],
});
if (!accounts || accounts.length === 0) {
throw new Error("no_accounts");
}
const partial = await buildWalletState(rawProvider, accounts[0]);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
return { success: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
let msg: string;
if (error?.code === 4001) {
// User rejected
msg = "已取消连接 / Connection cancelled";
} else if (error?.code === -32002) {
// MetaMask has a pending request — usually means it's not initialized or popup is already open
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。";
} else if (error?.message === "no_accounts") {
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
} else if (
error?.message?.toLowerCase().includes("not initialized") ||
error?.message?.toLowerCase().includes("setup") ||
error?.message?.toLowerCase().includes("onboarding")
) {
msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。";
} else {
msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`;
}
if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg }));
return { success: false, error: msg };
}
}, []);
// ── Force connect with known address (from WalletSelector callback) ─────────
// Use this when WalletSelector has already called eth_requestAccounts and got the address.
// Directly builds wallet state without triggering another popup.
const forceConnect = useCallback(async (address: string): Promise<void> => {
if (!address) return;
const rawProvider = detectProvider();
if (!rawProvider) {
// No provider available — set minimal connected state with just the address
if (mountedRef.current) {
setState({
...INITIAL_STATE,
address,
shortAddress: shortenAddress(address),
isConnected: true,
});
}
return;
}
try {
const partial = await buildWalletState(rawProvider, address);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch {
// Fallback: set minimal state
if (mountedRef.current) {
setState({
...INITIAL_STATE,
address,
shortAddress: shortenAddress(address),
isConnected: true,
});
}
}
}, []);
// ── Disconnect ──────────────────────────────────────────────────────────────
const disconnect = useCallback(() => {
setState(INITIAL_STATE);
}, []);
// ── Switch Network ──────────────────────────────────────────────────────────
const switchNetwork = useCallback(async (chainId: number) => {
try {
await switchToNetwork(chainId);
const rawProvider = detectProvider();
if (rawProvider) {
const provider = new BrowserProvider(rawProvider);
const network = await provider.getNetwork();
let signer: JsonRpcSigner | null = null;
try { signer = await provider.getSigner(); } catch { /* ignore */ }
if (mountedRef.current) {
setState(s => ({
...s,
chainId: Number(network.chainId),
provider,
signer,
error: null,
}));
}
}
} catch (err: unknown) {
if (mountedRef.current) setState(s => ({ ...s, error: (err as Error).message }));
}
}, []);
// ── Auto-detect on page load (silent, no popup) ─────────────────────────────
useEffect(() => {
let cancelled = false;
const tryAutoDetect = async (attempt: number) => {
if (cancelled) return;
const rawProvider = detectProvider();
if (!rawProvider) {
if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
}
return;
}
try {
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts", // Silent — no popup
});
if (cancelled) return;
if (accounts && accounts.length > 0) {
const partial = await buildWalletState(rawProvider, accounts[0]);
if (!cancelled && mountedRef.current) {
setState({ ...INITIAL_STATE, ...partial });
}
} else if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
}
} catch {
// Silently ignore — user hasn't connected yet
}
};
retryRef.current = setTimeout(() => tryAutoDetect(1), 300);
return () => {
cancelled = true;
if (retryRef.current) clearTimeout(retryRef.current);
};
}, []);
// ── Listen for account / chain changes ─────────────────────────────────────
useEffect(() => {
const rawProvider = detectProvider();
if (!rawProvider) return;
const eth = rawProvider as {
on?: (event: string, handler: (data: unknown) => void) => void;
removeListener?: (event: string, handler: (data: unknown) => void) => void;
};
if (!eth.on) return;
const handleAccountsChanged = async (accounts: unknown) => {
const accs = accounts as string[];
if (!mountedRef.current) return;
if (!accs || accs.length === 0) {
setState(INITIAL_STATE);
} else {
try {
const partial = await buildWalletState(rawProvider, accs[0]);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch {
if (mountedRef.current) {
setState(s => ({
...s,
address: accs[0],
shortAddress: shortenAddress(accs[0]),
isConnected: true,
}));
}
}
}
};
const handleChainChanged = async () => {
if (!mountedRef.current) return;
try {
const provider = new BrowserProvider(rawProvider);
const network = await provider.getNetwork();
let signer: JsonRpcSigner | null = null;
try { signer = await provider.getSigner(); } catch { /* ignore */ }
if (mountedRef.current) {
setState(s => ({
...s,
chainId: Number(network.chainId),
provider,
signer,
}));
}
} catch {
window.location.reload();
}
};
eth.on("accountsChanged", handleAccountsChanged);
eth.on("chainChanged", handleChainChanged);
return () => {
if (eth.removeListener) {
eth.removeListener("accountsChanged", handleAccountsChanged);
eth.removeListener("chainChanged", handleChainChanged);
}
};
}, []);
return { ...state, connect, forceConnect, disconnect, switchNetwork };
}

View File

@ -0,0 +1,194 @@
/**
* addTokenToWallet Seamless token auto-add after purchase
*
* References:
* - EIP-747: wallet_watchAsset standard
* - Document: "代币购买钱包自动添加无缝体验实现方案"
* - Document: "wallet_watchAsset TOP10钱包支持情况"
* - Document: "EIP-747避坑指南"
*
* EVM wallets (MetaMask, Trust, OKX, Coinbase, TokenPocket, Bitget, Rabby):
* window.ethereum.request({ method: 'wallet_watchAsset', params: { type: 'ERC20', options: {...} } })
*
* TRON wallets (TronLink, TokenPocket TRON mode):
* window.tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
* NOTE: TronLink auto-fetches symbol/decimals from contract only address is required.
*
* Key rules (from EIP-747 + wallet docs):
* 1. symbol MUST be 11 characters (MetaMask strict limit will return -32602 if longer)
* 2. symbol should match on-chain contract.symbol() to avoid MetaMask rejection
* 3. Must be triggered by user action (button click), NEVER on page load
* 4. Ensure wallet chainId matches token's chain before calling (use wallet_switchEthereumChain first)
* 5. Returns true immediately does NOT wait for user to confirm
*/
// XIC Token metadata — symbol "XIC" is 3 chars, well within the 11-char limit
export const XIC_TOKEN = {
// EVM contract addresses per chain (update when deployed)
evmAddresses: {
56: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", // BSC
1: "0x0000000000000000000000000000000000000000", // ETH (TBD)
137: "0x0000000000000000000000000000000000000000", // Polygon (TBD)
42161: "0x0000000000000000000000000000000000000000", // Arbitrum (TBD)
43114: "0x0000000000000000000000000000000000000000", // Avalanche (TBD)
} as Record<number, string>,
// TRON TRC-20 contract address (Base58 format) — update when deployed
tronAddress: "TXICTokenAddressHere", // TODO: update with actual TRC-20 XIC address
symbol: "XIC", // ≤ 11 chars ✓ (3 chars)
decimals: 18,
name: "New AssetChain XIC Token",
imageUrl: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
};
export type ChainType = "ERC20" | "TRC20";
export interface AddTokenResult {
success: boolean;
cancelled?: boolean;
error?: string;
}
/**
* Add XIC token to EVM wallet using wallet_watchAsset (EIP-747 / EIP-1193)
* Compatible with MetaMask, Trust Wallet, OKX, Coinbase, TokenPocket, Bitget, Rabby, imToken.
*
* Per EIP-747 docs: use window.ethereum.request directly.
* Per EIP-747 avoid-pitfalls doc: ensure chainId matches before calling.
*/
export async function addXicToEvmWallet(
chainId?: number,
provider?: { request?: (args: { method: string; params?: unknown }) => Promise<unknown> } | null
): Promise<AddTokenResult> {
// Use provided provider or fall back to window.ethereum
const eth = provider?.request
? provider
: (typeof window !== "undefined"
? (window as unknown as { ethereum?: { request: (args: { method: string; params?: unknown }) => Promise<unknown> } }).ethereum
: null);
if (!eth?.request) {
return { success: false, error: "No EVM wallet detected. Please install MetaMask or a compatible wallet." };
}
// Determine contract address for this chain
const address = chainId ? (XIC_TOKEN.evmAddresses[chainId] ?? XIC_TOKEN.evmAddresses[56]) : XIC_TOKEN.evmAddresses[56];
// Skip if contract not yet deployed on this chain
if (address === "0x0000000000000000000000000000000000000000") {
return { success: false, error: `XIC contract not yet deployed on chain ${chainId}` };
}
try {
// Per EIP-747: wallet_watchAsset with type "ERC20"
// symbol must be ≤ 11 chars and should match on-chain contract.symbol()
await eth.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address,
symbol: XIC_TOKEN.symbol, // "XIC" — 3 chars ✓
decimals: XIC_TOKEN.decimals,
image: XIC_TOKEN.imageUrl,
},
},
});
// Per EIP-747: returns true immediately, does NOT indicate user confirmed
console.log("[addTokenToWallet] XIC wallet_watchAsset request sent to EVM wallet");
return { success: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
if (error?.code === 4001) {
// User rejected the request
return { success: false, cancelled: true };
}
if (error?.code === 4100) {
// Method not supported by this wallet
return { success: false, error: "This wallet does not support wallet_watchAsset" };
}
if (error?.message?.includes("longer than 11")) {
// -32602: symbol too long (should not happen with "XIC" but guard anyway)
return { success: false, error: "Token symbol rejected by wallet" };
}
console.warn("[addTokenToWallet] wallet_watchAsset EVM error:", err);
return { success: false, error: error?.message || "Failed to add token to wallet" };
}
}
/**
* Add XIC TRC-20 token to TronLink wallet.
*
* Per TronLink official API docs:
* tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
*
* type values: 'trc10' | 'trc20' | 'trc721'
* For TRC-20: only address is required TronLink auto-fetches symbol/decimals from contract.
* TronLink will show a confirmation popup to the user.
*/
export async function addXicToTronWallet(): Promise<AddTokenResult> {
const tronWeb = typeof window !== "undefined"
? (window as unknown as {
tronWeb?: {
defaultAddress?: { base58?: string };
request: (args: { method: string; params?: unknown }) => Promise<unknown>;
}
}).tronWeb
: null;
if (!tronWeb) {
return { success: false, error: "TronLink not detected. Please install TronLink wallet." };
}
if (!tronWeb.defaultAddress?.base58) {
return { success: false, error: "TronLink is locked. Please unlock your wallet first." };
}
// Skip if TRC-20 contract not yet deployed
if (XIC_TOKEN.tronAddress === "TXICTokenAddressHere") {
return { success: false, error: "XIC TRC-20 contract not yet deployed" };
}
try {
// Per TronLink official API: wallet_watchAsset with type 'trc20'
// TronLink auto-fetches symbol, decimals, name from the contract
await tronWeb.request({
method: "wallet_watchAsset",
params: {
type: "trc20",
options: {
address: XIC_TOKEN.tronAddress, // Base58 TRC-20 contract address
},
},
});
console.log("[addTokenToWallet] XIC wallet_watchAsset request sent to TronLink");
return { success: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
if (error?.code === 4001) {
return { success: false, cancelled: true };
}
console.warn("[addTokenToWallet] wallet_watchAsset TRON error:", err);
// Non-fatal — some older TronLink versions may not support this
return { success: false, error: error?.message || "Failed to add TRC-20 token to TronLink" };
}
}
/**
* Main function: add XIC to wallet based on chain type.
* Call this after a successful purchase to prompt the user to add XIC.
*
* Per EIP-747 security rule: MUST be triggered by user action, never auto-called on page load.
*/
export async function addXicToWallet(
chainType: ChainType,
chainId?: number,
evmProvider?: { request?: (args: { method: string; params?: unknown }) => Promise<unknown> } | null
): Promise<AddTokenResult> {
if (chainType === "ERC20") {
return addXicToEvmWallet(chainId, evmProvider);
} else {
return addXicToTronWallet();
}
}

View File

@ -1,6 +1,6 @@
// NAC XIC Token Presale — Contract Configuration v2
// New Contract: XICPresale (购买即时发放版本)
// 预售总量: 25亿 XIC | 价格: $0.02/XIC | 时长: 180天 | 无购买上下限
// NAC XIC Token Presale — Contract Configuration
// Design: Dark Cyberpunk / Quantum Finance
// Colors: Amber Gold #f0b429, Quantum Blue #00d4ff, Deep Black #0a0a0f
// ============================================================
// CONTRACT ADDRESSES
@ -13,8 +13,8 @@ export const CONTRACTS = {
rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com",
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
presale: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2 — 购买即时发放
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
usdt: "0x55d398326f99059fF775485246999027B3197955",
},
// Ethereum Mainnet (Chain ID: 1)
@ -24,19 +24,20 @@ export const CONTRACTS = {
rpcUrl: "https://eth.llamarpc.com",
explorerUrl: "https://etherscan.io",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
token: "",
presale: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
token: "", // XIC not yet on ETH
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
},
// TRON (TRC20) — Manual transfer
TRON: {
chainId: 0,
chainId: 0, // Not EVM
chainName: "TRON",
explorerUrl: "https://tronscan.org",
presale: "",
presale: "", // TRC20 uses manual transfer
token: "",
usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
receivingWallet: "TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb",
// Receiving wallet for TRC20 USDT
receivingWallet: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
},
} as const;
@ -44,125 +45,23 @@ export const CONTRACTS = {
// PRESALE PARAMETERS
// ============================================================
export const PRESALE_CONFIG = {
tokenPrice: 0.02, // $0.02 per XIC
tokenPrice: 0.02, // $0.02 per XIC
tokenSymbol: "XIC",
tokenName: "New AssetChain Token",
tokenDecimals: 18,
minPurchaseUSDT: 0, // 无最小购买限制
maxPurchaseUSDT: 0, // 无最大购买限制0 = 无限制)
totalSupply: 100_000_000_000, // 1000亿 XIC 总量
presaleAllocation: 2_500_000_000, // 25亿 XIC 预售总量
presaleDurationDays: 180, // 预售时长 180天
minPurchaseUSDT: 0, // No minimum purchase limit
maxPurchaseUSDT: 50000, // Max $50,000 USDT per purchase
totalSupply: 100_000_000_000, // 100 billion XIC
presaleAllocation: 2_500_000_000, // 2.5 billion for presale (25亿)
// TRC20 memo format
trc20Memo: "XIC_PRESALE",
};
// ============================================================
// NEW PRESALE CONTRACT ABI (XICPresale v2 — 购买即时发放)
// PRESALE CONTRACT ABI (BSC & ETH — same interface)
// ============================================================
export const PRESALE_ABI = [
// ── Read Functions ──────────────────────────────────────
{
"inputs": [],
"name": "PRESALE_DURATION",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "availableXIC",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "calculateTokenAmount",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "bnbAmount", "type": "uint256" }],
"name": "calculateTokenAmountForBNB",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getBNBPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isPresaleActive",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleEndTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presalePaused",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleProgress",
"outputs": [
{ "internalType": "uint256", "name": "sold", "type": "uint256" },
{ "internalType": "uint256", "name": "cap", "type": "uint256" },
{ "internalType": "uint256", "name": "progressBps", "type": "uint256" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStartTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStarted",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "timeRemaining",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
// Read functions
{
"inputs": [],
"name": "tokenPrice",
@ -170,6 +69,13 @@ export const PRESALE_ABI = [
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokensSold",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalRaised",
@ -179,107 +85,55 @@ export const PRESALE_ABI = [
},
{
"inputs": [],
"name": "totalTokensSold",
"name": "presaleActive",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"inputs": [{ "internalType": "address", "name": "user", "type": "address" }],
"name": "userPurchases",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
// Write functions
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "userSpent",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "buyTokensWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "wallet",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "xicToken",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
// ── Write Functions ─────────────────────────────────────
{
"inputs": [],
"name": "buyWithBNB",
"name": "buyTokens",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "buyWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "startPresale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "bool", "name": "_paused", "type": "bool" }],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "recoverUnsoldTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }],
"name": "emergencyWithdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
// ── Events ──────────────────────────────────────────────
// Events
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "buyer", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "usdtAmount", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "tokenAmount", "type": "uint256" },
{ "indexed": false, "internalType": "string", "name": "paymentMethod", "type": "string" }
{ "indexed": false, "internalType": "uint256", "name": "tokenAmount", "type": "uint256" }
],
"name": "TokensPurchased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "uint256", "name": "startTime", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "endTime", "type": "uint256" }
],
"name": "PresaleStarted",
"type": "event"
}
] as const;
// ============================================================
// ERC20 USDT ABI (minimal)
// ERC20 USDT ABI (minimal — approve + allowance + balanceOf)
// ============================================================
export const ERC20_ABI = [
{
@ -330,6 +184,7 @@ export async function switchToNetwork(chainId: number): Promise<void> {
params: [{ chainId: hexChainId }],
});
} catch (err: unknown) {
// Chain not added yet — add it
if ((err as { code?: number }).code === 4902) {
const network = Object.values(CONTRACTS).find(n => n.chainId === chainId);
if (!network || !("rpcUrl" in network)) throw new Error("Unknown network");
@ -364,6 +219,7 @@ export function shortenAddress(addr: string): string {
return addr.slice(0, 6) + "..." + addr.slice(-4);
}
// Declare window.ethereum for TypeScript
declare global {
interface Window {
ethereum?: {

View File

@ -1,377 +0,0 @@
// NAC XIC Token Presale — Contract Configuration v2
// New Contract: XICPresale (购买即时发放版本)
// 预售总量: 25亿 XIC | 价格: $0.02/XIC | 时长: 180天 | 无购买上下限
// ============================================================
// CONTRACT ADDRESSES
// ============================================================
export const CONTRACTS = {
// BSC Mainnet (Chain ID: 56)
BSC: {
chainId: 56,
chainName: "BNB Smart Chain",
rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com",
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
// ⚠️ 新合约地址(部署后更新此处)
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4",
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
usdt: "0x55d398326f99059fF775485246999027B3197955",
},
// Ethereum Mainnet (Chain ID: 1)
ETH: {
chainId: 1,
chainName: "Ethereum",
rpcUrl: "https://eth.llamarpc.com",
explorerUrl: "https://etherscan.io",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
token: "",
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
},
// TRON (TRC20) — Manual transfer
TRON: {
chainId: 0,
chainName: "TRON",
explorerUrl: "https://tronscan.org",
presale: "",
token: "",
usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
receivingWallet: "TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb",
},
} as const;
// ============================================================
// PRESALE PARAMETERS
// ============================================================
export const PRESALE_CONFIG = {
tokenPrice: 0.02, // $0.02 per XIC
tokenSymbol: "XIC",
tokenName: "New AssetChain Token",
tokenDecimals: 18,
minPurchaseUSDT: 0, // 无最小购买限制
maxPurchaseUSDT: 0, // 无最大购买限制0 = 无限制)
totalSupply: 100_000_000_000, // 1000亿 XIC 总量
presaleAllocation: 2_500_000_000, // 25亿 XIC 预售总量
presaleDurationDays: 180, // 预售时长 180天
trc20Memo: "XIC_PRESALE",
};
// ============================================================
// NEW PRESALE CONTRACT ABI (XICPresale v2 — 购买即时发放)
// ============================================================
export const PRESALE_ABI = [
// ── Read Functions ──────────────────────────────────────
{
"inputs": [],
"name": "PRESALE_DURATION",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "availableXIC",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "calculateTokenAmount",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "bnbAmount", "type": "uint256" }],
"name": "calculateTokenAmountForBNB",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getBNBPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isPresaleActive",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleEndTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presalePaused",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleProgress",
"outputs": [
{ "internalType": "uint256", "name": "sold", "type": "uint256" },
{ "internalType": "uint256", "name": "cap", "type": "uint256" },
{ "internalType": "uint256", "name": "progressBps", "type": "uint256" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStartTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStarted",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "timeRemaining",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalRaised",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokensSold",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "userPurchases",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "userSpent",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "wallet",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "xicToken",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
// ── Write Functions ─────────────────────────────────────
{
"inputs": [],
"name": "buyWithBNB",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "buyWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "startPresale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "bool", "name": "_paused", "type": "bool" }],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "recoverUnsoldTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }],
"name": "emergencyWithdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
// ── Events ──────────────────────────────────────────────
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "buyer", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "usdtAmount", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "tokenAmount", "type": "uint256" },
{ "indexed": false, "internalType": "string", "name": "paymentMethod", "type": "string" }
],
"name": "TokensPurchased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "uint256", "name": "startTime", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "endTime", "type": "uint256" }
],
"name": "PresaleStarted",
"type": "event"
}
] as const;
// ============================================================
// ERC20 USDT ABI (minimal)
// ============================================================
export const ERC20_ABI = [
{
"inputs": [
{ "internalType": "address", "name": "spender", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "approve",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "owner", "type": "address" },
{ "internalType": "address", "name": "spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "account", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
"stateMutability": "view",
"type": "function"
}
] as const;
// ============================================================
// NETWORK SWITCH HELPER
// ============================================================
export async function switchToNetwork(chainId: number): Promise<void> {
if (!window.ethereum) throw new Error("No wallet detected");
const hexChainId = "0x" + chainId.toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }],
});
} catch (err: unknown) {
if ((err as { code?: number }).code === 4902) {
const network = Object.values(CONTRACTS).find(n => n.chainId === chainId);
if (!network || !("rpcUrl" in network)) throw new Error("Unknown network");
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: hexChainId,
chainName: network.chainName,
rpcUrls: [(network as { rpcUrl: string }).rpcUrl],
nativeCurrency: (network as { nativeCurrency: { name: string; symbol: string; decimals: number } }).nativeCurrency,
blockExplorerUrls: [network.explorerUrl],
}],
});
} else {
throw err;
}
}
}
// ============================================================
// FORMAT HELPERS
// ============================================================
export function formatNumber(n: number, decimals = 2): string {
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(decimals) + "B";
if (n >= 1_000_000) return (n / 1_000_000).toFixed(decimals) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(decimals) + "K";
return n.toFixed(decimals);
}
export function shortenAddress(addr: string): string {
if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4);
}
declare global {
interface Window {
ethereum?: {
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
on: (event: string, handler: (...args: unknown[]) => void) => void;
removeListener: (event: string, handler: (...args: unknown[]) => void) => void;
isMetaMask?: boolean;
};
}
}

View File

@ -102,31 +102,6 @@ export const translations = {
trc20_step4: "Contact support with your TX hash if tokens are not received within 24 hours",
trc20_warning: "⚠️ Only send USDT on the TRON network (TRC20). Sending other tokens or using a different network will result in permanent loss.",
// How to Buy Guide
guide_title: "How to Buy XIC",
guide_step1_title: "Step 1: Before Purchase",
guide_step1_1: "Visit the website and connect your wallet",
guide_step1_2: "Add XIC token (BSC network, contract: 0xc65e7A2738eD884dB8d26a6eb2fEcF7daCA2e90C)",
guide_step1_3: "Copy the token receiving address from the website",
guide_step2_title: "Step 2: Purchase",
guide_step2_1: "Paste the receiving address",
guide_step2_2: "Website automatically shows USDT amount",
guide_step2_3: "Enter the XIC quantity you want to buy",
guide_step2_4: "Click Buy — one wallet confirmation only",
guide_step3_title: "Step 3: After Purchase",
guide_step3_1: "Receive purchase receipt",
guide_step3_2: "Confirm payment is completed",
guide_step3_3: "Get transaction hash",
guide_step3_4: "XIC tokens automatically sent to your wallet",
// Add Token
add_token_btn: "Add XIC to Wallet",
add_token_success: "XIC token added to wallet!",
add_token_fail: "Failed to add token. Please add manually.",
// WhatsApp Support
support_whatsapp: "WhatsApp Support",
support_whatsapp_msg: "Didn't receive tokens? Contact us on WhatsApp, resolved within 24 hours",
support_whatsapp_btn: "Contact WhatsApp: +971 56 165 1888",
// Why NAC
why_rwa_title: "Native RWA Chain",
why_rwa_desc: "Purpose-built for Real World Asset tokenization with AI-native compliance",
@ -293,31 +268,6 @@ export const translations = {
trc20_step4: "如24小时内未收到代币请携带交易哈希联系客服",
trc20_warning: "⚠️ 请仅在 TRON 网络TRC20上发送 USDT。发送其他代币或使用其他网络将导致永久损失。",
// How to Buy Guide
guide_title: "XIC 购买指南",
guide_step1_title: "第一步:购买前准备",
guide_step1_1: "访问网站并连接钱包",
guide_step1_2: "添加 XIC 代币BSC 网络合约0xc65e7A2738eD884dB8d26a6eb2fEcF7daCA2e90C",
guide_step1_3: "复制网站上的代币接收地址",
guide_step2_title: "第二步:购买操作",
guide_step2_1: "粘贴接收地址",
guide_step2_2: "网站自动显示 USDT 数量",
guide_step2_3: "在输入框填入购买 XIC 的数量",
guide_step2_4: "点击购买 — 只需钱包确认一次",
guide_step3_title: "第三步:购买后确认",
guide_step3_1: "收到购买回执",
guide_step3_2: "确认已完成支付",
guide_step3_3: "获取交易哈希",
guide_step3_4: "XIC 代币自动发放到您的钱包",
// Add Token
add_token_btn: "添加 XIC 到钱包",
add_token_success: "XIC 代币已添加到钱包!",
add_token_fail: "添加失败,请手动添加代币。",
// WhatsApp Support
support_whatsapp: "WhatsApp 客服",
support_whatsapp_msg: "没有收到代币?联系 WhatsApp 客服24小时内给予解决",
support_whatsapp_btn: "联系 WhatsApp+971 56 165 1888",
// Why NAC
why_rwa_title: "原生 RWA 公链",
why_rwa_desc: "专为现实世界资产代币化而生,内置 AI 合规审批",

View File

@ -1,18 +1,40 @@
import { trpc } from "@/lib/trpc";
import { UNAUTHED_ERR_MSG } from '@shared/const';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { httpBatchLink, TRPCClientError } from "@trpc/client";
import { createRoot } from "react-dom/client";
import superjson from "superjson";
import App from "./App";
import { getLoginUrl } from "./const";
import "./index.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
staleTime: 30_000,
},
},
const queryClient = new QueryClient();
const redirectToLoginIfUnauthorized = (error: unknown) => {
if (!(error instanceof TRPCClientError)) return;
if (typeof window === "undefined") return;
const isUnauthorized = error.message === UNAUTHED_ERR_MSG;
if (!isUnauthorized) return;
window.location.href = getLoginUrl();
};
queryClient.getQueryCache().subscribe(event => {
if (event.type === "updated" && event.action.type === "error") {
const error = event.query.state.error;
redirectToLoginIfUnauthorized(error);
console.error("[API Query Error]", error);
}
});
queryClient.getMutationCache().subscribe(event => {
if (event.type === "updated" && event.action.type === "error") {
const error = event.mutation.state.error;
redirectToLoginIfUnauthorized(error);
console.error("[API Mutation Error]", error);
}
});
const trpcClient = trpc.createClient({

View File

@ -119,7 +119,114 @@ function LoginForm({ onLogin }: { onLogin: (token: string) => void }) {
);
}
// ─── Main D// ─── Settings Panel ───────────────────────────────────────────────
// ─── Receiving Address Panel (安全加固:接收地址仅管理员可修改) ────────────────────────────
function ReceivingAddressPanel({ token }: { token: string }) {
const { data: currentAddresses, refetch } = trpc.settings.getReceivingAddresses.useQuery();
const [values, setValues] = useState<Record<string, string>>({});
const [saving, setSaving] = useState<string | null>(null);
const [saved, setSaved] = useState<Set<string>>(new Set());
useEffect(() => {
if (currentAddresses) {
setValues({
trc20_receiving_address: currentAddresses.trc20_receiving_address || "",
bsc_receiving_address: currentAddresses.bsc_receiving_address || "",
eth_receiving_address: currentAddresses.eth_receiving_address || "",
});
}
}, [currentAddresses]);
const updateMutation = trpc.settings.updateReceivingAddress.useMutation({
onSuccess: (_, vars) => {
setSaving(null);
setSaved(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; });
refetch();
setTimeout(() => setSaved(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2500);
},
onError: (err) => {
setSaving(null);
alert(`保存失败: ${err.message}`);
},
});
const handleSave = (key: "trc20_receiving_address" | "bsc_receiving_address" | "eth_receiving_address") => {
setSaving(key);
updateMutation.mutate({ token, key, value: values[key] || "" });
};
const fields = [
{
key: "trc20_receiving_address" as const,
label: "TRC20 USDT 接收地址",
hint: "TRON 网络上接收 USDT 的钉包地址T 开头34 位)",
placeholder: "TWc2ug...",
color: "#FF0013",
},
{
key: "bsc_receiving_address" as const,
label: "BSC 预售合约地址",
hint: "BSC 网络上的预售合约地址0x 开头42 位)",
placeholder: "0x...",
color: "#F0B90B",
},
{
key: "eth_receiving_address" as const,
label: "ETH 预售合约地址",
hint: "Ethereum 网络上的预售合约地址0x 开头42 位)",
placeholder: "0x...",
color: "#627EEA",
},
];
return (
<div className="rounded-2xl p-5" style={{ background: "rgba(255,82,82,0.04)", border: "2px solid rgba(255,82,82,0.35)" }}>
<div className="flex items-center gap-2 mb-1">
<span style={{ color: "#ff5252", fontSize: "1.1rem" }}></span>
<h3 className="text-sm font-bold" style={{ color: "#ff5252" }}>
</h3>
</div>
<p className="text-xs mb-4" style={{ color: "rgba(255,255,255,0.45)" }}>
</p>
<div className="space-y-4">
{fields.map(({ key, label, hint, placeholder, color }) => (
<div key={key} className="rounded-xl p-4" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.07)" }}>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
<span className="text-sm font-semibold text-white/80">{label}</span>
</div>
<p className="text-xs text-white/40 mb-2">{hint}</p>
<div className="flex gap-2">
<input
type="text"
value={values[key] ?? ""}
onChange={e => setValues(prev => ({ ...prev, [key]: e.target.value }))}
placeholder={placeholder}
className="flex-1 px-3 py-2 rounded-lg text-sm font-mono"
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)", color: "white", outline: "none" }}
/>
<button
onClick={() => handleSave(key)}
disabled={saving === key}
className="px-4 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-all"
style={{
background: saved.has(key) ? "rgba(0,230,118,0.2)" : "rgba(255,82,82,0.15)",
border: saved.has(key) ? "1px solid rgba(0,230,118,0.4)" : "1px solid rgba(255,82,82,0.4)",
color: saved.has(key) ? "#00e676" : "#ff5252",
}}
>
{saving === key ? "保存中..." : saved.has(key) ? "✓ 已保存" : "保存"}
</button>
</div>
</div>
))}
</div>
</div>
);
}
// ─── Settings Panel ───────────────────────────────────────────────
function SettingsPanel({ token }: { token: string }) {
const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token });
const [editValues, setEditValues] = useState<Record<string, string>>({});
@ -361,6 +468,9 @@ function SettingsPanel({ token }: { token: string }) {
})}
</div>
{/* ── Receiving Addresses 接收地址配置 —— 仅管理员可修改 —— */}
<ReceivingAddressPanel token={token} />
{/* Telegram Notifications */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(0,230,118,0.15)" }}>
<h3 className="text-sm font-semibold mb-1" style={{ color: "#00e676" }}>Telegram Notifications</h3>
@ -428,7 +538,11 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void })
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 [activeTab, setActiveTab] = useState<"purchases" | "intents" | "settings">("purchases");
const [activeTab, setActiveTab] = useState<"purchases" | "intents" | "bridge" | "settings">("purchases");
const [bridgePage, setBridgePage] = useState(1);
const [bridgeStatusFilter, setBridgeStatusFilter] = useState<"all" | "pending" | "confirmed" | "distributed" | "failed">("all");
const [bridgeDistribTxInput, setBridgeDistribTxInput] = useState<Record<number, string>>({});
const [updatingBridgeId, setUpdatingBridgeId] = useState<number | null>(null);
const { data: statsData, refetch: refetchStats } = trpc.admin.stats.useQuery({ token });
const { data: intentsData, isLoading: intentsLoading } = trpc.admin.listIntents.useQuery({ token, showAll: false });
@ -439,6 +553,30 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void })
status: statusFilter,
});
const { data: bridgeData, refetch: refetchBridge, isLoading: bridgeLoading } = trpc.admin.listBridgeOrders.useQuery({
token,
page: bridgePage,
limit: 20,
status: bridgeStatusFilter,
});
const updateBridgeOrderMutation = trpc.admin.updateBridgeOrder.useMutation({
onSuccess: () => {
refetchBridge();
setUpdatingBridgeId(null);
},
});
const handleUpdateBridgeOrder = (id: number, status: "pending" | "confirmed" | "distributed" | "failed") => {
setUpdatingBridgeId(id);
updateBridgeOrderMutation.mutate({
token,
orderId: id,
status,
distributeTxHash: bridgeDistribTxInput[id] || undefined,
});
};
const markDistributedMutation = trpc.admin.markDistributed.useMutation({
onSuccess: () => {
refetchPurchases();
@ -597,6 +735,22 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void })
</span>
)}
</button>
<button
onClick={() => setActiveTab("bridge")}
className="px-4 py-2 rounded-xl text-sm font-semibold transition-all"
style={{
background: activeTab === "bridge" ? "rgba(138,71,229,0.15)" : "rgba(255,255,255,0.04)",
border: activeTab === "bridge" ? "1px solid rgba(138,71,229,0.4)" : "1px solid rgba(255,255,255,0.08)",
color: activeTab === "bridge" ? "#a855f7" : "rgba(255,255,255,0.5)",
}}
>
Bridge Orders
{bridgeData && bridgeData.total > 0 && (
<span className="ml-2 px-1.5 py-0.5 rounded-full text-xs" style={{ background: "rgba(138,71,229,0.2)", color: "#a855f7" }}>
{bridgeData.total}
</span>
)}
</button>
<button
onClick={() => setActiveTab("settings")}
className="px-4 py-2 rounded-xl text-sm font-semibold transition-all"
@ -833,6 +987,197 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void })
</div>
)}
{/* ── Bridge Orders Panel ── */}
{activeTab === "bridge" && (
<div className="space-y-4">
{/* Bridge Intents */}
<div className="rounded-2xl overflow-hidden" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(138,71,229,0.2)" }}>
<div className="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" }}>
Bridge Intents
<span className="text-white/40 text-sm ml-2">(users who registered intent but transfer not yet detected)</span>
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
{["ID", "Chain", "Sender", "XIC Receive Addr", "Expected USDT", "Matched", "Time"].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/30">{h}</th>
))}
</tr>
</thead>
<tbody>
{bridgeData?.intents && bridgeData.intents.length > 0 ? bridgeData.intents.map((intent, i) => {
const chainNames: Record<number, string> = { 56: "BSC", 1: "ETH", 137: "POLY", 42161: "ARB", 43114: "AVAX" };
return (
<tr key={intent.id} style={{ borderBottom: i < bridgeData.intents.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none" }}>
<td className="px-4 py-3 text-white/50 text-xs">{intent.id}</td>
<td className="px-4 py-3">
<span className="text-xs px-2 py-0.5 rounded-full" style={{ background: "rgba(138,71,229,0.15)", color: "#a855f7" }}>
{chainNames[intent.fromChainId] ?? `Chain ${intent.fromChainId}`}
</span>
</td>
<td className="px-4 py-3">{formatAddress(intent.senderAddress ?? null)}</td>
<td className="px-4 py-3">{formatAddress(intent.xicReceiveAddress)}</td>
<td className="px-4 py-3 text-white/80">
{intent.expectedUsdt ? `$${intent.expectedUsdt.toLocaleString()}` : <span className="text-white/30"></span>}
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full ${intent.matched ? "text-green-400" : "text-yellow-400"}`}
style={{ background: intent.matched ? "rgba(0,230,118,0.1)" : "rgba(240,180,41,0.1)" }}>
{intent.matched ? "Matched" : "Pending"}
</span>
</td>
<td className="px-4 py-3 text-white/30 text-xs">{formatDate(intent.createdAt)}</td>
</tr>
);
}) : (
<tr><td colSpan={7} className="px-4 py-8 text-center text-white/30 text-sm">No bridge intents yet</td></tr>
)}
</tbody>
</table>
</div>
</div>
{/* Bridge Orders */}
<div className="rounded-2xl overflow-hidden" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.06)" }}>
<div className="px-5 py-4 flex items-center justify-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
<h3 className="font-semibold text-white/80" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
Bridge Orders
<span className="text-white/40 text-sm ml-2">(confirmed USDT deposits awaiting XIC distribution)</span>
</h3>
{/* Status Filter */}
<div className="flex gap-2">
{(["all", "pending", "confirmed", "distributed", "failed"] as const).map(s => (
<button
key={s}
onClick={() => { setBridgeStatusFilter(s); setBridgePage(1); }}
className="px-2 py-1 rounded-lg text-xs font-medium transition-all"
style={{
background: bridgeStatusFilter === s ? "rgba(138,71,229,0.2)" : "rgba(255,255,255,0.04)",
border: bridgeStatusFilter === s ? "1px solid rgba(138,71,229,0.4)" : "1px solid rgba(255,255,255,0.08)",
color: bridgeStatusFilter === s ? "#a855f7" : "rgba(255,255,255,0.4)",
}}
>
{s}
</button>
))}
</div>
</div>
{bridgeLoading ? (
<div className="flex justify-center py-12"><span className="text-white/30">Loading...</span></div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
{["ID", "TX Hash", "Chain", "Sender", "USDT", "XIC", "XIC Receive Addr", "Status", "Distribute TX", "Action"].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/30">{h}</th>
))}
</tr>
</thead>
<tbody>
{bridgeData?.orders && bridgeData.orders.length > 0 ? bridgeData.orders.map((order, i) => {
const chainNames: Record<number, string> = { 56: "BSC", 1: "ETH", 137: "POLY", 42161: "ARB", 43114: "AVAX" };
const isUpdating = updatingBridgeId === order.id;
return (
<tr key={order.id} style={{ borderBottom: i < bridgeData.orders.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none" }}>
<td className="px-4 py-3 text-white/50 text-xs">{order.id}</td>
<td className="px-4 py-3">{formatAddress(order.txHash)}</td>
<td className="px-4 py-3">
<span className="text-xs px-2 py-0.5 rounded-full" style={{ background: "rgba(138,71,229,0.15)", color: "#a855f7" }}>
{chainNames[order.fromChainId] ?? `Chain ${order.fromChainId}`}
</span>
</td>
<td className="px-4 py-3">{formatAddress(order.walletAddress)}</td>
<td className="px-4 py-3 text-amber-400 font-semibold">${order.fromAmount.toLocaleString()}</td>
<td className="px-4 py-3 text-cyan-400 font-semibold">{order.toAmount.toLocaleString(undefined, { maximumFractionDigits: 0 })} XIC</td>
<td className="px-4 py-3">{formatAddress(order.xicReceiveAddress ?? null)}</td>
<td className="px-4 py-3"><StatusBadge status={order.status as any} /></td>
<td className="px-4 py-3">
{order.status !== "distributed" ? (
<input
type="text"
value={bridgeDistribTxInput[order.id] || ""}
onChange={e => setBridgeDistribTxInput(prev => ({ ...prev, [order.id]: e.target.value }))}
placeholder="0x... BSC TX hash"
className="w-32 rounded px-2 py-1 text-xs text-white outline-none"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.12)", fontFamily: "'JetBrains Mono', monospace" }}
/>
) : (
formatAddress(order.distributeTxHash ?? null)
)}
</td>
<td className="px-4 py-3">
{order.status === "confirmed" && (
<button
onClick={() => handleUpdateBridgeOrder(order.id, "distributed")}
disabled={isUpdating}
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all"
style={{
background: isUpdating ? "rgba(0,230,118,0.1)" : "rgba(0,230,118,0.2)",
border: "1px solid rgba(0,230,118,0.4)",
color: "#00e676",
cursor: isUpdating ? "not-allowed" : "pointer",
}}
>
{isUpdating ? "..." : "Mark Distributed"}
</button>
)}
{order.status === "pending" && (
<button
onClick={() => handleUpdateBridgeOrder(order.id, "confirmed")}
disabled={isUpdating}
className="px-3 py-1.5 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",
}}
>
{isUpdating ? "..." : "Confirm"}
</button>
)}
</td>
</tr>
);
}) : (
<tr><td colSpan={10} className="px-4 py-8 text-center text-white/30 text-sm">No bridge orders yet</td></tr>
)}
</tbody>
</table>
</div>
)}
{/* Pagination */}
{bridgeData && bridgeData.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/30">{bridgeData.total} total orders</span>
<div className="flex gap-2">
<button
onClick={() => setBridgePage(p => Math.max(1, p - 1))}
disabled={bridgePage === 1}
className="px-3 py-1.5 rounded-lg text-xs transition-all"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: bridgePage === 1 ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.6)" }}
>
Prev
</button>
<span className="px-3 py-1.5 text-xs text-white/40">Page {bridgePage} / {Math.ceil(bridgeData.total / 20)}</span>
<button
onClick={() => setBridgePage(p => p + 1)}
disabled={bridgePage >= Math.ceil(bridgeData.total / 20)}
className="px-3 py-1.5 rounded-lg text-xs transition-all"
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: bridgePage >= Math.ceil(bridgeData.total / 20) ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.6)" }}
>
Next
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* ── Site Settings Panel ── */}
{activeTab === "settings" && (
<SettingsPanel token={token} />

1408
client/src/pages/Bridge.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
// Colors: Amber Gold #f0b429 | Quantum Blue #00d4ff | Deep Black #0a0a0f
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { createPortal } from "react-dom";
import { toast } from "sonner";
import { Link } from "wouter";
import { useWallet } from "@/hooks/useWallet";
@ -132,8 +133,13 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
},
});
// 从数据库加载接收地址(只读,不可编辑)
const { data: receivingAddresses } = trpc.settings.getReceivingAddresses.useQuery();
const trc20ReceivingAddress = receivingAddresses?.trc20_receiving_address || "";
const copyAddress = () => {
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
if (!trc20ReceivingAddress) return;
navigator.clipboard.writeText(trc20ReceivingAddress);
setCopied(true);
toast.success(lang === "zh" ? "地址已复制到剪贴板!" : "Address copied to clipboard!");
setTimeout(() => setCopied(false), 2000);
@ -159,12 +165,12 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
<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" ? "必填您的XIC接收地址BSC/ETH包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
{lang === "zh" ? "必填您的XIC接收地址BSC/ETH包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
</p>
</div>
<p className="text-xs text-white/50">
{lang === "zh"
? "XIC代币将发放到您的BSC/ETH包地址0x开头。请确保填写正确的地址否则无法收到代币。"
? "XIC代币将发放到您的BSC/ETH包地址0x开头。请确保填写正确的地址否则无法收到代币。"
: "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}
</p>
<div className="space-y-2">
@ -270,18 +276,37 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
</div>
<div className="nac-card-blue rounded-xl p-4 space-y-3">
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
<span className="text-xs px-2 py-0.5 rounded" style={{ background: "rgba(255,255,255,0.08)", color: "rgba(255,255,255,0.4)" }}>
{lang === "zh" ? "只读" : "Read Only"}
</span>
</div>
{/* 接收地址只读显示——不可编辑,只能复制,防止页面夹持欺诈 */}
<div
className="trc20-address p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}
onClick={copyAddress}
className="trc20-address p-3 rounded-lg select-all"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.1)",
color: "rgba(255,255,255,0.6)",
fontFamily: "monospace",
fontSize: "0.85rem",
wordBreak: "break-all",
cursor: "default",
userSelect: "text",
}}
>
{CONTRACTS.TRON.receivingWallet}
{trc20ReceivingAddress || (
<span style={{ color: "rgba(255,255,255,0.3)" }}>
{lang === "zh" ? "加载中...请稍候" : "Loading..."}
</span>
)}
</div>
<button
onClick={copyAddress}
disabled={!trc20ReceivingAddress}
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff" }}
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff", opacity: trc20ReceivingAddress ? 1 : 0.5 }}
>
{copied ? t("trc20_copied") : t("trc20_copy")}
</button>
@ -315,7 +340,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
}
// ─── EVM Purchase Panel ─────────────────────────────────────────────────────
function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn }) {
function EVMPurchasePanel({ network, lang, wallet, onOpenWalletModal }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn; onOpenWalletModal?: () => void }) {
const { t } = useTranslation(lang);
const { purchaseState, buyWithUSDT, reset, calcTokens, getUsdtBalance } = usePresale(wallet, network);
const [usdtInput, setUsdtInput] = useState("100");
@ -357,7 +382,46 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
}
}, [purchaseState.step, purchaseState.error, purchaseState.tokenAmount, lang]);
if (purchaseState.step === "success") {
if (!wallet.isConnected) {
return (
<div className="space-y-4">
<p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
<button
onClick={() => onOpenWalletModal?.()}
className="w-full py-3 rounded-xl text-base font-bold transition-all hover:opacity-90 flex items-center justify-center gap-2"
style={{ background: "linear-gradient(135deg, rgba(240,180,41,0.9) 0%, rgba(255,215,0,0.9) 100%)", color: "#0a0a0f", fontFamily: "'Space Grotesk', sans-serif", boxShadow: "0 0 16px rgba(240,180,41,0.3)" }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
</svg>
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
</button>
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
</div>
);
}
if (isWrongNetwork) {
return (
<div className="space-y-4">
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.3)" }}>
<div className="text-3xl mb-2"></div>
<p className="text-amber-300 font-semibold mb-1">{t("buy_wrong_network")}</p>
<p className="text-white/60 text-sm mb-4">{t("buy_wrong_msg")} {CONTRACTS[network].chainName}</p>
<button
onClick={() => wallet.switchNetwork(targetChainId)}
className="btn-primary-nac px-6 py-2 rounded-lg text-sm font-bold"
>
{t("buy_switch")} {network === "BSC" ? "BSC" : "Ethereum"}
</button>
</div>
</div>
);
}
if (purchaseState.step === "success") {
return (
<div className="space-y-4 text-center py-4">
<div className="text-5xl mb-3">🎉</div>
@ -388,32 +452,16 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
return (
<div className="space-y-4">
{/* Wallet info — only shown when connected */}
{wallet.isConnected && !isWrongNetwork && (
<div className="flex items-center justify-between px-3 py-2 rounded-lg" style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-400" style={{ boxShadow: "0 0 6px #00e676" }} />
<span className="text-xs text-white/60 counter-digit">{shortenAddress(wallet.address || "")}</span>
</div>
{usdtBalance !== null && (
<span className="text-xs text-white/50">{t("buy_balance")} <span className="text-white/80 counter-digit">{usdtBalance.toFixed(2)} USDT</span></span>
)}
{/* Wallet info */}
<div className="flex items-center justify-between px-3 py-2 rounded-lg" style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-400" style={{ boxShadow: "0 0 6px #00e676" }} />
<span className="text-xs text-white/60 counter-digit">{shortenAddress(wallet.address || "")}</span>
</div>
)}
{/* Wrong network banner */}
{isWrongNetwork && (
<div className="rounded-xl p-4 text-center" style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.3)" }}>
<div className="text-3xl mb-2"></div>
<p className="text-amber-300 font-semibold mb-1">{t("buy_wrong_network")}</p>
<p className="text-white/60 text-sm mb-4">{t("buy_wrong_msg")} {CONTRACTS[network].chainName}</p>
<button
onClick={() => wallet.switchNetwork(targetChainId)}
className="btn-primary-nac px-6 py-2 rounded-lg text-sm font-bold"
>
{t("buy_switch")} {network === "BSC" ? "BSC" : "Ethereum"}
</button>
</div>
)}
{usdtBalance !== null && (
<span className="text-xs text-white/50">{t("buy_balance")} <span className="text-white/80 counter-digit">{usdtBalance.toFixed(2)} USDT</span></span>
)}
</div>
{/* USDT Amount Input */}
<div className="space-y-2">
@ -481,44 +529,19 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
</div>
)}
{/* Buy Button — or Connect Wallet if not connected */}
{!wallet.isConnected ? (
<div className="space-y-3">
<p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
<WalletSelector
lang={lang}
connectedAddress={wallet.address ?? undefined}
onAddressDetected={(addr) => {
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}}
compact
/>
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
</div>
) : isWrongNetwork ? (
<button
onClick={() => wallet.switchNetwork(targetChainId)}
className="btn-primary-nac w-full py-4 rounded-xl text-base font-bold"
style={{ fontFamily: "Space Grotesk, sans-serif" }}
>
{lang === "zh"
? `切换到 ${CONTRACTS[network].chainName} 后即可购买`
: `Switch to ${CONTRACTS[network].chainName} to Buy`}
</button>
) : (
<button
onClick={handleBuy}
disabled={isProcessing || !isValidAmount}
className="btn-primary-nac w-full py-4 rounded-xl text-base font-bold"
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
>
{isProcessing
? purchaseState.step === "approving" ? t("buy_approving")
: purchaseState.step === "approved" ? t("buy_approved")
: t("buy_processing")
: `${t("buy_btn")} ${formatNumber(tokenAmount)} XIC`}
</button>
)}
{/* Buy Button */}
<button
onClick={handleBuy}
disabled={isProcessing || !isValidAmount}
className="btn-primary-nac w-full py-4 rounded-xl text-base font-bold"
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
>
{isProcessing
? purchaseState.step === "approving" ? t("buy_approving")
: purchaseState.step === "approved" ? t("buy_approved")
: t("buy_processing")
: `${t("buy_btn")} ${formatNumber(tokenAmount)} XIC`}
</button>
<p className="text-xs text-center text-white/30">
{PRESALE_CONFIG.maxPurchaseUSDT > 0
@ -526,27 +549,24 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
: (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}
</p>
{/* Add XIC to Wallet button — only show on BSC where token address is known */}
{network === "BSC" && CONTRACTS.BSC.token && (
{/* Add XIC to Wallet button — only show on BSC where token address is known AND wallet is connected */}
{network === "BSC" && CONTRACTS.BSC.token && wallet.isConnected && (
<button
onClick={async () => {
try {
// Use the raw provider to call wallet_watchAsset
const rawProvider = (window as unknown as { ethereum?: { request: (args: { method: string; params?: unknown }) => Promise<unknown> } }).ethereum;
if (!rawProvider) {
toast.error(lang === "zh" ? "未检测到钱包,请先安装 MetaMask 或其他 EVM 钱包" : "No wallet detected. Please install MetaMask or another EVM wallet.");
// Use wallet.provider (ethers BrowserProvider) which wraps the connected wallet's provider
// This works regardless of which wallet is connected (MetaMask, OKX, TokenPocket, etc.)
if (!wallet.provider) {
toast.error(lang === "zh" ? "钱包未连接,请先连接钱包" : "Wallet not connected. Please connect your wallet first.");
return;
}
await rawProvider.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: CONTRACTS.BSC.token,
symbol: PRESALE_CONFIG.tokenSymbol,
decimals: PRESALE_CONFIG.tokenDecimals,
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
},
await wallet.provider.send("wallet_watchAsset", {
type: "ERC20",
options: {
address: CONTRACTS.BSC.token,
symbol: PRESALE_CONFIG.tokenSymbol,
decimals: PRESALE_CONFIG.tokenDecimals,
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
},
});
toast.success(lang === "zh" ? "XIC 代币已添加到钱包!" : "XIC token added to wallet!");
@ -810,10 +830,9 @@ function ChatSupport({ lang }: { lang: Lang }) {
// ─── Navbar Wallet Button ─────────────────────────────────────────────────────
type WalletHookReturn = ReturnType<typeof useWallet>;
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) {
function NavWalletButton({ lang, wallet, showWalletModal, setShowWalletModal }: { lang: Lang; wallet: WalletHookReturn; showWalletModal: boolean; setShowWalletModal: (v: boolean) => void }) {
const { t } = useTranslation(lang);
const [showMenu, setShowMenu] = useState(false);
const [showWalletModal, setShowWalletModal] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -824,24 +843,10 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Detect mobile browser
const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Handle connect button click — show wallet selector modal
const handleConnectClick = async () => {
// On mobile browsers, skip direct connect attempt and show modal immediately
// (mobile browsers don't support wallet extensions)
if (isMobile) {
setShowWalletModal(true);
return;
}
// On desktop: first try direct connect (works if wallet is already set up and locked)
const result = await wallet.connect();
if (!result.success && result.error) {
// If direct connect failed, show the wallet selector modal for guided setup
setShowWalletModal(true);
toast.error(result.error, { duration: 6000 });
}
// Handle connect button click — always show wallet selector modal
// Both desktop and mobile users see the modal to choose their wallet
const handleConnectClick = () => {
setShowWalletModal(true);
};
if (!wallet.isConnected) {
@ -861,67 +866,6 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
{wallet.isConnecting ? t("nav_connecting") : t("nav_connect")}
</button>
{/* Wallet Connection Modal */}
{showWalletModal && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
>
<div
className="w-full max-w-md rounded-2xl p-6 relative"
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)" }}
>
{/* Close button */}
<button
onClick={() => setShowWalletModal(false)}
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
</h3>
<p className="text-xs text-white/40 mb-4">
{lang === "zh"
? "选择您的钱包进行连接,或手动输入地址"
: "Select your wallet to connect, or enter address manually"}
</p>
{/* MetaMask initialization guide */}
<div
className="rounded-xl p-3 mb-4"
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
>
<p className="text-xs text-amber-300/80 leading-relaxed">
{lang === "zh"
? "💡 首次使用 MetaMask请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按钮重新检测。"
: "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."}
</p>
</div>
<WalletSelector
lang={lang}
connectedAddress={wallet.address ?? undefined}
onAddressDetected={async (addr) => {
// After address detected from WalletSelector, sync wallet state
const result = await wallet.connect();
if (result.success) {
setShowWalletModal(false);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
} else {
// Even if connect() failed, we have the address — close modal
setShowWalletModal(false);
toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}
}}
/>
</div>
</div>
)}
</>
);
}
@ -1020,6 +964,8 @@ export default function Home() {
// 钱包状态提升到顶层共享给NavWalletButton和EVMPurchasePanel
const wallet = useWallet();
// showWalletModal提升到顶层供NavWalletButton和EVMPurchasePanel共用
const [showWalletModal, setShowWalletModal] = useState(false);
const networks: NetworkTab[] = ["BSC", "ETH", "TRON"];
@ -1058,8 +1004,13 @@ export default function Home() {
{lang === "zh" ? "📖 购买教程" : "📖 Tutorial"}
</span>
</Link>
<Link href="/bridge">
<span className="text-sm font-semibold cursor-pointer transition-colors hidden md:block px-2 py-1 rounded-lg" style={{ color: "#f0b429", background: "rgba(240,180,41,0.1)", border: "1px solid rgba(240,180,41,0.3)" }}>
{lang === "zh" ? "⚡ 跨链桥" : "⚡ Bridge"}
</span>
</Link>
<LangToggle lang={lang} setLang={setLang} />
<NavWalletButton lang={lang} wallet={wallet} />
<NavWalletButton lang={lang} wallet={wallet} showWalletModal={showWalletModal} setShowWalletModal={setShowWalletModal} />
</div>
</nav>
@ -1244,8 +1195,8 @@ export default function Home() {
</p>
</div>
)}
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />}
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />}
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
{activeNetwork === "TRON" && (
<div className="space-y-4">
<div className="space-y-2">
@ -1364,6 +1315,64 @@ export default function Home() {
{/* ── Chat Support Widget ── */}
<ChatSupport lang={lang} />
{/* ── Global Wallet Connection Modal (Portal) ── */}
{showWalletModal && createPortal(
<div
className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center sm:p-4"
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
>
<div
className="w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)", maxHeight: "85vh" }}
>
<button
onClick={() => setShowWalletModal(false)}
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
</h3>
{!/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && (
<>
<p className="text-xs text-white/40 mb-4">
{lang === "zh" ? "选择您的钱包进行连接,或手动输入地址" : "Select your wallet to connect, or enter address manually"}
</p>
<div className="rounded-xl p-3 mb-4" style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}>
<p className="text-xs text-amber-300/80 leading-relaxed">
{lang === "zh"
? "💡 首次使用 MetaMask请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按鈕重新检测。"
: "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."}
</p>
</div>
</>
)}
<WalletSelector
lang={lang}
compact={false}
showTron={false}
connectedAddress={undefined}
onAddressDetected={async (addr, _network, rawProvider) => {
if (rawProvider) {
await wallet.connectWithProvider(rawProvider, addr);
} else {
const result = await wallet.connect();
if (!result.success) {
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
}
}
setShowWalletModal(false);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}}
/>
</div>
</div>
, document.body)}
<style>{`
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-10px); }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,198 +0,0 @@
# XICPresale 合约部署操作手册
**版本:** v2.0
**日期:** 2026-03-09
**合约文件:** `XICPresale.sol`
---
## 一、合约参数总览
| 参数 | 值 | 说明 |
|---|---|---|
| 预售总量(硬顶) | 2,500,000,000 XIC25亿 | 合约内置,可通过 setHardCap 修改 |
| 代币价格 | $0.02 USDT / XIC | tokenPrice = 2e1618位精度 |
| 预售时长 | 180 天(半年) | 从 startPresale() 调用时开始计时 |
| 最小购买 | 无限制 | 任意金额均可 |
| 最大购买 | 无限制 | 任意金额均可 |
| 支持支付方式 | USDTBSC、BNB | USDT 即时到账BNB 通过预言机换算 |
| 未售出回收 | 预售结束后 Owner 可回收 | recoverUnsoldTokens() |
---
## 二、部署前准备
### 2.1 需要准备的地址
| 地址 | 说明 | 当前值 |
|---|---|---|
| XIC Token 合约 | XIC 代币地址(不变) | `0x59FF34dD59680a7125782b1f6df2A86ed46F5A24` |
| BSC USDT 合约 | BSC 链 USDT 地址(不变) | `0x55d398326f99059fF775485246999027B3197955` |
| 收款钱包wallet | 接收 USDT 和 BNB 的地址 | 您的收款钱包地址 |
| 价格预言机oracle | BNB/USD 价格来源 | `0xefdab9b5...`(原合约预言机,或填 address(0) 禁用 BNB 购买) |
### 2.2 部署工具
推荐使用 **Remix IDE**https://remix.ethereum.org**Hardhat**
---
## 三、部署步骤Remix IDE
### Step 1打开 Remix IDE
访问 https://remix.ethereum.org新建文件 `XICPresale.sol`,粘贴合约源码。
### Step 2编译
- Compiler 版本:`0.8.20`
- 勾选 `Optimize`runs = `200`
- 点击 `Compile XICPresale.sol`
### Step 3部署
切换到 **Deploy & Run Transactions** 面板:
- Environment选择 `Injected Provider - MetaMask`(确保 MetaMask 连接 BSC 主网Chain ID: 56
- Contract选择 `XICPresale`
- 填写构造函数参数:
```
_xicToken: 0x59FF34dD59680a7125782b1f6df2A86ed46F5A24
_usdt: 0x55d398326f99059fF775485246999027B3197955
_wallet: [您的收款钱包地址]
_oracle: 0xefdab9b5...(或 0x0000000000000000000000000000000000000000 禁用BNB购买
```
- 点击 `Deploy`MetaMask 弹出确认,支付 Gas 费(约 0.01~0.02 BNB
### Step 4记录合约地址
部署成功后,记录新合约地址(格式:`0x...`),更新前端 `contracts.ts` 中的 `presale` 字段。
---
## 四、部署后操作Owner 钱包执行)
### Step A向新合约转入 25亿 XIC
在 XIC Token 合约(`0x59FF34dD59680a7125782b1f6df2A86ed46F5A24`)的 BscScan 写入页面:
访问https://bscscan.com/address/0x59FF34dD59680a7125782b1f6df2A86ed46F5A24#writeContract
1. 连接 Owner 钱包
2. 找到 `transfer` 函数
3. 填写参数:
- `recipient`:新预售合约地址
- `amount``2500000000000000000000000000`25亿 × 10^18共28位数字
4. 点击 Write确认交易
**验证:** 在 BscScan 上查看新合约地址的 XIC 余额,应显示 2,500,000,000 XIC。
### Step B启动预售
在新预售合约的 BscScan 写入页面:
1. 连接 Owner 钱包
2. 找到 `startPresale()` 函数
3. 点击 Write确认交易
**此操作将:**
- 设置 `presaleStarted = true`
- 记录 `presaleStartTime = 当前时间`
- 设置 `presaleEndTime = 当前时间 + 180天`
**从此刻起,用户即可购买 XIC**
---
## 五、合约函数说明
### 用户函数
| 函数 | 说明 |
|---|---|
| `buyWithUSDT(uint256 usdtAmount)` | 用 USDT 购买usdtAmount 为 6 decimals如 100 USDT = 100000000 |
| `buyWithBNB()` | 用 BNB 购买,发送 BNB 时调用payable |
### 查询函数
| 函数 | 说明 |
|---|---|
| `isPresaleActive()` | 预售是否当前可购买 |
| `timeRemaining()` | 预售剩余秒数 |
| `availableXIC()` | 合约当前可售 XIC 余额 |
| `calculateTokenAmount(usdtAmount)` | 计算 USDT 对应的 XIC 数量 |
| `presaleProgress()` | 预售进度(已售/硬顶/百分比) |
| `totalTokensSold()` | 已售 XIC 总量 |
| `totalRaised()` | 已筹 USDT 总量 |
| `userPurchases(address)` | 查询用户购买的 XIC 总量 |
### Owner 管理函数
| 函数 | 说明 |
|---|---|
| `startPresale()` | 启动预售(只能调用一次) |
| `setPaused(bool)` | 暂停/恢复预售 |
| `recoverUnsoldTokens()` | 预售结束后回收未售出 XIC |
| `setTokenPrice(uint256)` | 修改价格18 decimals |
| `setHardCap(uint256)` | 修改硬顶 |
| `setWallet(address)` | 修改收款钱包 |
| `setPriceOracle(address)` | 修改 BNB 价格预言机 |
| `emergencyWithdraw(address, uint256)` | 紧急提取代币(预售中禁止提取 XIC |
| `withdrawBNB()` | 提取误转入的 BNB |
| `transferOwnership(address)` | 转移合约所有权 |
---
## 六、预售结束后操作
### 6.1 自动结束
预售在以下任一条件满足时自动停止:
- 距 `startPresale()` 已过 180 天
- 25亿 XIC 全部售完
### 6.2 回收未售出 XIC
预售结束后Owner 调用 `recoverUnsoldTokens()`
1. 访问新合约 BscScan 写入页面
2. 连接 Owner 钱包
3. 调用 `recoverUnsoldTokens()`
4. 合约将剩余 XIC 全部转回 Owner 钱包
---
## 七、安全注意事项
1. **部署后立即在 BscScan 上 Verify 合约源码**,增加透明度和信任度
2. **转入 XIC 前三次核对合约地址**,防止转错
3. **amount 精度**25亿 XIC = `2500000000000000000000000000`25后跟26个零共28位
4. **预售进行中禁止提取 XIC**:合约内置保护,`emergencyWithdraw` 在预售活跃期间无法提取 XIC
5. **BNB 购买**:如不需要 BNB 购买功能,部署时 `_oracle``0x0000000000000000000000000000000000000000`
---
## 八、前端更新
部署完成后,更新前端 `client/src/lib/contracts.ts`
```typescript
BSC: {
presale: "0x[新合约地址]", // 替换为新部署的合约地址
// 其他不变
}
```
同时更新 `PRESALE_CONFIG`
```typescript
export const PRESALE_CONFIG = {
tokenPrice: 0.02, // $0.02 per XIC ✅
presaleAllocation: 2_500_000_000, // 25亿 XIC ✅
presaleDuration: 180, // 180天 ✅
minPurchaseUSDT: 0, // 无最小限制 ✅
maxPurchaseUSDT: 0, // 无最大限制0 = 无限制)✅
};
```

View File

@ -1,655 +0,0 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_xicToken",
"type": "address"
},
{
"internalType": "address",
"name": "_usdt",
"type": "address"
},
{
"internalType": "address",
"name": "_wallet",
"type": "address"
},
{
"internalType": "address",
"name": "_oracle",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "EmergencyWithdraw",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newHardCap",
"type": "uint256"
}
],
"name": "HardCapChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "totalSold",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalRaised",
"type": "uint256"
}
],
"name": "PresaleEnded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "PresalePaused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "startTime",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "endTime",
"type": "uint256"
}
],
"name": "PresaleStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newPrice",
"type": "uint256"
}
],
"name": "TokenPriceChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "usdtAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "paymentMethod",
"type": "string"
}
],
"name": "TokensPurchased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "UnsoldTokensRecovered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "newWallet",
"type": "address"
}
],
"name": "WalletChanged",
"type": "event"
},
{
"inputs": [],
"name": "PRESALE_DURATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "availableXIC",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "buyWithBNB",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "usdtAmount",
"type": "uint256"
}
],
"name": "buyWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "usdtAmount",
"type": "uint256"
}
],
"name": "calculateTokenAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "bnbAmount",
"type": "uint256"
}
],
"name": "calculateTokenAmountForBNB",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "emergencyWithdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getBNBPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isPresaleActive",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleEndTime",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presalePaused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleProgress",
"outputs": [
{
"internalType": "uint256",
"name": "sold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "cap",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "progressBps",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStartTime",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStarted",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "priceOracle",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "recoverUnsoldTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_hardCap",
"type": "uint256"
}
],
"name": "setHardCap",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "_paused",
"type": "bool"
}
],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_oracle",
"type": "address"
}
],
"name": "setPriceOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_tokenPrice",
"type": "uint256"
}
],
"name": "setTokenPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_wallet",
"type": "address"
}
],
"name": "setWallet",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "startPresale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "timeRemaining",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalRaised",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokensSold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "usdt",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "userPurchases",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "userSpent",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "wallet",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawBNB",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "xicToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]

View File

@ -1,443 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title XICPresale
* @notice NAC XIC Token Presale Contract v2
* @dev USDT XIC
*
*
* - 2,500,000,000 XIC25亿
* - $0.02 USDT / XIC
* - $50,000,000 USDT5000
* - /
* - 180
* - XIC Owner
* - USDT BNB
*
*
* - XIC Token: 0x59FF34dD59680a7125782b1f6df2A86ed46F5A24
* - BSC USDT: 0x55d398326f99059fF775485246999027B3197955
*/
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function decimals() external view returns (uint8);
}
interface IPriceOracle {
function getBNBPrice() external view returns (uint256); // BNB/USD price, 18 decimals
}
contract XICPresale {
// Constants
uint256 public constant PRESALE_DURATION = 180 days; //
// State Variables
address public owner;
address public wallet; // USDT/BNB
IERC20 public immutable xicToken; // XIC
IERC20 public immutable usdt; // BSC USDT
// $0.02 USDT per XIC18 decimals
uint256 public tokenPrice = 2e16; // 0.02 * 1e18
// 25亿 XIC
uint256 public hardCap = 2_500_000_000 * 1e18;
//
uint256 public presaleStartTime; // Unix
uint256 public presaleEndTime; // = startTime + 180 days
bool public presaleStarted; //
// Owner /
bool public presalePaused = false;
//
uint256 public totalTokensSold; // XIC 18 decimals
uint256 public totalRaised; // USDT 6 decimals
//
mapping(address => uint256) public userPurchases; // XIC
mapping(address => uint256) public userSpent; // USDT
// BNB
address public priceOracle;
// Events
event PresaleStarted(uint256 startTime, uint256 endTime);
event PresalePaused(bool paused);
event PresaleEnded(uint256 totalSold, uint256 totalRaised);
event TokensPurchased(
address indexed buyer,
uint256 usdtAmount,
uint256 tokenAmount,
string paymentMethod
);
event UnsoldTokensRecovered(uint256 amount);
event WalletChanged(address newWallet);
event TokenPriceChanged(uint256 newPrice);
event HardCapChanged(uint256 newHardCap);
event EmergencyWithdraw(address token, uint256 amount);
// Modifiers
modifier onlyOwner() {
require(msg.sender == owner, "Presale: caller is not owner");
_;
}
modifier whenActive() {
require(presaleStarted, "Presale: not started yet");
require(!presalePaused, "Presale: presale is paused");
require(block.timestamp <= presaleEndTime, "Presale: presale has ended");
require(totalTokensSold < hardCap, "Presale: hard cap reached");
_;
}
modifier afterPresale() {
require(
presaleStarted && (block.timestamp > presaleEndTime || totalTokensSold >= hardCap),
"Presale: presale still active"
);
_;
}
// Constructor
/**
* @param _xicToken XIC
* @param _usdt BSC USDT
* @param _wallet USDT BNB
* @param _oracle BNB address(0) BNB
*/
constructor(
address _xicToken,
address _usdt,
address _wallet,
address _oracle
) {
require(_xicToken != address(0), "Invalid XIC token address");
require(_usdt != address(0), "Invalid USDT address");
require(_wallet != address(0), "Invalid wallet address");
owner = msg.sender;
xicToken = IERC20(_xicToken);
usdt = IERC20(_usdt);
wallet = _wallet;
priceOracle = _oracle;
}
// Owner: Start Presale
/**
* @notice Owner 180
* @dev XIC 25亿 XIC
*/
function startPresale() external onlyOwner {
require(!presaleStarted, "Presale: already started");
uint256 xicBalance = xicToken.balanceOf(address(this));
require(xicBalance >= hardCap, "Presale: insufficient XIC in contract");
presaleStarted = true;
presaleStartTime = block.timestamp;
presaleEndTime = block.timestamp + PRESALE_DURATION;
emit PresaleStarted(presaleStartTime, presaleEndTime);
}
/**
* @notice / 使
*/
function setPaused(bool _paused) external onlyOwner {
presalePaused = _paused;
emit PresalePaused(_paused);
}
// Core Purchase Functions
/**
* @notice USDT XIC
* @param usdtAmount USDT 6 decimals 100 USDT = 100_000_000
*
*
* 1. USDT.approve(presaleAddress, usdtAmount)
* 2.
* 3. USDT wallet
* 4. XIC
*/
function buyWithUSDT(uint256 usdtAmount) external whenActive {
require(usdtAmount > 0, "Presale: amount must be > 0");
// USDT (6d) 18d XIC
// XIC = usdtAmount * 1e12 * 1e18 / tokenPrice
uint256 tokenAmount = (usdtAmount * 1e12 * 1e18) / tokenPrice;
require(tokenAmount > 0, "Presale: token amount too small");
require(
totalTokensSold + tokenAmount <= hardCap,
"Presale: exceeds hard cap"
);
// XIC
require(
xicToken.balanceOf(address(this)) >= tokenAmount,
"Presale: insufficient XIC in contract"
);
// USDT
require(
usdt.allowance(msg.sender, address(this)) >= usdtAmount,
"Presale: insufficient USDT allowance"
);
require(
usdt.balanceOf(msg.sender) >= usdtAmount,
"Presale: insufficient USDT balance"
);
// 1. USDT wallet
require(
usdt.transferFrom(msg.sender, wallet, usdtAmount),
"Presale: USDT transfer failed"
);
// 2. XIC
require(
xicToken.transfer(msg.sender, tokenAmount),
"Presale: XIC transfer failed"
);
//
totalTokensSold += tokenAmount;
totalRaised += usdtAmount;
userPurchases[msg.sender] += tokenAmount;
userSpent[msg.sender] += usdtAmount;
emit TokensPurchased(msg.sender, usdtAmount, tokenAmount, "USDT");
}
/**
* @notice BNB XIC
* @dev BNB
*/
function buyWithBNB() external payable whenActive {
require(msg.value > 0, "Presale: BNB amount must be > 0");
require(priceOracle != address(0), "Presale: BNB purchase not supported");
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
require(bnbPriceUSD > 0, "Presale: invalid BNB price");
// BNB 18d XIC
uint256 usdValue18 = (msg.value * bnbPriceUSD) / 1e18;
uint256 tokenAmount = (usdValue18 * 1e18) / tokenPrice;
require(tokenAmount > 0, "Presale: token amount too small");
require(
totalTokensSold + tokenAmount <= hardCap,
"Presale: exceeds hard cap"
);
require(
xicToken.balanceOf(address(this)) >= tokenAmount,
"Presale: insufficient XIC in contract"
);
// 1. BNB wallet
(bool bnbOk, ) = wallet.call{value: msg.value}("");
require(bnbOk, "Presale: BNB transfer to wallet failed");
// 2. XIC
require(
xicToken.transfer(msg.sender, tokenAmount),
"Presale: XIC transfer failed"
);
// USDT6d
uint256 usdtEquiv = usdValue18 / 1e12;
totalTokensSold += tokenAmount;
totalRaised += usdtEquiv;
userPurchases[msg.sender] += tokenAmount;
userSpent[msg.sender] += usdtEquiv;
emit TokensPurchased(msg.sender, usdtEquiv, tokenAmount, "BNB");
}
// Post-Presale: Recover Unsold Tokens
/**
* @notice Owner XIC
* @dev
*/
function recoverUnsoldTokens() external onlyOwner afterPresale {
uint256 remaining = xicToken.balanceOf(address(this));
require(remaining > 0, "Presale: no unsold tokens to recover");
require(
xicToken.transfer(owner, remaining),
"Presale: recovery transfer failed"
);
emit UnsoldTokensRecovered(remaining);
emit PresaleEnded(totalTokensSold, totalRaised);
}
// View Functions
/**
* @notice USDT XIC
* @param usdtAmount USDT 6 decimals
*/
function calculateTokenAmount(uint256 usdtAmount) external view returns (uint256) {
return (usdtAmount * 1e12 * 1e18) / tokenPrice;
}
/**
* @notice BNB XIC
*/
function calculateTokenAmountForBNB(uint256 bnbAmount) external view returns (uint256) {
if (priceOracle == address(0)) return 0;
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
if (bnbPriceUSD == 0) return 0;
uint256 usdValue18 = (bnbAmount * bnbPriceUSD) / 1e18;
return (usdValue18 * 1e18) / tokenPrice;
}
/**
* @notice XIC
*/
function availableXIC() external view returns (uint256) {
return xicToken.balanceOf(address(this));
}
/**
* @notice / basis points 0-10000
*/
function presaleProgress() external view returns (
uint256 sold,
uint256 cap,
uint256 progressBps
) {
sold = totalTokensSold;
cap = hardCap;
progressBps = cap > 0 ? (sold * 10000) / cap : 0;
}
/**
* @notice
*/
function timeRemaining() external view returns (uint256) {
if (!presaleStarted || block.timestamp >= presaleEndTime) return 0;
return presaleEndTime - block.timestamp;
}
/**
* @notice
*/
function isPresaleActive() external view returns (bool) {
return presaleStarted
&& !presalePaused
&& block.timestamp <= presaleEndTime
&& totalTokensSold < hardCap;
}
/**
* @notice BNB USD18 decimals
*/
function getBNBPrice() external view returns (uint256) {
if (priceOracle == address(0)) return 0;
return IPriceOracle(priceOracle).getBNBPrice();
}
// Owner Admin Functions
function setWallet(address _wallet) external onlyOwner {
require(_wallet != address(0), "Invalid wallet address");
wallet = _wallet;
emit WalletChanged(_wallet);
}
function setTokenPrice(uint256 _tokenPrice) external onlyOwner {
require(_tokenPrice > 0, "Invalid token price");
tokenPrice = _tokenPrice;
emit TokenPriceChanged(_tokenPrice);
}
function setHardCap(uint256 _hardCap) external onlyOwner {
require(_hardCap >= totalTokensSold, "Hard cap below sold amount");
hardCap = _hardCap;
emit HardCapChanged(_hardCap);
}
function setPriceOracle(address _oracle) external onlyOwner {
priceOracle = _oracle;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid owner address");
owner = newOwner;
}
/**
* @notice
* @dev XIC
*/
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
// XIC
if (
token == address(xicToken)
&& presaleStarted
&& block.timestamp <= presaleEndTime
&& totalTokensSold < hardCap
) {
revert("Presale: cannot withdraw XIC during active presale");
}
IERC20 tokenContract = IERC20(token);
require(tokenContract.balanceOf(address(this)) >= amount, "Insufficient balance");
require(tokenContract.transfer(owner, amount), "Transfer failed");
emit EmergencyWithdraw(token, amount);
}
/**
* @notice BNB
*/
function withdrawBNB() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No BNB to withdraw");
(bool ok, ) = owner.call{value: balance}("");
require(ok, "BNB withdrawal failed");
}
// Receive
receive() external payable {
if (presaleStarted && !presalePaused && priceOracle != address(0)) {
// BNB
require(msg.value > 0, "No BNB sent");
uint256 bnbPriceUSD = IPriceOracle(priceOracle).getBNBPrice();
require(bnbPriceUSD > 0, "Invalid BNB price");
uint256 usdValue18 = (msg.value * bnbPriceUSD) / 1e18;
uint256 tokenAmount = (usdValue18 * 1e18) / tokenPrice;
require(tokenAmount > 0, "Token amount too small");
require(totalTokensSold + tokenAmount <= hardCap, "Hard cap reached");
require(xicToken.balanceOf(address(this)) >= tokenAmount, "Insufficient XIC");
(bool bnbOk, ) = wallet.call{value: msg.value}("");
require(bnbOk, "BNB transfer failed");
require(xicToken.transfer(msg.sender, tokenAmount), "XIC transfer failed");
uint256 usdtEquiv = usdValue18 / 1e12;
totalTokensSold += tokenAmount;
totalRaised += usdtEquiv;
userPurchases[msg.sender] += tokenAmount;
userSpent[msg.sender] += usdtEquiv;
emit TokensPurchased(msg.sender, usdtEquiv, tokenAmount, "BNB");
}
}
}

View File

@ -1,377 +0,0 @@
// NAC XIC Token Presale — Contract Configuration v2
// New Contract: XICPresale (购买即时发放版本)
// 预售总量: 25亿 XIC | 价格: $0.02/XIC | 时长: 180天 | 无购买上下限
// ============================================================
// CONTRACT ADDRESSES
// ============================================================
export const CONTRACTS = {
// BSC Mainnet (Chain ID: 56)
BSC: {
chainId: 56,
chainName: "BNB Smart Chain",
rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com",
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
// ⚠️ 新合约地址(部署后更新此处)
presale: "PENDING_DEPLOYMENT",
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
usdt: "0x55d398326f99059fF775485246999027B3197955",
},
// Ethereum Mainnet (Chain ID: 1)
ETH: {
chainId: 1,
chainName: "Ethereum",
rpcUrl: "https://eth.llamarpc.com",
explorerUrl: "https://etherscan.io",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
token: "",
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
},
// TRON (TRC20) — Manual transfer
TRON: {
chainId: 0,
chainName: "TRON",
explorerUrl: "https://tronscan.org",
presale: "",
token: "",
usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
receivingWallet: "TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb",
},
} as const;
// ============================================================
// PRESALE PARAMETERS
// ============================================================
export const PRESALE_CONFIG = {
tokenPrice: 0.02, // $0.02 per XIC
tokenSymbol: "XIC",
tokenName: "New AssetChain Token",
tokenDecimals: 18,
minPurchaseUSDT: 0, // 无最小购买限制
maxPurchaseUSDT: 0, // 无最大购买限制0 = 无限制)
totalSupply: 100_000_000_000, // 1000亿 XIC 总量
presaleAllocation: 2_500_000_000, // 25亿 XIC 预售总量
presaleDurationDays: 180, // 预售时长 180天
trc20Memo: "XIC_PRESALE",
};
// ============================================================
// NEW PRESALE CONTRACT ABI (XICPresale v2 — 购买即时发放)
// ============================================================
export const PRESALE_ABI = [
// ── Read Functions ──────────────────────────────────────
{
"inputs": [],
"name": "PRESALE_DURATION",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "availableXIC",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "calculateTokenAmount",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "bnbAmount", "type": "uint256" }],
"name": "calculateTokenAmountForBNB",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getBNBPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isPresaleActive",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleEndTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presalePaused",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleProgress",
"outputs": [
{ "internalType": "uint256", "name": "sold", "type": "uint256" },
{ "internalType": "uint256", "name": "cap", "type": "uint256" },
{ "internalType": "uint256", "name": "progressBps", "type": "uint256" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStartTime",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleStarted",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "timeRemaining",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalRaised",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokensSold",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "userPurchases",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "userSpent",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "wallet",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "xicToken",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
// ── Write Functions ─────────────────────────────────────
{
"inputs": [],
"name": "buyWithBNB",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "buyWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "startPresale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "bool", "name": "_paused", "type": "bool" }],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "recoverUnsoldTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }],
"name": "emergencyWithdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
// ── Events ──────────────────────────────────────────────
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "buyer", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "usdtAmount", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "tokenAmount", "type": "uint256" },
{ "indexed": false, "internalType": "string", "name": "paymentMethod", "type": "string" }
],
"name": "TokensPurchased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "uint256", "name": "startTime", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "endTime", "type": "uint256" }
],
"name": "PresaleStarted",
"type": "event"
}
] as const;
// ============================================================
// ERC20 USDT ABI (minimal)
// ============================================================
export const ERC20_ABI = [
{
"inputs": [
{ "internalType": "address", "name": "spender", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "approve",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "owner", "type": "address" },
{ "internalType": "address", "name": "spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "account", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
"stateMutability": "view",
"type": "function"
}
] as const;
// ============================================================
// NETWORK SWITCH HELPER
// ============================================================
export async function switchToNetwork(chainId: number): Promise<void> {
if (!window.ethereum) throw new Error("No wallet detected");
const hexChainId = "0x" + chainId.toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }],
});
} catch (err: unknown) {
if ((err as { code?: number }).code === 4902) {
const network = Object.values(CONTRACTS).find(n => n.chainId === chainId);
if (!network || !("rpcUrl" in network)) throw new Error("Unknown network");
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: hexChainId,
chainName: network.chainName,
rpcUrls: [(network as { rpcUrl: string }).rpcUrl],
nativeCurrency: (network as { nativeCurrency: { name: string; symbol: string; decimals: number } }).nativeCurrency,
blockExplorerUrls: [network.explorerUrl],
}],
});
} else {
throw err;
}
}
}
// ============================================================
// FORMAT HELPERS
// ============================================================
export function formatNumber(n: number, decimals = 2): string {
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(decimals) + "B";
if (n >= 1_000_000) return (n / 1_000_000).toFixed(decimals) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(decimals) + "K";
return n.toFixed(decimals);
}
export function shortenAddress(addr: string): string {
if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4);
}
declare global {
interface Window {
ethereum?: {
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
on: (event: string, handler: (...args: unknown[]) => void) => void;
removeListener: (event: string, handler: (...args: unknown[]) => void) => void;
isMetaMask?: boolean;
};
}
}

View File

@ -1,232 +0,0 @@
// NAC XIC Token Presale — Contract Configuration
// Design: Dark Cyberpunk / Quantum Finance
// Colors: Amber Gold #f0b429, Quantum Blue #00d4ff, Deep Black #0a0a0f
// ============================================================
// CONTRACT ADDRESSES
// ============================================================
export const CONTRACTS = {
// BSC Mainnet (Chain ID: 56)
BSC: {
chainId: 56,
chainName: "BNB Smart Chain",
rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com",
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c",
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
usdt: "0x55d398326f99059fF775485246999027B3197955",
},
// Ethereum Mainnet (Chain ID: 1)
ETH: {
chainId: 1,
chainName: "Ethereum",
rpcUrl: "https://eth.llamarpc.com",
explorerUrl: "https://etherscan.io",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
token: "", // XIC not yet on ETH
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
},
// TRON (TRC20) — Manual transfer
TRON: {
chainId: 0, // Not EVM
chainName: "TRON",
explorerUrl: "https://tronscan.org",
presale: "", // TRC20 uses manual transfer
token: "",
usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
// Receiving wallet for TRC20 USDT
receivingWallet: "TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb",
},
} as const;
// ============================================================
// PRESALE PARAMETERS
// ============================================================
export const PRESALE_CONFIG = {
tokenPrice: 0.02, // $0.02 per XIC
tokenSymbol: "XIC",
tokenName: "New AssetChain Token",
tokenDecimals: 18,
minPurchaseUSDT: 0, // No minimum purchase limit
maxPurchaseUSDT: 50000, // Maximum $50,000 USDT
totalSupply: 100_000_000_000, // 100 billion XIC
presaleAllocation: 30_000_000_000, // 30 billion for presale
// TRC20 memo format
trc20Memo: "XIC_PRESALE",
};
// ============================================================
// PRESALE CONTRACT ABI (BSC & ETH — same interface)
// ============================================================
export const PRESALE_ABI = [
// Read functions
{
"inputs": [],
"name": "tokenPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokensSold",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalRaised",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "presaleActive",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hardCap",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "user", "type": "address" }],
"name": "userPurchases",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
// Write functions
{
"inputs": [{ "internalType": "uint256", "name": "usdtAmount", "type": "uint256" }],
"name": "buyTokensWithUSDT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "buyTokens",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
// Events
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "buyer", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "usdtAmount", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "tokenAmount", "type": "uint256" }
],
"name": "TokensPurchased",
"type": "event"
}
] as const;
// ============================================================
// ERC20 USDT ABI (minimal — approve + allowance + balanceOf)
// ============================================================
export const ERC20_ABI = [
{
"inputs": [
{ "internalType": "address", "name": "spender", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "approve",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "owner", "type": "address" },
{ "internalType": "address", "name": "spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "account", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
"stateMutability": "view",
"type": "function"
}
] as const;
// ============================================================
// NETWORK SWITCH HELPER
// ============================================================
export async function switchToNetwork(chainId: number): Promise<void> {
if (!window.ethereum) throw new Error("No wallet detected");
const hexChainId = "0x" + chainId.toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }],
});
} catch (err: unknown) {
// Chain not added yet — add it
if ((err as { code?: number }).code === 4902) {
const network = Object.values(CONTRACTS).find(n => n.chainId === chainId);
if (!network || !("rpcUrl" in network)) throw new Error("Unknown network");
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: hexChainId,
chainName: network.chainName,
rpcUrls: [(network as { rpcUrl: string }).rpcUrl],
nativeCurrency: (network as { nativeCurrency: { name: string; symbol: string; decimals: number } }).nativeCurrency,
blockExplorerUrls: [network.explorerUrl],
}],
});
} else {
throw err;
}
}
}
// ============================================================
// FORMAT HELPERS
// ============================================================
export function formatNumber(n: number, decimals = 2): string {
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(decimals) + "B";
if (n >= 1_000_000) return (n / 1_000_000).toFixed(decimals) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(decimals) + "K";
return n.toFixed(decimals);
}
export function shortenAddress(addr: string): string {
if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4);
}
// Declare window.ethereum for TypeScript
declare global {
interface Window {
ethereum?: {
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
on: (event: string, handler: (...args: unknown[]) => void) => void;
removeListener: (event: string, handler: (...args: unknown[]) => void) => void;
isMetaMask?: boolean;
};
}
}

View File

@ -1,328 +0,0 @@
/**
* On-chain data service
* Reads presale stats from BSC and ETH contracts using ethers.js
* Caches results in DB to avoid rate limiting
*
* RPC Strategy: Multi-node failover pool tries each node in order until one succeeds
*/
import { ethers } from "ethers";
import { eq } from "drizzle-orm";
import { getDb } from "./db";
import { presaleStatsCache } from "../drizzle/schema";
// ─── Multi-node RPC Pool ────────────────────────────────────────────────────────
// Multiple public RPC endpoints for each chain — tried in order, first success wins
const RPC_POOLS = {
BSC: [
"https://bsc-dataseed1.binance.org/",
"https://bsc-dataseed2.binance.org/",
"https://bsc-dataseed3.binance.org/",
"https://bsc-dataseed4.binance.org/",
"https://bsc-dataseed1.defibit.io/",
"https://bsc-dataseed2.defibit.io/",
"https://bsc.publicnode.com",
"https://binance.llamarpc.com",
"https://rpc.ankr.com/bsc",
],
ETH: [
"https://eth.llamarpc.com",
"https://ethereum.publicnode.com",
"https://rpc.ankr.com/eth",
"https://1rpc.io/eth",
"https://eth.drpc.org",
"https://cloudflare-eth.com",
"https://rpc.payload.de",
],
};
// ─── Contract Addresses ────────────────────────────────────────────────────────
export const CONTRACTS = {
BSC: {
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c",
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
rpc: RPC_POOLS.BSC[0],
chainId: 56,
chainName: "BNB Smart Chain",
explorerUrl: "https://bscscan.com",
usdt: "0x55d398326f99059fF775485246999027B3197955",
},
ETH: {
presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3",
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
rpc: RPC_POOLS.ETH[0],
chainId: 1,
chainName: "Ethereum",
explorerUrl: "https://etherscan.io",
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
},
TRON: {
receivingWallet: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
evmReceivingWallet: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
usdtContract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
},
};
// Minimal ABI for reading presale stats
const PRESALE_ABI = [
"function totalUSDTRaised() view returns (uint256)",
"function totalTokensSold() view returns (uint256)",
"function weiRaised() view returns (uint256)",
"function tokensSold() view returns (uint256)",
"function usdtRaised() view returns (uint256)",
];
// Token price: $0.02 per XIC
export const TOKEN_PRICE_USDT = 0.02;
export const HARD_CAP_USDT = 5_000_000;
export const TOTAL_SUPPLY = 100_000_000_000;
export const MAX_PURCHASE_USDT = 50_000;
export interface PresaleStats {
chain: string;
usdtRaised: number;
tokensSold: number;
lastUpdated: Date;
fromCache: boolean;
rpcUsed?: string;
}
export interface CombinedStats {
totalUsdtRaised: number;
totalTokensSold: number;
hardCap: number;
progressPct: number;
bsc: PresaleStats | null;
eth: PresaleStats | null;
trc20UsdtRaised: number;
trc20TokensSold: number;
lastUpdated: Date;
}
// Cache TTL: 60 seconds
const CACHE_TTL_MS = 60_000;
// RPC timeout: 8 seconds per node
const RPC_TIMEOUT_MS = 8_000;
/**
* Try each RPC node in the pool until one succeeds.
* Returns { usdtRaised, tokensSold, rpcUsed } or throws if all fail.
*/
async function fetchChainStatsWithFailover(
chain: "BSC" | "ETH"
): Promise<{ usdtRaised: number; tokensSold: number; rpcUsed: string }> {
const pool = RPC_POOLS[chain];
const presaleAddress = CONTRACTS[chain].presale;
const errors: string[] = [];
for (const rpcUrl of pool) {
try {
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
staticNetwork: true,
polling: false,
});
// Set a timeout for the provider
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`RPC timeout: ${rpcUrl}`)), RPC_TIMEOUT_MS)
);
const contract = new ethers.Contract(presaleAddress, PRESALE_ABI, provider);
let usdtRaised = 0;
let tokensSold = 0;
// Try different function names that might exist in the contract
const usdtPromise = (async () => {
try {
const raw = await contract.totalUSDTRaised();
return Number(ethers.formatUnits(raw, 6));
} catch {
try {
const raw = await contract.usdtRaised();
return Number(ethers.formatUnits(raw, 6));
} catch {
try {
const raw = await contract.weiRaised();
return Number(ethers.formatUnits(raw, 6));
} catch {
return 0;
}
}
}
})();
const tokensPromise = (async () => {
try {
const raw = await contract.totalTokensSold();
return Number(ethers.formatUnits(raw, 18));
} catch {
try {
const raw = await contract.tokensSold();
return Number(ethers.formatUnits(raw, 18));
} catch {
return 0;
}
}
})();
const [usdtResult, tokensResult] = await Promise.race([
Promise.all([usdtPromise, tokensPromise]),
timeoutPromise,
]);
usdtRaised = usdtResult;
tokensSold = tokensResult;
console.log(`[OnChain] ${chain} stats fetched via ${rpcUrl}: $${usdtRaised} USDT, ${tokensSold} XIC`);
return { usdtRaised, tokensSold, rpcUsed: rpcUrl };
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
errors.push(`${rpcUrl}: ${errMsg}`);
console.warn(`[OnChain] ${chain} RPC failed (${rpcUrl}): ${errMsg}`);
}
}
throw new Error(`All ${chain} RPC nodes failed:\n${errors.join("\n")}`);
}
export async function getPresaleStats(chain: "BSC" | "ETH"): Promise<PresaleStats> {
const db = await getDb();
// Check cache first
if (db) {
try {
const cached = await db
.select()
.from(presaleStatsCache)
.where(eq(presaleStatsCache.chain, chain))
.limit(1);
if (cached.length > 0) {
const row = cached[0];
const age = Date.now() - new Date(row.lastUpdated).getTime();
if (age < CACHE_TTL_MS) {
return {
chain,
usdtRaised: Number(row.usdtRaised || 0),
tokensSold: Number(row.tokensSold || 0),
lastUpdated: new Date(row.lastUpdated),
fromCache: true,
};
}
}
} catch (e) {
console.warn("[OnChain] Cache read error:", e);
}
}
// Fetch fresh from chain with failover
let usdtRaised = 0;
let tokensSold = 0;
let rpcUsed = "";
try {
const data = await fetchChainStatsWithFailover(chain);
usdtRaised = data.usdtRaised;
tokensSold = data.tokensSold;
rpcUsed = data.rpcUsed;
} catch (e) {
console.error(`[OnChain] All ${chain} RPC nodes exhausted:`, e);
}
// Update cache
if (db) {
try {
const existing = await db
.select()
.from(presaleStatsCache)
.where(eq(presaleStatsCache.chain, chain))
.limit(1);
if (existing.length > 0) {
await db
.update(presaleStatsCache)
.set({
usdtRaised: String(usdtRaised),
tokensSold: String(tokensSold),
lastUpdated: new Date(),
})
.where(eq(presaleStatsCache.chain, chain));
} else {
await db.insert(presaleStatsCache).values({
chain,
usdtRaised: String(usdtRaised),
tokensSold: String(tokensSold),
lastUpdated: new Date(),
});
}
} catch (e) {
console.warn("[OnChain] Cache write error:", e);
}
}
return {
chain,
usdtRaised,
tokensSold,
lastUpdated: new Date(),
fromCache: false,
rpcUsed,
};
}
export async function getCombinedStats(): Promise<CombinedStats> {
const [bsc, eth] = await Promise.allSettled([
getPresaleStats("BSC"),
getPresaleStats("ETH"),
]);
const bscStats = bsc.status === "fulfilled" ? bsc.value : null;
const ethStats = eth.status === "fulfilled" ? eth.value : null;
// Get TRC20 stats from DB
let trc20UsdtRaised = 0;
let trc20TokensSold = 0;
try {
const db = await getDb();
if (db) {
const { trc20Purchases } = await import("../drizzle/schema");
const { sql, inArray } = await import("drizzle-orm");
const result = await db
.select({
totalUsdt: sql<string>`SUM(CAST(${trc20Purchases.usdtAmount} AS DECIMAL(30,6)))`,
totalXic: sql<string>`SUM(CAST(${trc20Purchases.xicAmount} AS DECIMAL(30,6)))`,
})
.from(trc20Purchases)
.where(inArray(trc20Purchases.status, ["confirmed", "distributed"]));
if (result[0]) {
trc20UsdtRaised = Number(result[0].totalUsdt || 0);
trc20TokensSold = Number(result[0].totalXic || 0);
}
}
} catch (e) {
console.warn("[OnChain] TRC20 stats error:", e);
}
const totalUsdtRaised =
(bscStats?.usdtRaised || 0) +
(ethStats?.usdtRaised || 0) +
trc20UsdtRaised;
const totalTokensSold =
(bscStats?.tokensSold || 0) +
(ethStats?.tokensSold || 0) +
trc20TokensSold;
return {
totalUsdtRaised,
totalTokensSold,
hardCap: HARD_CAP_USDT,
progressPct: Math.min((totalUsdtRaised / HARD_CAP_USDT) * 100, 100),
bsc: bscStats,
eth: ethStats,
trc20UsdtRaised,
trc20TokensSold,
lastUpdated: new Date(),
};
}

View File

@ -1,300 +0,0 @@
// NAC XIC Presale — Purchase Logic Hook v2
// 适配新合约 XICPresale购买即时发放版本
// 关键变更:
// - 函数名: buyTokensWithUSDT → buyWithUSDT
// - 函数名: buyTokens (BNB) → buyWithBNB
// - BSC USDT 精度: 18 decimals保持不变BSC USDT 是 18d
// - 新增: 从链上读取实时预售状态(剩余时间、进度等)
// - 新增: BNB 购买支持
import { useState, useCallback, useEffect } from "react";
import { Contract, parseUnits, formatUnits, parseEther } from "ethers";
import { CONTRACTS, PRESALE_ABI, ERC20_ABI, PRESALE_CONFIG, formatNumber } from "@/lib/contracts";
import { WalletState } from "./useWallet";
export type PurchaseStep =
| "idle"
| "approving"
| "approved"
| "purchasing"
| "success"
| "error";
export interface PurchaseState {
step: PurchaseStep;
txHash: string | null;
error: string | null;
tokenAmount: number;
}
export interface PresaleStats {
totalSold: number; // 已售 XIC 数量
totalRaised: number; // 已筹 USDT 金额
hardCap: number; // 硬顶 XIC 数量
progressPercent: number; // 进度百分比 0-100
timeRemaining: number; // 剩余秒数
isActive: boolean; // 是否可购买
presaleStarted: boolean; // 是否已启动
presaleEndTime: number; // 结束时间戳(秒)
availableXIC: number; // 合约可售 XIC 余额
bnbPrice: number; // BNB 当前价格USD
}
export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
const [purchaseState, setPurchaseState] = useState<PurchaseState>({
step: "idle",
txHash: null,
error: null,
tokenAmount: 0,
});
const [presaleStats, setPresaleStats] = useState<PresaleStats>({
totalSold: 0,
totalRaised: 0,
hardCap: PRESALE_CONFIG.presaleAllocation,
progressPercent: 0,
timeRemaining: 0,
isActive: false,
presaleStarted: false,
presaleEndTime: 0,
availableXIC: 0,
bnbPrice: 0,
});
const networkConfig = CONTRACTS[network];
// ── 从链上读取预售状态 ──────────────────────────────────────
const fetchPresaleStats = useCallback(async () => {
if (network !== "BSC") return; // 新合约只在 BSC
try {
const provider = wallet.provider;
if (!provider) return;
const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, provider);
const [
totalSoldRaw,
totalRaisedRaw,
hardCapRaw,
progressResult,
timeRemainingRaw,
isActive,
presaleStarted,
presaleEndTimeRaw,
availableXICRaw,
bnbPriceRaw,
] = await Promise.all([
presaleContract.totalTokensSold(),
presaleContract.totalRaised(),
presaleContract.hardCap(),
presaleContract.presaleProgress(),
presaleContract.timeRemaining(),
presaleContract.isPresaleActive(),
presaleContract.presaleStarted(),
presaleContract.presaleEndTime(),
presaleContract.availableXIC(),
presaleContract.getBNBPrice().catch(() => BigInt(0)),
]);
setPresaleStats({
totalSold: parseFloat(formatUnits(totalSoldRaw, 18)),
totalRaised: parseFloat(formatUnits(totalRaisedRaw, 18)), // BSC USDT 18d
hardCap: parseFloat(formatUnits(hardCapRaw, 18)),
progressPercent: Number(progressResult.progressBps) / 100,
timeRemaining: Number(timeRemainingRaw),
isActive: Boolean(isActive),
presaleStarted: Boolean(presaleStarted),
presaleEndTime: Number(presaleEndTimeRaw),
availableXIC: parseFloat(formatUnits(availableXICRaw, 18)),
bnbPrice: parseFloat(formatUnits(bnbPriceRaw, 18)),
});
} catch (err) {
console.error("[usePresale] fetchPresaleStats error:", err);
}
}, [wallet.provider, network, networkConfig]);
// 定期刷新预售状态(每 30 秒)
useEffect(() => {
fetchPresaleStats();
const interval = setInterval(fetchPresaleStats, 30_000);
return () => clearInterval(interval);
}, [fetchPresaleStats]);
// ── 用 USDT 购买(新合约函数名: buyWithUSDT──────────────────
const buyWithUSDT = useCallback(
async (usdtAmount: number) => {
if (!wallet.signer || !wallet.address) {
setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" }));
return;
}
const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice;
setPurchaseState({ step: "approving", txHash: null, error: null, tokenAmount });
try {
// BSC USDT 是 18 decimals
const usdtDecimals = network === "ETH" ? 6 : 18;
const usdtAmountWei = parseUnits(usdtAmount.toString(), usdtDecimals);
const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.signer);
const presaleAddress = networkConfig.presale;
// Step 1: 检查并授权 USDT
const currentAllowance = await usdtContract.allowance(wallet.address, presaleAddress);
if (currentAllowance < usdtAmountWei) {
const approveTx = await usdtContract.approve(presaleAddress, usdtAmountWei);
await approveTx.wait();
}
setPurchaseState(s => ({ ...s, step: "approved" }));
// Step 2: 调用新合约的 buyWithUSDT不是 buyTokensWithUSDT
const presaleContract = new Contract(presaleAddress, PRESALE_ABI, wallet.signer);
const buyTx = await presaleContract.buyWithUSDT(usdtAmountWei);
setPurchaseState(s => ({ ...s, step: "purchasing", txHash: buyTx.hash }));
const receipt = await buyTx.wait();
// 从事件中读取实际收到的 XIC 数量
let actualTokenAmount = tokenAmount;
if (receipt?.logs) {
for (const log of receipt.logs) {
try {
const parsed = presaleContract.interface.parseLog(log);
if (parsed?.name === "TokensPurchased") {
actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18));
}
} catch { /* ignore */ }
}
}
setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount }));
// 刷新预售状态
await fetchPresaleStats();
} catch (err: unknown) {
const errMsg = (err as { reason?: string; message?: string }).reason
|| (err as Error).message
|| "Transaction failed";
setPurchaseState(s => ({ ...s, step: "error", error: errMsg }));
}
},
[wallet, network, networkConfig, fetchPresaleStats]
);
// ── 用 BNB 购买(新合约函数名: buyWithBNB──────────────────
const buyWithBNB = useCallback(
async (bnbAmount: number) => {
if (!wallet.signer || !wallet.address) {
setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" }));
return;
}
const bnbAmountWei = parseEther(bnbAmount.toString());
const estimatedTokens = presaleStats.bnbPrice > 0
? (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice
: 0;
setPurchaseState({ step: "purchasing", txHash: null, error: null, tokenAmount: estimatedTokens });
try {
const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, wallet.signer);
const buyTx = await presaleContract.buyWithBNB({ value: bnbAmountWei });
setPurchaseState(s => ({ ...s, txHash: buyTx.hash }));
const receipt = await buyTx.wait();
let actualTokenAmount = estimatedTokens;
if (receipt?.logs) {
for (const log of receipt.logs) {
try {
const parsed = presaleContract.interface.parseLog(log);
if (parsed?.name === "TokensPurchased") {
actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18));
}
} catch { /* ignore */ }
}
}
setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount }));
await fetchPresaleStats();
} catch (err: unknown) {
const errMsg = (err as { reason?: string; message?: string }).reason
|| (err as Error).message
|| "Transaction failed";
setPurchaseState(s => ({ ...s, step: "error", error: errMsg }));
}
},
[wallet, networkConfig, presaleStats.bnbPrice, fetchPresaleStats]
);
const reset = useCallback(() => {
setPurchaseState({ step: "idle", txHash: null, error: null, tokenAmount: 0 });
}, []);
// 计算 USDT 对应的 XIC 数量
const calcTokens = (usdtAmount: number): number => {
return usdtAmount / PRESALE_CONFIG.tokenPrice;
};
// 计算 BNB 对应的 XIC 数量
const calcTokensForBNB = (bnbAmount: number): number => {
if (presaleStats.bnbPrice <= 0) return 0;
return (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice;
};
// 获取用户 USDT 余额
const getUsdtBalance = useCallback(async (): Promise<number> => {
if (!wallet.provider || !wallet.address) return 0;
try {
const usdtDecimals = network === "ETH" ? 6 : 18;
const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.provider);
const balance = await usdtContract.balanceOf(wallet.address);
return parseFloat(formatUnits(balance, usdtDecimals));
} catch {
return 0;
}
}, [wallet, network, networkConfig]);
// 获取用户 XIC 余额
const getXICBalance = useCallback(async (): Promise<number> => {
if (!wallet.provider || !wallet.address || network !== "BSC") return 0;
try {
const xicContract = new Contract(CONTRACTS.BSC.token, ERC20_ABI, wallet.provider);
const balance = await xicContract.balanceOf(wallet.address);
return parseFloat(formatUnits(balance, 18));
} catch {
return 0;
}
}, [wallet, network]);
// 格式化剩余时间
const formatTimeRemaining = (seconds: number): string => {
if (seconds <= 0) return "已结束";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (days > 0) return `${days}${hours}小时 ${minutes}`;
if (hours > 0) return `${hours}小时 ${minutes}${secs}`;
return `${minutes}${secs}`;
};
return {
purchaseState,
presaleStats,
buyWithUSDT,
buyWithBNB,
reset,
calcTokens,
calcTokensForBNB,
getUsdtBalance,
getXICBalance,
fetchPresaleStats,
formatTimeRemaining,
// 兼容旧接口
calcTokens: calcTokens,
};
}

View File

@ -1,110 +0,0 @@
// NAC XIC Presale — Purchase Logic Hook
// Handles BSC USDT, ETH USDT purchase flows
import { useState, useCallback } from "react";
import { Contract, parseUnits, formatUnits } from "ethers";
import { CONTRACTS, PRESALE_ABI, ERC20_ABI, PRESALE_CONFIG } from "@/lib/contracts";
import { WalletState } from "./useWallet";
export type PurchaseStep =
| "idle"
| "approving"
| "approved"
| "purchasing"
| "success"
| "error";
// All 6 steps are valid
export interface PurchaseState {
step: PurchaseStep;
txHash: string | null;
error: string | null;
tokenAmount: number;
}
export function usePresale(wallet: WalletState, network: "BSC" | "ETH") {
const [purchaseState, setPurchaseState] = useState<PurchaseState>({
step: "idle",
txHash: null,
error: null,
tokenAmount: 0,
});
const networkConfig = CONTRACTS[network];
const buyWithUSDT = useCallback(
async (usdtAmount: number) => {
if (!wallet.signer || !wallet.address) {
setPurchaseState(s => ({ ...s, step: "error", error: "Please connect your wallet first." }));
return;
}
const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice;
setPurchaseState({ step: "approving", txHash: null, error: null, tokenAmount });
try {
// USDT on BSC has 18 decimals, on ETH has 6 decimals
const usdtDecimals = network === "ETH" ? 6 : 18;
const usdtAmountWei = parseUnits(usdtAmount.toString(), usdtDecimals);
// Step 1: Approve USDT spending
const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.signer);
const presaleAddress = networkConfig.presale;
// Check current allowance
const currentAllowance = await usdtContract.allowance(wallet.address, presaleAddress);
if (currentAllowance < usdtAmountWei) {
const approveTx = await usdtContract.approve(presaleAddress, usdtAmountWei);
await approveTx.wait();
}
setPurchaseState(s => ({ ...s, step: "approved" }));
// Step 2: Buy tokens
const presaleContract = new Contract(presaleAddress, PRESALE_ABI, wallet.signer);
const buyTx = await presaleContract.buyTokensWithUSDT(usdtAmountWei);
setPurchaseState(s => ({ ...s, step: "purchasing", txHash: buyTx.hash }));
await buyTx.wait();
setPurchaseState(s => ({ ...s, step: "success" }));
} catch (err: unknown) {
const errMsg = (err as { reason?: string; message?: string }).reason
|| (err as Error).message
|| "Transaction failed";
setPurchaseState(s => ({ ...s, step: "error", error: errMsg }));
}
},
[wallet, network, networkConfig]
);
const reset = useCallback(() => {
setPurchaseState({ step: "idle", txHash: null, error: null, tokenAmount: 0 });
}, []);
// Calculate token amount from USDT input
const calcTokens = (usdtAmount: number): number => {
return usdtAmount / PRESALE_CONFIG.tokenPrice;
};
// Get user's USDT balance
const getUsdtBalance = useCallback(async (): Promise<number> => {
if (!wallet.provider || !wallet.address) return 0;
try {
const usdtDecimals = network === "ETH" ? 6 : 18;
const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.provider);
const balance = await usdtContract.balanceOf(wallet.address);
return parseFloat(formatUnits(balance, usdtDecimals));
} catch {
return 0;
}
}, [wallet, network, networkConfig]);
return {
purchaseState,
buyWithUSDT,
reset,
calcTokens,
getUsdtBalance,
};
}

View File

@ -1,88 +0,0 @@
# 部署日志 — 钱包连接体验修复
**日期:** 2026-03-10
**服务器:** 43.224.155.27AI服务器
**项目:** nac-presale-testXIC代币预售网站
**部署人:** NAC Admin
**Git Commit** 706eead
---
## 工单内容
### 需求1三步操作指引购买前/购买时/购买后)
- 在购买区域上方添加三步操作指引
- 改为文字段落格式(非卡片,避免小屏幕叠加问题)
### 需求2添加XIC代币到钱包按钮
- 一键调用 wallet_watchAsset API
- 在 Token Details 卡片和购买成功页面均有显示
### 需求3WhatsApp客服联系方式
- 在购买成功收据页面添加 WhatsApp 客服链接
### 需求4钱包连接体验修复用户反馈
- 连接钱包后应自动识别当前链并切换对应网络标签
- 连接成功后自动触发 watchAsset 让钱包弹出,让用户感知连接成功
- 支持前10大EVM钱包MetaMask/OKX/TP/Trust/Coinbase/Bitget/Rabby/SafePal/imToken/Phantom
- 修复多钱包环境下 provider 冲突问题用户选哪个钱包就用哪个钱包的provider
---
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| client/src/hooks/useWallet.ts | forceConnect接受specificProvider参数暴露watchAsset()方法rawProviderRef跟踪用户选择的钱包provider |
| client/src/components/WalletSelector.tsx | 支持10大钱包connect()返回{address, provider}onAddressDetected传递provider |
| client/src/pages/Home.tsx | forceConnect传入provider连接后自动切换BSC/ETH网络标签handleAddToken改用wallet.watchAsset()NavWalletButton增加onNetworkDetected回调三步指引改为文字段落格式 |
| client/src/lib/i18n.ts | 添加三步指引、添加代币、WhatsApp客服翻译键中/英) |
---
## 支持的钱包列表10大EVM钱包
1. MetaMaskwindow.ethereum.isMetaMask
2. OKX钱包window.okxwallet
3. TP钱包window.trustwallet / window.tpwallet
4. Trust Walletwindow.trustwallet.isTrust
5. Coinbase Walletwindow.coinbaseWalletExtension
6. Bitget Walletwindow.bitkeep.ethereum
7. Rabby Walletwindow.ethereum.isRabby
8. SafePalwindow.safepal
9. imTokenwindow.imToken
10. Phantom EVMwindow.phantom.ethereum
---
## 构建结果
- 构建状态:✅ 成功vite build + esbuild
- 构建时间7.14s
- 输出大小824.20 kBgzip: 262.24 kB
- PM2进程nac-presale-testid:8已重启状态 online
---
## 测试验证
- 网站访问https://pre-sale.newassetchain.io ✅
- 三步指引文字段落显示:✅
- 购买区域正常显示:✅
- 多语言(中/英)切换:✅
---
## 后台管理员账号
- 管理员用户名nacadmin
- 管理员密码NACadmin2026!
- Gitea地址http://103.96.148.7:3333
- 宝塔面板http://43.224.155.27:12/btwest账号cproot / 密码vajngkvf
---
## 备注
- Git push 到备份服务器103.96.148.7超时本地commit已保存706eead
- 备份文件已保存:*.bak.20260309_211827

View File

@ -1,51 +0,0 @@
# 部署日志 — 钱包连接修复
## 日期
2026-03-09
## 部署目标
修复手机端钱包连接失败问题
## 问题根因
用户在手机 Chrome 浏览器中访问预售页面,手机浏览器不支持 MetaMask 扩展插件,
导致"未检测到 EVM 钱包",且旧版本没有提供 DeepLink 引导。
## 修复内容
### 1. WalletSelector.tsx (v2 → v3)
- 新增移动端浏览器检测 (isMobileBrowser)
- 新增 MobileDeepLinkPanel 组件
- 支持 MetaMask App DeepLink: https://metamask.app.link/dapp/
- 支持 Trust Wallet DeepLink: https://link.trustwallet.com/open_url
- 支持 OKX Wallet DeepLink: okx://wallet/dapp/url
- 支持 TokenPocket DeepLink: tpoutside://pull
- 在钱包 App 内置浏览器中检测到 window.ethereum 时正常显示连接按钮
### 2. useWallet.ts (v2 → v3)
- connect() 函数现在返回 { success: boolean; error?: string }
- 增加 MetaMask 未初始化检测(已安装但未创建/导入钱包)
- 增加中文友好错误提示
### 3. Home.tsx
- NavWalletButton 在手机端直接显示模态框(跳过直接连接尝试)
- 连接失败时通过 toast.error() 显示错误信息
## 构建结果
- 构建成功0 TypeScript 错误
- dist/public/assets/index-*.js 包含 DeepLink 代码
## 服务器状态
- PM2 进程 nac-presale-test (id:8) 已重启,状态 online
- HTTP 200 响应正常
## 测试说明
- 手机端:点击"连接钱包"按钮 → 显示"手机端连接钱包"面板 → 点击钱包 App 按钮 → 在 App 内置浏览器中打开预售页面 → 正常连接钱包
- 桌面端MetaMask 已初始化 → 直接连接;未初始化 → 显示引导提示
## 后台管理员账号
- 宝塔面板: http://43.224.155.27:12/btwest
- 账号: cproot
- 密码: vajngkvf
## 操作人
Manus AI Agent

View File

@ -0,0 +1,95 @@
# v13 部署日志
**部署时间:** 2026-03-10
**部署人员:** Manus AI
**部署服务器:** AI服务器 43.224.155.27:22000
**部署目录:** /www/wwwroot/nac-presale-test/
**PM2进程** nac-presale-test (id=8)
**服务端口:** 3100
**域名:** https://pre-sale.newassetchain.io
---
## 本次更新内容
### 1. 自研跨链桥完全移除Li.Fi第三方协议
**背景:** 之前Bridge页面使用Li.Fi协议由于XIC代币尚未在任何DEX上市Li.Fi无法获取报价Bridge功能完全无法使用。
**解决方案:** 完全移除Li.Fi改为NAC自研跨链桥
- 用户选择源链BSC/ETH/Polygon/Arbitrum/Avalanche
- 系统显示对应链的官方USDT收款地址
- 用户发送USDT后点击"I Have Sent USDT"注册意向
- 后端监听器自动监控各链的USDT转入
- 确认收款后按$0.02/XIC预售价分发XIC代币
### 2. 官方收款地址更新
| 网络 | 地址 |
|------|------|
| TRC20/USDT (TRON) | TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp |
| ERC20/USDT (Ethereum) | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 |
| BEP20/USDT (BSC) | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 |
| Polygon USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 |
| Arbitrum USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 |
| Avalanche USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 |
### 3. 新增文件
- `server/bridgeMonitor.ts` - 多链USDT转入监听器BSC/ETH/Polygon/Arbitrum/Avalanche
- `server/_core/index.ts` - 添加Bridge监听器启动入口
### 4. 数据库变更
新增表:
- `bridge_intents` - 用户购买意向记录(注册后等待确认)
- `bridge_orders` - 已确认的跨链购买订单
修改表:
- `bridge_orders.status` - 添加 `distributed` 枚举值
---
## 测试结果
| 功能 | 状态 | 备注 |
|------|------|------|
| Bridge页面加载 | ✅ 正常 | |
| 5条链选择BSC/ETH/POLY/ARB/AVAX | ✅ 正常 | |
| Gas费说明随链切换 | ✅ 正常 | |
| 收款地址显示 | ✅ 正常 | 官方地址 |
| USDT金额输入 | ✅ 正常 | |
| XIC计算$0.02/XIC | ✅ 正常 | 100 USDT = 5,000 XIC |
| 复制收款地址 | ✅ 正常 | |
| "I Have Sent USDT"提交 | ✅ 正常 | 注册意向到数据库 |
| 成功提示显示 | ✅ 正常 | 绿色成功框 |
| My Transactions查询 | ✅ 正常 | 显示待处理订单 |
| 主页TRC20收款地址 | ✅ 正常 | TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp |
| 主页链上数据 | ✅ 正常 | $9,900 Raised, 495K Tokens Sold |
---
## 后台管理员信息
- **后台管理员账号:** 通过Manus OAuth登录
- **数据库用户:** nac_presale / NACpresale2026!
- **数据库名:** nac_presale
- **MySQL连接** 127.0.0.1:3306仅本地
- **宝塔面板:** http://43.224.155.27:12/btwestcproot/vajngkvf
- **PM2进程名** nac-presale-testid=8
---
## 待优化事项
1. Polygon/Arbitrum/Avalanche 链的 USDT 合约地址需要配置目前使用通用ERC20地址
2. 钱包连接功能MetaMask需要在有MetaMask扩展的浏览器中验证
3. Bridge监听器的RPC节点可考虑使用付费节点提高稳定性
---
## Git同步
- **Gitea仓库** https://git.newassetchain.io/nacadmin/xic-presale
- **分支:** main
- **提交信息:** v13: 自研跨链桥移除Li.Fi更新官方收款地址

136
docs/deployment-log-v16.md Normal file
View File

@ -0,0 +1,136 @@
# NAC XIC Token Presale — v16 部署日志
**部署日期:** 2026-03-10
**版本:** v16
**部署人员:** NAC Admin
**部署服务器:** AI服务器 43.224.155.27
**生产地址:** https://pre-sale.newassetchain.io
**备用地址:** https://ico.newassetchain.io
---
## 后台管理员账户
| 项目 | 值 |
|---|---|
| 管理员入口 | https://pre-sale.newassetchain.io/admin |
| 管理员密码 | NACadmin2026! |
| Gitea 账号 | nacadmin / NACadmin2026! |
| Gitea 仓库 | https://git.newassetchain.io/nacadmin/nac-presale |
---
## v16 核心更新内容
### 1. 数据库架构升级
- 新增 `transaction_logs` 表(防重放,所有链上交易唯一记录)
- 新增 `listener_state` 表(记录各链最后扫描区块高度)
- 数据库:`mysql://nac_presale:NACpresale2026!@127.0.0.1:3306/nac_presale`
### 2. 统一发币服务tokenDistributionService
- 创建 `server/tokenDistributionService.ts`
- 所有支付渠道ERC20/TRC20/未来法币)共用 `creditXic()` 方法
- 幂等性保障:通过 `transaction_logs` 防止重复发币
- `bridgeMonitor.ts``trc20Monitor.ts` 均已集成
### 3. TRON 链完整支持
- Bridge 页面新增 TRX 链选项chainId: 728126428
- 创建 `useTronBridge.ts` hookTronLink 连接 + TRC20 USDT 转账)
- TronLink 未安装时自动跳转官网引导安装
- TRON 收款地址:`TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp`
### 4. wallet_watchAsset 自动添加代币
- 创建 `client/src/lib/addTokenToWallet.ts`
- EVM 链:`window.ethereum.request({ method: 'wallet_watchAsset' })`
- TRON 链:`tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20' } })`
- symbol 限制 ≤ 11 字符XIC 符合规范)
- 购买成功后自动弹出添加代币提示
### 5. 订单状态轮询
- 用户注册意图后,前端每 5 秒自动刷新订单列表
- 订单状态pending → confirmed → distributed → failed
- "My Transactions" 区域实时显示最新状态
### 6. 去除 manus.im 内联
- 构建时覆盖 `VITE_OAUTH_PORTAL_URL=` 环境变量
- 前端 bundle 中无 `manus.im` 引用(已验证)
- 中国用户可正常访问
---
## 服务器配置
### PM2 进程
```
id: 8
name: nac-presale-test
script: /www/wwwroot/nac-presale-test/dist/index.js
port: 3100
status: online
```
### Nginx 配置
- 配置文件:`/etc/nginx/conf.d/nac-presale.conf`
- 反向代理:`proxy_pass http://127.0.0.1:3100`
- SSL`_.newassetchain.io` 通配符证书
### 后端监听服务(启动日志)
```
[BridgeMonitor] Starting multi-chain USDT deposit monitor...
[TRC20Monitor] Starting monitor for TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp
Server running on http://localhost:3100/
```
---
## 测试验证结果
| 测试项目 | 结果 |
|---|---|
| 主页加载 | ✅ 正常 |
| 倒计时 | ✅ 正常112天 |
| 链上数据读取 | ✅ 正常($9,900 已募资) |
| 中英文切换 | ✅ 正常 |
| BSC 链选择 | ✅ 正常 |
| ETH 链选择 | ✅ 正常 |
| TRX 链选择 | ✅ 正常TRON 地址显示) |
| Connect Wallet 模态框 | ✅ 正常createPortal |
| Connect TronLink | ✅ 正常(未安装时跳转官网) |
| Bridge 页面 | ✅ 正常 |
| 管理员后台 | ✅ 正常(/admin |
| Bridge Orders 管理 | ✅ 正常 |
| HTTPS 访问 | ✅ HTTP 200 |
| manus.im 内联 | ✅ 已清除 |
| Vitest 测试 | ✅ 18/18 通过 |
---
## Git 同步记录
- **Gitea 仓库:** https://git.newassetchain.io/nacadmin/nac-presale
- **提交 Hash** f6bed914
- **提交信息:** v16: TRON链支持+tokenDistributionService+wallet_watchAsset+订单轮询+transaction_logs防重放
- **同步时间:** 2026-03-10 08:15 UTC+4
---
## 备份记录
- **旧版 dist 备份:** `/www/wwwroot/nac-presale-test/dist_backup_20260310_081106`
- **Manus 检查点:** f6bed914v16完整重构
---
## 下一阶段计划v17
根据文档五(混合支付架构方案),下一阶段将实现:
1. **支付宝 PC/H5 支付集成**(需要支付宝商户账号)
2. **微信支付集成**(需要微信商户账号)
3. **PayPal Orders v2 API 集成**(需要 PayPal 商户账号)
4. 所有法币支付渠道统一调用 `tokenDistributionService.creditXic()`
---
*日志生成时间2026-03-10*
*NAC XIC Token Presale Platform v16*

View File

@ -0,0 +1,15 @@
CREATE TABLE `bridge_orders` (
`id` int AUTO_INCREMENT NOT NULL,
`txHash` varchar(128) NOT NULL,
`walletAddress` varchar(64) NOT NULL,
`fromChainId` int NOT NULL,
`fromToken` varchar(32) NOT NULL,
`fromAmount` decimal(30,6) NOT NULL,
`toChainId` int NOT NULL,
`toToken` varchar(32) NOT NULL,
`toAmount` decimal(30,6) NOT NULL,
`status` enum('pending','completed','failed') NOT NULL DEFAULT 'completed',
`createdAt` timestamp NOT NULL DEFAULT (now()),
CONSTRAINT `bridge_orders_id` PRIMARY KEY(`id`),
CONSTRAINT `bridge_orders_txHash_unique` UNIQUE(`txHash`)
);

View File

@ -0,0 +1,21 @@
CREATE TABLE `bridge_intents` (
`id` int AUTO_INCREMENT NOT NULL,
`fromChainId` int NOT NULL,
`senderAddress` varchar(64) NOT NULL,
`xicReceiveAddress` varchar(64) NOT NULL,
`expectedUsdt` decimal(20,6),
`matched` boolean NOT NULL DEFAULT false,
`matchedOrderId` int,
`createdAt` timestamp NOT NULL DEFAULT (now()),
CONSTRAINT `bridge_intents_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `bridge_orders` MODIFY COLUMN `toChainId` int NOT NULL DEFAULT 56;--> statement-breakpoint
ALTER TABLE `bridge_orders` MODIFY COLUMN `toToken` varchar(32) NOT NULL DEFAULT 'XIC';--> statement-breakpoint
ALTER TABLE `bridge_orders` MODIFY COLUMN `status` enum('pending','confirmed','distributed','failed') NOT NULL DEFAULT 'pending';--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `xicReceiveAddress` varchar(64);--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `confirmedAt` timestamp;--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `distributedAt` timestamp;--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `distributeTxHash` varchar(128);--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `blockNumber` bigint;--> statement-breakpoint
ALTER TABLE `bridge_orders` ADD `updatedAt` timestamp DEFAULT (now()) NOT NULL ON UPDATE CURRENT_TIMESTAMP;

View File

@ -0,0 +1 @@
ALTER TABLE `bridge_intents` MODIFY COLUMN `senderAddress` varchar(64);

View File

@ -0,0 +1,21 @@
CREATE TABLE `listener_state` (
`id` varchar(32) NOT NULL,
`lastBlock` bigint NOT NULL DEFAULT 0,
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `listener_state_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE TABLE `transaction_logs` (
`id` int AUTO_INCREMENT NOT NULL,
`txHash` varchar(128) NOT NULL,
`chainType` varchar(16) NOT NULL,
`fromAddress` varchar(64) NOT NULL,
`toAddress` varchar(64) NOT NULL,
`amount` decimal(30,6) NOT NULL,
`blockNumber` bigint,
`status` int NOT NULL DEFAULT 0,
`orderNo` varchar(128),
`createdAt` timestamp NOT NULL DEFAULT (now()),
CONSTRAINT `transaction_logs_id` PRIMARY KEY(`id`),
CONSTRAINT `transaction_logs_txHash_unique` UNIQUE(`txHash`)
);

View File

@ -0,0 +1,24 @@
CREATE TABLE `fiat_orders` (
`id` int AUTO_INCREMENT NOT NULL,
`orderId` varchar(64) NOT NULL,
`gatewayOrderId` varchar(128),
`channel` enum('alipay','wechat','paypal') NOT NULL,
`userId` varchar(64),
`payerEmail` varchar(128),
`payerOpenId` varchar(128),
`xicReceiveAddress` varchar(64),
`usdtEquivalent` decimal(20,6) NOT NULL,
`currency` varchar(8) NOT NULL DEFAULT 'USD',
`originalAmount` decimal(20,4) NOT NULL,
`xicAmount` decimal(30,6) NOT NULL,
`status` enum('pending','paid','distributed','refunded','failed','expired') NOT NULL DEFAULT 'pending',
`qrCodeUrl` text,
`paymentUrl` text,
`callbackPayload` text,
`distributedAt` timestamp,
`expiredAt` timestamp,
`createdAt` timestamp NOT NULL DEFAULT (now()),
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `fiat_orders_id` PRIMARY KEY(`id`),
CONSTRAINT `fiat_orders_orderId_unique` UNIQUE(`orderId`)
);

View File

@ -0,0 +1,10 @@
CREATE TABLE `site_settings` (
`id` int AUTO_INCREMENT NOT NULL,
`key` varchar(64) NOT NULL,
`value` text NOT NULL,
`label` varchar(128),
`description` varchar(256),
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `site_settings_id` PRIMARY KEY(`id`),
CONSTRAINT `site_settings_key_unique` UNIQUE(`key`)
);

View File

@ -0,0 +1,532 @@
{
"version": "5",
"dialect": "mysql",
"id": "f2da11d5-2ee3-40ce-9180-11a9480a5b91",
"prevId": "6b25cb51-fd4a-43ff-9411-e1efd553f304",
"tables": {
"bridge_orders": {
"name": "bridge_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"walletAddress": {
"name": "walletAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromToken": {
"name": "fromToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAmount": {
"name": "fromAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toChainId": {
"name": "toChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toToken": {
"name": "toToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toAmount": {
"name": "toAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','completed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'completed'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_orders_id": {
"name": "bridge_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"bridge_orders_txHash_unique": {
"name": "bridge_orders_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"presale_config": {
"name": "presale_config",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"key": {
"name": "key",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"label": {
"name": "label",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'text'"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_config_id": {
"name": "presale_config_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"presale_config_key_unique": {
"name": "presale_config_key_unique",
"columns": [
"key"
]
}
},
"checkConstraint": {}
},
"presale_stats_cache": {
"name": "presale_stats_cache",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"chain": {
"name": "chain",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtRaised": {
"name": "usdtRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"tokensSold": {
"name": "tokensSold",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"weiRaised": {
"name": "weiRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"lastUpdated": {
"name": "lastUpdated",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_stats_cache_id": {
"name": "presale_stats_cache_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_intents": {
"name": "trc20_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"tronAddress": {
"name": "tronAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedPurchaseId": {
"name": "matchedPurchaseId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_intents_id": {
"name": "trc20_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_purchases": {
"name": "trc20_purchases",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtAmount": {
"name": "usdtAmount",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_purchases_id": {
"name": "trc20_purchases_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"trc20_purchases_txHash_unique": {
"name": "trc20_purchases_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@ -0,0 +1,653 @@
{
"version": "5",
"dialect": "mysql",
"id": "b1822efc-d652-4957-970f-f71b733359b6",
"prevId": "f2da11d5-2ee3-40ce-9180-11a9480a5b91",
"tables": {
"bridge_intents": {
"name": "bridge_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"senderAddress": {
"name": "senderAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedOrderId": {
"name": "matchedOrderId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_intents_id": {
"name": "bridge_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"bridge_orders": {
"name": "bridge_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"walletAddress": {
"name": "walletAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromToken": {
"name": "fromToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAmount": {
"name": "fromAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toChainId": {
"name": "toChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 56
},
"toToken": {
"name": "toToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'XIC'"
},
"toAmount": {
"name": "toAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"confirmedAt": {
"name": "confirmedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_orders_id": {
"name": "bridge_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"bridge_orders_txHash_unique": {
"name": "bridge_orders_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"presale_config": {
"name": "presale_config",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"key": {
"name": "key",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"label": {
"name": "label",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'text'"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_config_id": {
"name": "presale_config_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"presale_config_key_unique": {
"name": "presale_config_key_unique",
"columns": [
"key"
]
}
},
"checkConstraint": {}
},
"presale_stats_cache": {
"name": "presale_stats_cache",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"chain": {
"name": "chain",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtRaised": {
"name": "usdtRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"tokensSold": {
"name": "tokensSold",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"weiRaised": {
"name": "weiRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"lastUpdated": {
"name": "lastUpdated",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_stats_cache_id": {
"name": "presale_stats_cache_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_intents": {
"name": "trc20_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"tronAddress": {
"name": "tronAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedPurchaseId": {
"name": "matchedPurchaseId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_intents_id": {
"name": "trc20_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_purchases": {
"name": "trc20_purchases",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtAmount": {
"name": "usdtAmount",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_purchases_id": {
"name": "trc20_purchases_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"trc20_purchases_txHash_unique": {
"name": "trc20_purchases_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@ -0,0 +1,653 @@
{
"version": "5",
"dialect": "mysql",
"id": "7e9d948e-d569-4194-bb75-6219b837045e",
"prevId": "b1822efc-d652-4957-970f-f71b733359b6",
"tables": {
"bridge_intents": {
"name": "bridge_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"senderAddress": {
"name": "senderAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedOrderId": {
"name": "matchedOrderId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_intents_id": {
"name": "bridge_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"bridge_orders": {
"name": "bridge_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"walletAddress": {
"name": "walletAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromToken": {
"name": "fromToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAmount": {
"name": "fromAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toChainId": {
"name": "toChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 56
},
"toToken": {
"name": "toToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'XIC'"
},
"toAmount": {
"name": "toAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"confirmedAt": {
"name": "confirmedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_orders_id": {
"name": "bridge_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"bridge_orders_txHash_unique": {
"name": "bridge_orders_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"presale_config": {
"name": "presale_config",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"key": {
"name": "key",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"label": {
"name": "label",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'text'"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_config_id": {
"name": "presale_config_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"presale_config_key_unique": {
"name": "presale_config_key_unique",
"columns": [
"key"
]
}
},
"checkConstraint": {}
},
"presale_stats_cache": {
"name": "presale_stats_cache",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"chain": {
"name": "chain",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtRaised": {
"name": "usdtRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"tokensSold": {
"name": "tokensSold",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"weiRaised": {
"name": "weiRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"lastUpdated": {
"name": "lastUpdated",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_stats_cache_id": {
"name": "presale_stats_cache_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_intents": {
"name": "trc20_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"tronAddress": {
"name": "tronAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedPurchaseId": {
"name": "matchedPurchaseId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_intents_id": {
"name": "trc20_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_purchases": {
"name": "trc20_purchases",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtAmount": {
"name": "usdtAmount",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_purchases_id": {
"name": "trc20_purchases_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"trc20_purchases_txHash_unique": {
"name": "trc20_purchases_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@ -0,0 +1,790 @@
{
"version": "5",
"dialect": "mysql",
"id": "a2f8d4a4-e049-4e02-8011-f14d50b32f7e",
"prevId": "7e9d948e-d569-4194-bb75-6219b837045e",
"tables": {
"bridge_intents": {
"name": "bridge_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"senderAddress": {
"name": "senderAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedOrderId": {
"name": "matchedOrderId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_intents_id": {
"name": "bridge_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"bridge_orders": {
"name": "bridge_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"walletAddress": {
"name": "walletAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromToken": {
"name": "fromToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAmount": {
"name": "fromAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toChainId": {
"name": "toChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 56
},
"toToken": {
"name": "toToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'XIC'"
},
"toAmount": {
"name": "toAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"confirmedAt": {
"name": "confirmedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_orders_id": {
"name": "bridge_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"bridge_orders_txHash_unique": {
"name": "bridge_orders_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"listener_state": {
"name": "listener_state",
"columns": {
"id": {
"name": "id",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastBlock": {
"name": "lastBlock",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"listener_state_id": {
"name": "listener_state_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"presale_config": {
"name": "presale_config",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"key": {
"name": "key",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"label": {
"name": "label",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'text'"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_config_id": {
"name": "presale_config_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"presale_config_key_unique": {
"name": "presale_config_key_unique",
"columns": [
"key"
]
}
},
"checkConstraint": {}
},
"presale_stats_cache": {
"name": "presale_stats_cache",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"chain": {
"name": "chain",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtRaised": {
"name": "usdtRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"tokensSold": {
"name": "tokensSold",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"weiRaised": {
"name": "weiRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"lastUpdated": {
"name": "lastUpdated",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_stats_cache_id": {
"name": "presale_stats_cache_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"transaction_logs": {
"name": "transaction_logs",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"chainType": {
"name": "chainType",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toAddress": {
"name": "toAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"orderNo": {
"name": "orderNo",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"transaction_logs_id": {
"name": "transaction_logs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"transaction_logs_txHash_unique": {
"name": "transaction_logs_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"trc20_intents": {
"name": "trc20_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"tronAddress": {
"name": "tronAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedPurchaseId": {
"name": "matchedPurchaseId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_intents_id": {
"name": "trc20_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_purchases": {
"name": "trc20_purchases",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtAmount": {
"name": "usdtAmount",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_purchases_id": {
"name": "trc20_purchases_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"trc20_purchases_txHash_unique": {
"name": "trc20_purchases_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@ -0,0 +1,959 @@
{
"version": "5",
"dialect": "mysql",
"id": "3cfb2ddb-f45d-428a-80da-78d8e6fed501",
"prevId": "a2f8d4a4-e049-4e02-8011-f14d50b32f7e",
"tables": {
"bridge_intents": {
"name": "bridge_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"senderAddress": {
"name": "senderAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedOrderId": {
"name": "matchedOrderId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_intents_id": {
"name": "bridge_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"bridge_orders": {
"name": "bridge_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"walletAddress": {
"name": "walletAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromChainId": {
"name": "fromChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromToken": {
"name": "fromToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAmount": {
"name": "fromAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toChainId": {
"name": "toChainId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 56
},
"toToken": {
"name": "toToken",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'XIC'"
},
"toAmount": {
"name": "toAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"confirmedAt": {
"name": "confirmedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"bridge_orders_id": {
"name": "bridge_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"bridge_orders_txHash_unique": {
"name": "bridge_orders_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"fiat_orders": {
"name": "fiat_orders",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"orderId": {
"name": "orderId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"gatewayOrderId": {
"name": "gatewayOrderId",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"channel": {
"name": "channel",
"type": "enum('alipay','wechat','paypal')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payerEmail": {
"name": "payerEmail",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payerOpenId": {
"name": "payerOpenId",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"xicReceiveAddress": {
"name": "xicReceiveAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"usdtEquivalent": {
"name": "usdtEquivalent",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"currency": {
"name": "currency",
"type": "varchar(8)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'USD'"
},
"originalAmount": {
"name": "originalAmount",
"type": "decimal(20,4)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','paid','distributed','refunded','failed','expired')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"qrCodeUrl": {
"name": "qrCodeUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"paymentUrl": {
"name": "paymentUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"callbackPayload": {
"name": "callbackPayload",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expiredAt": {
"name": "expiredAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"fiat_orders_id": {
"name": "fiat_orders_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"fiat_orders_orderId_unique": {
"name": "fiat_orders_orderId_unique",
"columns": [
"orderId"
]
}
},
"checkConstraint": {}
},
"listener_state": {
"name": "listener_state",
"columns": {
"id": {
"name": "id",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastBlock": {
"name": "lastBlock",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"listener_state_id": {
"name": "listener_state_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"presale_config": {
"name": "presale_config",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"key": {
"name": "key",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"label": {
"name": "label",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'text'"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_config_id": {
"name": "presale_config_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"presale_config_key_unique": {
"name": "presale_config_key_unique",
"columns": [
"key"
]
}
},
"checkConstraint": {}
},
"presale_stats_cache": {
"name": "presale_stats_cache",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"chain": {
"name": "chain",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtRaised": {
"name": "usdtRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"tokensSold": {
"name": "tokensSold",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"weiRaised": {
"name": "weiRaised",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0'"
},
"lastUpdated": {
"name": "lastUpdated",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"presale_stats_cache_id": {
"name": "presale_stats_cache_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"transaction_logs": {
"name": "transaction_logs",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"chainType": {
"name": "chainType",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"toAddress": {
"name": "toAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"orderNo": {
"name": "orderNo",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"transaction_logs_id": {
"name": "transaction_logs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"transaction_logs_txHash_unique": {
"name": "transaction_logs_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"trc20_intents": {
"name": "trc20_intents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"tronAddress": {
"name": "tronAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expectedUsdt": {
"name": "expectedUsdt",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"matched": {
"name": "matched",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"matchedPurchaseId": {
"name": "matchedPurchaseId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_intents_id": {
"name": "trc20_intents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"trc20_purchases": {
"name": "trc20_purchases",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"txHash": {
"name": "txHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromAddress": {
"name": "fromAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"usdtAmount": {
"name": "usdtAmount",
"type": "decimal(20,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"xicAmount": {
"name": "xicAmount",
"type": "decimal(30,6)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blockNumber": {
"name": "blockNumber",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','confirmed','distributed','failed')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"distributedAt": {
"name": "distributedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"distributeTxHash": {
"name": "distributeTxHash",
"type": "varchar(128)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"evmAddress": {
"name": "evmAddress",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trc20_purchases_id": {
"name": "trc20_purchases_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"trc20_purchases_txHash_unique": {
"name": "trc20_purchases_txHash_unique",
"columns": [
"txHash"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,48 @@
"when": 1772955197567,
"tag": "0004_parallel_unus",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1773124399358,
"tag": "0005_certain_betty_ross",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1773135614044,
"tag": "0006_colossal_unicorn",
"breakpoints": true
},
{
"idx": 7,
"version": "5",
"when": 1773136228889,
"tag": "0007_wide_menace",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1773142627500,
"tag": "0008_lowly_pride",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1773146163886,
"tag": "0009_charming_lady_deathstrike",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1773149657785,
"tag": "0010_grey_johnny_blaze",
"breakpoints": true
}
]
}

View File

@ -96,4 +96,124 @@ export const presaleConfig = mysqlTable("presale_config", {
});
export type PresaleConfig = typeof presaleConfig.$inferSelect;
export type InsertPresaleConfig = typeof presaleConfig.$inferInsert;
export type InsertPresaleConfig = typeof presaleConfig.$inferInsert;
// Cross-chain bridge orders — NAC self-developed cross-chain bridge
// User sends USDT on any supported chain to our receiving address
// Backend monitors and records confirmed transfers, then distributes XIC
export const bridgeOrders = mysqlTable("bridge_orders", {
id: int("id").autoincrement().primaryKey(),
txHash: varchar("txHash", { length: 128 }).notNull().unique(),
walletAddress: varchar("walletAddress", { length: 64 }).notNull(), // sender wallet on source chain
fromChainId: int("fromChainId").notNull(),
fromToken: varchar("fromToken", { length: 32 }).notNull(),
fromAmount: decimal("fromAmount", { precision: 30, scale: 6 }).notNull(), // USDT amount sent
toChainId: int("toChainId").notNull().default(56), // always BSC for XIC
toToken: varchar("toToken", { length: 32 }).notNull().default("XIC"),
toAmount: decimal("toAmount", { precision: 30, scale: 6 }).notNull(), // XIC amount to distribute
xicReceiveAddress: varchar("xicReceiveAddress", { length: 64 }), // BSC address to receive XIC
status: mysqlEnum("status", ["pending", "confirmed", "distributed", "failed"]).default("pending").notNull(),
confirmedAt: timestamp("confirmedAt"),
distributedAt: timestamp("distributedAt"),
distributeTxHash: varchar("distributeTxHash", { length: 128 }),
blockNumber: bigint("blockNumber", { mode: "number" }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type BridgeOrder = typeof bridgeOrders.$inferSelect;
export type InsertBridgeOrder = typeof bridgeOrders.$inferInsert;
// Bridge deposit intents — user pre-registers before sending USDT
// Helps backend match incoming transfers to the correct XIC receive address
export const bridgeIntents = mysqlTable("bridge_intents", {
id: int("id").autoincrement().primaryKey(),
fromChainId: int("fromChainId").notNull(),
senderAddress: varchar("senderAddress", { length: 64 }), // sender on source chain (optional, filled when wallet connected)
xicReceiveAddress: varchar("xicReceiveAddress", { length: 64 }).notNull(), // BSC address to receive XIC
expectedUsdt: decimal("expectedUsdt", { precision: 20, scale: 6 }), // expected USDT amount
matched: boolean("matched").default(false).notNull(),
matchedOrderId: int("matchedOrderId"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type BridgeIntent = typeof bridgeIntents.$inferSelect;
export type InsertBridgeIntent = typeof bridgeIntents.$inferInsert;
// Transaction logs — idempotency guard to prevent double-processing
// Every on-chain transfer (ERC20 or TRC20) is recorded here before processing.
// If txHash already exists → skip (prevents double-distribution on re-org or retry).
export const transactionLogs = mysqlTable("transaction_logs", {
id: int("id").autoincrement().primaryKey(),
txHash: varchar("txHash", { length: 128 }).notNull().unique(),
chainType: varchar("chainType", { length: 16 }).notNull(), // 'ERC20' | 'TRC20' | 'ALIPAY' | 'WECHAT' | 'PAYPAL'
fromAddress: varchar("fromAddress", { length: 64 }).notNull(),
toAddress: varchar("toAddress", { length: 64 }).notNull(),
amount: decimal("amount", { precision: 30, scale: 6 }).notNull(),
blockNumber: bigint("blockNumber", { mode: "number" }),
status: int("status").default(0).notNull(), // 0=unprocessed, 1=processed, 2=no_match
orderNo: varchar("orderNo", { length: 128 }), // matched bridge order txHash or fiat orderId
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type TransactionLog = typeof transactionLogs.$inferSelect;
export type InsertTransactionLog = typeof transactionLogs.$inferInsert;
// Listener state — tracks last processed block per chain
// Prevents re-scanning already-processed blocks on restart
export const listenerState = mysqlTable("listener_state", {
id: varchar("id", { length: 32 }).primaryKey(), // 'erc20' | 'trc20'
lastBlock: bigint("lastBlock", { mode: "number" }).notNull().default(0),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ListenerState = typeof listenerState.$inferSelect;
// ─── Fiat Payment Orders ──────────────────────────────────────────────────────
// Records orders created via Alipay / WeChat Pay / PayPal.
// On payment success callback, creditXic() is called to distribute XIC tokens.
// orderId is the unique order number generated by our system (e.g. ALIPAY-20260310-xxxxx).
// gatewayOrderId is the payment gateway's own order number (for reconciliation).
export const fiatOrders = mysqlTable("fiat_orders", {
id: int("id").autoincrement().primaryKey(),
orderId: varchar("orderId", { length: 64 }).notNull().unique(), // our internal order ID
gatewayOrderId: varchar("gatewayOrderId", { length: 128 }), // gateway's order ID
channel: mysqlEnum("channel", ["alipay", "wechat", "paypal"]).notNull(),
userId: varchar("userId", { length: 64 }), // Manus user ID (optional)
payerEmail: varchar("payerEmail", { length: 128 }), // PayPal payer email
payerOpenId: varchar("payerOpenId", { length: 128 }), // WeChat openid
xicReceiveAddress: varchar("xicReceiveAddress", { length: 64 }), // BSC address to receive XIC
usdtEquivalent: decimal("usdtEquivalent", { precision: 20, scale: 6 }).notNull(), // USD/CNY converted to USD
currency: varchar("currency", { length: 8 }).notNull().default("USD"), // USD | CNY
originalAmount: decimal("originalAmount", { precision: 20, scale: 4 }).notNull(), // amount in original currency
xicAmount: decimal("xicAmount", { precision: 30, scale: 6 }).notNull(), // XIC tokens to distribute
status: mysqlEnum("status", ["pending", "paid", "distributed", "refunded", "failed", "expired"])
.default("pending")
.notNull(),
qrCodeUrl: text("qrCodeUrl"), // WeChat/Alipay QR code URL
paymentUrl: text("paymentUrl"), // redirect URL (Alipay H5 / PayPal)
callbackPayload: text("callbackPayload"), // raw callback body for audit
distributedAt: timestamp("distributedAt"),
expiredAt: timestamp("expiredAt"), // order expiry time
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type FiatOrder = typeof fiatOrders.$inferSelect;
export type InsertFiatOrder = typeof fiatOrders.$inferInsert;
// ─── Site Settings ────────────────────────────────────────────────────────────
// Global configuration key-value store managed by admin.
// Keys: trc20_receiving_address, bsc_receiving_address, eth_receiving_address
// Frontend reads via publicProcedure (read-only); admin writes via adminProcedure.
export const siteSettings = mysqlTable("site_settings", {
id: int("id").autoincrement().primaryKey(),
key: varchar("key", { length: 64 }).notNull().unique(),
value: text("value").notNull(),
label: varchar("label", { length: 128 }),
description: varchar("description", { length: 256 }),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type SiteSetting = typeof siteSettings.$inferSelect;
export type InsertSiteSetting = typeof siteSettings.$inferInsert;

View File

@ -1,22 +0,0 @@
module.exports = {
apps: [{
name: 'nac-presale-test',
script: '/www/wwwroot/nac-presale-test/dist/index.js',
cwd: '/www/wwwroot/nac-presale-test',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '512M',
env: {
NODE_ENV: 'production',
PORT: '3100',
DATABASE_URL: 'mysql://nac_presale:NACpresale2026!@127.0.0.1:3306/nac_presale',
JWT_SECRET: 'NACpresaleJWT2026SecretKey!@#$%^',
VITE_APP_ID: 'nac-presale',
OAUTH_SERVER_URL: 'https://api.manus.im',
OWNER_OPEN_ID: 'nacadmin',
BUILT_IN_FORGE_API_URL: 'https://api.manus.im',
BUILT_IN_FORGE_API_KEY: 'nac-presale-forge-key'
}
}]
};

View File

@ -1,86 +0,0 @@
# NAC XIC Token 预售网站 v6 最终部署日志
**部署时间:** 2026-03-09 00:34 UTC
**部署服务器:** 103.96.148.7:22000
**部署域名:** https://pre-sale.newassetchain.io
**PM2 进程:** nac-presale (id:0, cluster 模式, status: online)
**运行端口:** 3002
**Nginx 代理:** pre-sale.newassetchain.io → 127.0.0.1:3002
---
## 部署验证结果
| 检查项 | 结果 |
|--------|------|
| 生产构建 | ✓ 成功8.18s |
| Manus 内联清除 | ✓ HTML 无 manus-runtime/__manus__ |
| 数据库备份 | ✓ /www/backup/nac_presale_20260309_*.sql |
| 旧版本备份 | ✓ dist.bak.20260309* |
| 新版本部署 | ✓ /www/wwwroot/nac-presale-app/dist |
| PM2 重启 | ✓ online重启次数: 6 |
| HTTP 端口 3002 | ✓ 200 OK |
| 域名 HTTPS | ✓ 200 OK |
| 浏览器测试 | ✓ 页面正常加载 |
| TRON 标签功能 | ✓ TronLink 检测区域正常显示 |
| EVM 地址输入 | ✓ Connect MetaMask 按钮正常 |
| TRC20 收款地址 | ✓ TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb 正常显示 |
| 链上数据 | ✓ Live On-Chain Data 0.2%$9,900 已筹集 |
---
## 本次更新内容v6
### TronLink 钱包检测功能
- TRON 标签新增 TronLink 检测区域(红色边框卡片)
- 未安装 TronLink显示"Install TronLink Wallet →"链接
- 已安装未连接:显示"连接 TronLink 自动验证"按钮
- 已连接:显示绿色已连接地址确认卡片
- 支持中英文双语
### EVM 地址自动填充
- 新增"Connect MetaMask to auto-fill"按钮
- 已连接 EVM 钱包时自动同步地址
- 修复 connectedAddress prop 变化不更新的问题
### 合约地址(已确认正确)
- BSC 预售合约:`0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c`
- ETH 预售合约:`0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3`
- XIC 代币合约:`0x59ff34dd59680a7125782b1f6df2a86ed46f5a24`
---
## 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 103.96.148.7 |
| SSH 端口 | 22000 |
| 管理员用户名 | root |
| 管理员密码 | XKUigTFMJXhH |
| 宝塔面板 | http://103.96.148.7:12/btwest |
| 面板账号 | cproot |
| 面板密码 | vajngkvf |
| 数据库名 | nac_presale |
| 数据库用户 | root |
| 数据库密码 | vaingkvf |
| PM2 进程 | nac-presale (id:0, port:3002) |
| PM2 路径 | /home/server/nodejs/v24.13.0/lib/node_modules/pm2/bin/pm2 |
---
## 后台管理员账号
| 项目 | 值 |
|------|-----|
| 后台地址 | https://pre-sale.newassetchain.io/admin |
| 管理员用户名 | admin |
| 管理员密码 | NACadmin2026! |
---
## 待办事项
- [ ] 申请 Telegram Bot 并配置通知Bot Token + Chat ID
- [ ] 完整链上购买测试(真实 MetaMask + USDT
- [ ] 同步代码到 Gitea 库nacadmin/xic-presale

View File

@ -1,81 +0,0 @@
# NAC XIC Token 预售网站 v6 部署日志
**部署时间:** 2026-03-09
**部署服务器:** 103.96.148.7:22000
**部署域名:** https://pre-sale.newassetchain.io
**PM2 进程名:** nac-presale端口 3002
---
## 本次更新内容
### 1. TronLink 钱包检测功能
- TRON 标签新增 TronLink 钱包检测区域(红色边框卡片)
- 已安装 TronLink 且未连接:显示"连接 TronLink 自动验证"按钮
- 已安装 TronLink 且已连接:显示已连接的 TRON 地址(绿色)
- 未安装 TronLink显示"安装 TronLink 钱包 →"链接(跳转 tronlink.org
- 页面加载时自动检测 TronLink 连接状态(`tronWeb.defaultAddress.base58`
- 支持中英文双语显示
### 2. EVM 地址自动填充优化
- 新增"Connect MetaMask to auto-fill"按钮(蓝色边框)
- 已连接 EVM 钱包时自动同步地址到输入框
- 修复 `connectedAddress` prop 变化时不更新的问题
### 3. 合约地址确认
- BSC 预售合约:`0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c` ✓
- ETH 预售合约:`0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3` ✓
- XIC 代币合约:`0x59ff34dd59680a7125782b1f6df2a86ed46f5a24` ✓
---
## 部署流程
| 步骤 | 状态 | 备注 |
|------|------|------|
| 数据库备份 | ✓ | `/www/backup/nac_presale_20260309_*.sql` |
| TypeScript 检查 | ✓ | 无错误 |
| 生产构建 | ✓ | `pnpm build` 成功 |
| Manus 内联清除 | ✓ | HTML 无 `manus-runtime`/`__manus__` |
| 旧版本备份 | ✓ | `dist.bak.20260309*` |
| 新版本部署 | ✓ | `/www/wwwroot/nac-presale-app/dist` |
| PM2 重启 | ✓ | `nac-presale` 进程 online |
| HTTP 测试 | ✓ | 200 OK |
| 域名浏览器测试 | ✓ | TRON 标签 TronLink 区域正常显示 |
---
## 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 103.96.148.7 |
| SSH 端口 | 22000 |
| 管理员用户名 | root |
| 管理员密码 | XKUigTFMJXhH |
| 宝塔面板 | http://103.96.148.7:12/btwest |
| 面板账号 | cproot |
| 面板密码 | vajngkvf |
| 数据库名 | nac_presale |
| 数据库用户 | root |
| 数据库密码 | vaingkvf |
| PM2 进程 | nac-presale (id:0, port:3002) |
| Nginx 配置 | /www/vhost/nginx/pre-sale.newassetchain.io.conf |
---
## 后台管理员账号
| 项目 | 值 |
|------|-----|
| 后台地址 | https://pre-sale.newassetchain.io/admin |
| 管理员用户名 | admin |
| 管理员密码 | NACadmin2026! |
---
## 待办事项
- [ ] 申请 Telegram Bot 并配置通知Bot Token + Chat ID
- [ ] 完整链上购买测试(真实 MetaMask + USDT
- [ ] 同步代码到 Gitea 库nacadmin/xic-presale

View File

@ -1,164 +0,0 @@
# NAC XIC Token 预售网站 v6 浏览器测试报告
**测试日期:** 2026-03-09
**测试域名:** https://pre-sale.newassetchain.io
**测试版本:** v6检查点 809b6327
**测试人员:** Manus 自动化测试
**服务器:** 103.96.148.7(备份服务器)
---
## 后台管理员账号
| 项目 | 值 |
|------|-----|
| 后台地址 | https://pre-sale.newassetchain.io/admin |
| 管理员密码 | NACadmin2026! |
---
## 测试结果总览
| 测试项 | 状态 | 说明 |
|--------|------|------|
| 主页加载 | ✅ 通过 | 页面正常加载,无 JS 错误 |
| Manus 内联清除 | ✅ 通过 | 无 manus-runtime、__manus__ 等引用 |
| 倒计时器 | ✅ 通过 | 显示 113天 18时 49分 17秒实时更新 |
| 募资进度条 | ✅ 通过 | 显示 $9,900 已募集,$5.00M 硬顶 |
| 链上数据标识 | ✅ 通过 | 显示 "Live On-Chain Data 0.2%" |
| 代币统计数据 | ✅ 通过 | 495.00K XIC 已售,$0.02 代币价格,$0.10 目标上市价 |
---
## 购买面板测试
### BSC 标签
| 测试项 | 状态 | 说明 |
|--------|------|------|
| BSC 标签切换 | ✅ 通过 | 正确显示 BSC ERC20 USDT |
| 合约地址 | ✅ 通过 | 0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c |
| Connect Wallet 按钮 | ✅ 通过 | 显示正常,点击无报错 |
| USDT 金额输入 | ✅ 通过 | 快捷按钮 $100/$500/$1000/$5000 正常 |
### ETH 标签
| 测试项 | 状态 | 说明 |
|--------|------|------|
| ETH 标签切换 | ✅ 通过 | 正确显示 Ethereum ERC20 USDT |
| 合约地址 | ✅ 通过 | 0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3 |
### TRON 标签
| 测试项 | 状态 | 说明 |
|--------|------|------|
| TRON 标签切换 | ✅ 通过 | 正确显示 TRON TRC20 USDT |
| USDT 金额输入 | ✅ 通过 | 输入 500 正常,快捷按钮正常 |
| EVM 地址输入框 | ✅ 通过 | 显示警告提示,输入框可用 |
| EVM 地址保存 | ✅ 通过 | API 调用 presale.registerTrc20Intent 成功57ms |
| MetaMask 自动填充按钮 | ✅ 通过 | 显示 "Connect MetaMask to auto-fill" 按钮 |
| TronLink 检测区域 | ✅ 通过 | 显示 "TronLink 钱包(可选)" 区域 |
| TronLink 安装链接 | ✅ 通过 | 显示 "安装 TronLink 钱包 →" 链接 |
| TRON 接收地址 | ✅ 通过 | 显示 TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb |
| 复制地址按钮 | ✅ 通过 | 按钮显示正常 |
| BSC 合约链接 | ✅ 通过 | 显示 "BSC 合约 ↗" 外链 |
| ETH 合约链接 | ✅ 通过 | 显示 "ETH 合约 ↗" 外链 |
---
## 多语言测试
| 测试项 | 状态 | 说明 |
|--------|------|------|
| 英文模式 | ✅ 通过 | 默认英文,所有文本正确 |
| 中文切换 | ✅ 通过 | 点击"中文"按钮后全站切换为中文 |
| 中文导航栏 | ✅ 通过 | 官网/浏览器/文档/购买教程 |
| 中文购买面板 | ✅ 通过 | "购买 XIC 代币"、"选择网络"等 |
| 中文 FAQ | ✅ 通过 | 8 个 FAQ 项目全部中文显示 |
| 中文 FAQ 展开 | ✅ 通过 | 点击 "01 XIC 代币是什么?" 正确展开 |
| 中文功能卡片 | ✅ 通过 | "原生 RWA 公链"、"CBPP 共识协议"、"Charter 智能合约" |
---
## FAQ 测试
| 测试项 | 状态 | 说明 |
|--------|------|------|
| FAQ 列表显示 | ✅ 通过 | 8 个问题全部显示 |
| FAQ 手风琴展开 | ✅ 通过 | 点击后内容正确展开 |
| FAQ 内容准确性 | ✅ 通过 | 内容与 NAC 技术描述一致 |
---
## 教程页面测试(/tutorial
| 测试项 | 状态 | 说明 |
|--------|------|------|
| 教程页面加载 | ✅ 通过 | 正常加载 |
| 钱包选择器 | ✅ 通过 | MetaMask/Trust Wallet/OKX/Binance Web3 等 7 个钱包 |
| 网络选择器 | ✅ 通过 | BSC/ETH/TRON 三个网络 |
| 步骤内容 | ✅ 通过 | MetaMask+BSC 显示 6 步详细教程 |
| 返回按钮 | ✅ 通过 | "← New AssetChain TUTORIAL" 返回链接 |
| 语言切换 | ✅ 通过 | 教程页面有语言切换按钮 |
---
## 管理员后台测试(/admin
| 测试项 | 状态 | 说明 |
|--------|------|------|
| 登录页面 | ✅ 通过 | 正常显示密码输入框 |
| 密码验证 | ✅ 通过 | NACadmin2026! 登录成功 |
| 统计面板 | ✅ 通过 | $9,900 已募集、0.49M XIC 已售、1 笔购买、1 待分发 |
| TRC20 购买列表 | ✅ 通过 | 1 条记录,状态 Confirmed |
| 状态筛选器 | ✅ 通过 | All/Pending/Confirmed/Distributed/Failed |
| 导出 CSV | ✅ 通过 | 按钮显示正常 |
| EVM 地址意图标签 | ✅ 通过 | 标签切换正常 |
| 站点设置标签 | ✅ 通过 | 正常显示所有配置项 |
| 预售结束时间 | ✅ 通过 | 2026-06-30 23:59 |
| 代币价格 | ✅ 通过 | $0.02 USDT |
| 硬顶 | ✅ 通过 | 5,000,000 USDT |
| 上市目标价格 | ✅ 通过 | $0.10 USDT |
| 总供应量 | ✅ 通过 | 100,000,000,000 XIC |
| TRON 接收地址配置 | ✅ 通过 | TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp |
| Telegram Bot 配置 | ✅ 通过 | 显示 Token 和 Chat ID 输入框,待配置 |
---
## API 接口测试
| 接口 | 状态 | 响应时间 |
|------|------|---------|
| presale.stats | ✅ 通过 | 1171ms |
| presale.recentPurchases | ✅ 通过 | 46ms |
| presale.registerTrc20Intent | ✅ 通过 | 57ms |
---
## 发现的问题
| 问题 | 严重程度 | 状态 |
|------|---------|------|
| 无真实 MetaMask 环境,无法测试完整购买流程 | 低 | 需用户真实环境测试 |
| 无真实 TronLink 环境,无法测试 TronLink 自动连接 | 低 | 需用户真实环境测试 |
| Telegram Bot 未配置 | 低 | 待用户提供 Token 后配置 |
---
## 待办事项
- [ ] 用户提供 Telegram Bot Token 和 Chat ID 后配置通知
- [ ] 用真实 MetaMask 测试 BSC/ETH 购买流程
- [ ] 用真实 TronLink 测试 TRON 购买流程
- [ ] 同步代码到 Gitea 代码库
---
## 部署信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 103.96.148.7 |
| 应用端口 | 3002 |
| PM2 进程名 | nac-presale-app |
| Nginx 域名 | pre-sale.newassetchain.io |
| 数据库 | MySQLnac_presale 库) |
| 数据库备份 | /tmp/nac_presale_backup_*.sql |
| 检查点版本 | 809b6327 |

19115
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,8 @@
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/s3-request-presigner": "^3.693.0",
"@hookform/resolvers": "^5.2.2",
"@lifi/sdk": "^3.16.0",
"@lifi/wallet-management": "^3.22.7",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@ -42,7 +44,7 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query": "^5.90.21",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
@ -79,6 +81,7 @@
"tronweb": "^6.2.2",
"vaul": "^1.1.2",
"viem": "^2.47.0",
"wagmi": "^3.5.0",
"wouter": "^3.3.5",
"zod": "^4.1.12"
},

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import { appRouter } from "../routers";
import { createContext } from "./context";
import { serveStatic, setupVite } from "./vite";
import { startTRC20Monitor } from "../trc20Monitor";
import { startBridgeMonitor } from "../bridgeMonitor";
function isPortAvailable(port: number): Promise<boolean> {
return new Promise(resolve => {
@ -64,6 +65,9 @@ async function startServer() {
// Start TRC20 monitor in background
startTRC20Monitor().catch(e => console.error("[TRC20Monitor] Start error:", e));
// Start Bridge monitor (multi-chain USDT deposit listener)
startBridgeMonitor();
}
startServer().catch(console.error);

302
server/bridgeMonitor.ts Normal file
View File

@ -0,0 +1,302 @@
/**
* NAC Cross-Chain Bridge Monitor
* Self-developed bridge: monitors USDT transfers on BSC/ETH/Polygon/Arbitrum/Avalanche
* When user sends USDT to our receiving address, we record the order and distribute XIC
*/
import { getDb } from "./db";
import { bridgeOrders, bridgeIntents } from "../drizzle/schema";
import { eq, and, desc } from "drizzle-orm";
import { creditXic } from "./tokenDistributionService";
// ─── Presale Config ───────────────────────────────────────────────────────────
export const XIC_PRICE_USDT = 0.02; // $0.02 per XIC
// ─── Chain Configs ────────────────────────────────────────────────────────────
export interface ChainConfig {
chainId: number;
name: string;
symbol: string; // native token symbol (BNB, ETH, MATIC, AVAX)
icon: string;
color: string;
usdtAddress: string; // USDT contract on this chain
receivingAddress: string; // Our USDT receiving address on this chain
rpcUrl: string;
explorerUrl: string;
explorerTxPath: string; // e.g. /tx/
decimals: number; // USDT decimals (6 for most, 18 for BSC)
}
export const BRIDGE_CHAINS: ChainConfig[] = [
{
chainId: 56,
name: "BSC",
symbol: "BNB",
icon: "🟡",
color: "#F0B90B",
usdtAddress: "0x55d398326f99059fF775485246999027B3197955",
receivingAddress: process.env.BRIDGE_BSC_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com",
explorerTxPath: "/tx/",
decimals: 18, // BSC USDT is 18 decimals
},
{
chainId: 1,
name: "Ethereum",
symbol: "ETH",
icon: "🔵",
color: "#627EEA",
usdtAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
receivingAddress: process.env.BRIDGE_ETH_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
rpcUrl: "https://ethereum.publicnode.com",
explorerUrl: "https://etherscan.io",
explorerTxPath: "/tx/",
decimals: 6, // ETH USDT is 6 decimals
},
{
chainId: 137,
name: "Polygon",
symbol: "MATIC",
icon: "🟣",
color: "#8247E5",
usdtAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
receivingAddress: process.env.BRIDGE_POLYGON_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
rpcUrl: "https://polygon-rpc.com/",
explorerUrl: "https://polygonscan.com",
explorerTxPath: "/tx/",
decimals: 6,
},
{
chainId: 42161,
name: "Arbitrum",
symbol: "ETH",
icon: "🔷",
color: "#28A0F0",
usdtAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
receivingAddress: process.env.BRIDGE_ARB_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
rpcUrl: "https://arb1.arbitrum.io/rpc",
explorerUrl: "https://arbiscan.io",
explorerTxPath: "/tx/",
decimals: 6,
},
{
chainId: 43114,
name: "Avalanche",
symbol: "AVAX",
icon: "🔴",
color: "#E84142",
usdtAddress: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
receivingAddress: process.env.BRIDGE_AVAX_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
rpcUrl: "https://api.avax.network/ext/bc/C/rpc",
explorerUrl: "https://snowtrace.io",
explorerTxPath: "/tx/",
decimals: 6,
},
];
// ─── ERC-20 Transfer event ABI ────────────────────────────────────────────────
// Transfer(address indexed from, address indexed to, uint256 value)
const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
// ─── Fetch USDT transfers to our address via eth_getLogs ──────────────────────
async function fetchUsdtTransfers(chain: ChainConfig, fromBlock: string = "latest"): Promise<Array<{
txHash: string;
fromAddress: string;
toAddress: string;
amount: number; // in USDT (human-readable)
blockNumber: number;
}>> {
try {
// Pad address to 32 bytes for topic matching
const paddedTo = "0x000000000000000000000000" + chain.receivingAddress.slice(2).toLowerCase();
const payload = {
jsonrpc: "2.0",
id: 1,
method: "eth_getLogs",
params: [{
fromBlock: fromBlock === "latest" ? "latest" : fromBlock,
toBlock: "latest",
address: chain.usdtAddress,
topics: [
TRANSFER_TOPIC,
null, // any sender
paddedTo, // to our receiving address
],
}],
};
const res = await fetch(chain.rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10000),
});
const data = await res.json();
if (!data.result || !Array.isArray(data.result)) return [];
return data.result.map((log: any) => {
const fromAddress = "0x" + log.topics[1].slice(26);
const toAddress = "0x" + log.topics[2].slice(26);
const rawAmount = BigInt(log.data);
const divisor = BigInt(10 ** chain.decimals);
const amount = Number(rawAmount) / Number(divisor);
const blockNumber = parseInt(log.blockNumber, 16);
return {
txHash: log.transactionHash,
fromAddress: fromAddress.toLowerCase(),
toAddress: toAddress.toLowerCase(),
amount,
blockNumber,
};
});
} catch (err) {
console.error(`[BridgeMonitor] Error fetching transfers on ${chain.name}:`, err);
return [];
}
}
// ─── Get latest block number ──────────────────────────────────────────────────
async function getLatestBlock(chain: ChainConfig): Promise<number> {
try {
const res = await fetch(chain.rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
signal: AbortSignal.timeout(8000),
});
const data = await res.json();
return parseInt(data.result, 16);
} catch {
return 0;
}
}
// ─── Process new transfers and save to DB ────────────────────────────────────
async function processTransfers(chain: ChainConfig): Promise<void> {
const db = await getDb();
if (!db) return;
// Get recent blocks (last ~100 blocks ≈ ~5 min on BSC, ~20 min on ETH)
const latestBlock = await getLatestBlock(chain);
if (!latestBlock) return;
const lookbackBlocks = chain.chainId === 1 ? 50 : 200; // ETH slower, BSC faster
const fromBlock = "0x" + Math.max(0, latestBlock - lookbackBlocks).toString(16);
const transfers = await fetchUsdtTransfers(chain, fromBlock);
for (const transfer of transfers) {
if (transfer.amount < 0.01) continue; // ignore dust
// Check if already recorded
try {
const existing = await db
.select({ id: bridgeOrders.id })
.from(bridgeOrders)
.where(eq(bridgeOrders.txHash, transfer.txHash))
.limit(1);
if (existing.length > 0) continue; // already recorded
// Calculate XIC amount
const xicAmount = transfer.amount / XIC_PRICE_USDT;
// Try to find matching intent (user pre-registered their XIC receive address)
const intent = await db
.select()
.from(bridgeIntents)
.where(and(
eq(bridgeIntents.fromChainId, chain.chainId),
eq(bridgeIntents.senderAddress, transfer.fromAddress),
eq(bridgeIntents.matched, false),
))
.orderBy(desc(bridgeIntents.createdAt))
.limit(1);
const xicReceiveAddress = intent.length > 0 ? intent[0].xicReceiveAddress : null;
// Record the order first (pending status)
try {
await db.insert(bridgeOrders).values({
txHash: transfer.txHash,
walletAddress: transfer.fromAddress,
fromChainId: chain.chainId,
fromToken: "USDT",
fromAmount: String(transfer.amount),
toChainId: 56,
toToken: "XIC",
toAmount: String(xicAmount),
xicReceiveAddress,
status: "pending",
blockNumber: transfer.blockNumber,
});
} catch (insertErr: any) {
if (insertErr?.code !== "ER_DUP_ENTRY") throw insertErr;
}
// Mark intent as matched
if (intent.length > 0) {
await db
.update(bridgeIntents)
.set({ matched: true, matchedOrderId: undefined })
.where(eq(bridgeIntents.id, intent[0].id));
}
// Use unified tokenDistributionService (idempotent via transaction_logs)
await creditXic({
txHash: transfer.txHash,
chainType: "ERC20",
fromAddress: transfer.fromAddress,
toAddress: chain.receivingAddress,
usdtAmount: transfer.amount,
xicAmount,
blockNumber: transfer.blockNumber,
xicReceiveAddress: xicReceiveAddress ?? undefined,
remark: `${chain.name} auto-detected`,
});
console.log(`[BridgeMonitor] New ${chain.name} deposit: ${transfer.amount} USDT from ${transfer.fromAddress}${xicAmount} XIC`);
} catch (err: any) {
if (err?.code === "ER_DUP_ENTRY") continue;
console.error(`[BridgeMonitor] Error recording transfer ${transfer.txHash}:`, err);
}
}
}
// ─── Start monitoring all chains ──────────────────────────────────────────────
let monitorInterval: NodeJS.Timeout | null = null;
export function startBridgeMonitor(): void {
if (monitorInterval) return;
console.log("[BridgeMonitor] Starting multi-chain USDT deposit monitor...");
const run = async () => {
for (const chain of BRIDGE_CHAINS) {
await processTransfers(chain).catch(err =>
console.error(`[BridgeMonitor] Error on ${chain.name}:`, err)
);
}
};
// Run immediately, then every 30 seconds
run();
monitorInterval = setInterval(run, 30_000);
}
export function stopBridgeMonitor(): void {
if (monitorInterval) {
clearInterval(monitorInterval);
monitorInterval = null;
console.log("[BridgeMonitor] Stopped.");
}
}
// ─── Export chain config for use in routes ───────────────────────────────────
export function getChainConfig(chainId: number): ChainConfig | undefined {
return BRIDGE_CHAINS.find(c => c.chainId === chainId);
}

131
server/fiatPayment.test.ts Normal file
View File

@ -0,0 +1,131 @@
/**
* Fiat Payment Services Unit Tests
* Tests for Alipay, WeChat Pay, and PayPal service helper functions.
* All external API calls are mocked; no real credentials required.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ─── Mock DB ──────────────────────────────────────────────────────────────────
vi.mock("./db", () => ({
getDb: vi.fn().mockResolvedValue(null),
}));
vi.mock("./tokenDistributionService", () => ({
creditXic: vi.fn().mockResolvedValue({ success: true, alreadyProcessed: false }),
calcXicAmount: vi.fn().mockImplementation((usd: number) => Math.floor(usd / 0.02)),
}));
// ─── Alipay Tests ─────────────────────────────────────────────────────────────
describe("alipayService", () => {
it("generateAlipayOrderId returns ALIPAY- prefixed string", async () => {
const { generateAlipayOrderId } = await import("./services/alipayService");
const id = generateAlipayOrderId();
expect(id).toMatch(/^ALIPAY-\d+-[A-Z0-9]+$/);
});
it("generateAlipayOrderId is unique on each call", async () => {
const { generateAlipayOrderId } = await import("./services/alipayService");
const ids = new Set(Array.from({ length: 100 }, () => generateAlipayOrderId()));
expect(ids.size).toBe(100);
});
it("createAlipayOrder returns error when DB unavailable", async () => {
const { createAlipayOrder } = await import("./services/alipayService");
const result = await createAlipayOrder({
orderId: "ALIPAY-TEST-001",
subject: "Test",
totalAmount: "100.00",
xicReceiveAddress: "0x1234567890abcdef1234567890abcdef12345678",
});
// DB is mocked to return null, so should fail gracefully
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
// ─── WeChat Pay Tests ─────────────────────────────────────────────────────────
describe("wechatPayService", () => {
it("generateWechatOrderId returns WECHAT- prefixed string", async () => {
const { generateWechatOrderId } = await import("./services/wechatPayService");
const id = generateWechatOrderId();
expect(id).toMatch(/^WECHAT-\d+-[A-Z0-9]+$/);
});
it("generateWechatOrderId is unique on each call", async () => {
const { generateWechatOrderId } = await import("./services/wechatPayService");
const ids = new Set(Array.from({ length: 100 }, () => generateWechatOrderId()));
expect(ids.size).toBe(100);
});
it("createWechatOrder returns error when DB unavailable", async () => {
const { createWechatOrder } = await import("./services/wechatPayService");
const result = await createWechatOrder({
orderId: "WECHAT-TEST-001",
description: "Test",
totalFen: 10000,
xicReceiveAddress: "0x1234567890abcdef1234567890abcdef12345678",
payType: "NATIVE",
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
// ─── PayPal Tests ─────────────────────────────────────────────────────────────
describe("paypalService", () => {
it("generatePaypalOrderId returns PAYPAL- prefixed string", async () => {
const { generatePaypalOrderId } = await import("./services/paypalService");
const id = generatePaypalOrderId();
expect(id).toMatch(/^PAYPAL-\d+-[A-Z0-9]+$/);
});
it("generatePaypalOrderId is unique on each call", async () => {
const { generatePaypalOrderId } = await import("./services/paypalService");
const ids = new Set(Array.from({ length: 100 }, () => generatePaypalOrderId()));
expect(ids.size).toBe(100);
});
it("createPaypalOrder returns error when DB unavailable", async () => {
const { createPaypalOrder } = await import("./services/paypalService");
const result = await createPaypalOrder({
orderId: "PAYPAL-TEST-001",
usdAmount: "100.00",
xicReceiveAddress: "0x1234567890abcdef1234567890abcdef12345678",
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
// ─── tokenDistributionService.calcXicAmount ───────────────────────────────────
describe("calcXicAmount (mocked)", () => {
it("calculates XIC correctly at $0.02/XIC", async () => {
const { calcXicAmount } = await import("./tokenDistributionService");
expect(calcXicAmount(100)).toBe(5000);
expect(calcXicAmount(1000)).toBe(50000);
expect(calcXicAmount(0.02)).toBe(1);
});
});
// ─── Order ID Format Validation ───────────────────────────────────────────────
describe("Order ID format", () => {
it("all order IDs have correct prefix format", async () => {
const { generateAlipayOrderId } = await import("./services/alipayService");
const { generateWechatOrderId } = await import("./services/wechatPayService");
const { generatePaypalOrderId } = await import("./services/paypalService");
const alipay = generateAlipayOrderId();
const wechat = generateWechatOrderId();
const paypal = generatePaypalOrderId();
expect(alipay.startsWith("ALIPAY-")).toBe(true);
expect(wechat.startsWith("WECHAT-")).toBe(true);
expect(paypal.startsWith("PAYPAL-")).toBe(true);
// All should be under 64 chars (DB constraint)
expect(alipay.length).toBeLessThanOrEqual(64);
expect(wechat.length).toBeLessThanOrEqual(64);
expect(paypal.length).toBeLessThanOrEqual(64);
});
});

View File

@ -25,11 +25,11 @@ const RPC_POOLS = {
"https://rpc.ankr.com/bsc",
],
ETH: [
"https://eth.llamarpc.com",
"https://ethereum.publicnode.com",
"https://ethereum.publicnode.com", // China-accessible
"https://rpc.ankr.com/eth",
"https://1rpc.io/eth",
"https://eth.drpc.org",
"https://1rpc.io/eth",
"https://eth.llamarpc.com",
"https://cloudflare-eth.com",
"https://rpc.payload.de",
],
@ -38,7 +38,7 @@ const RPC_POOLS = {
// ─── Contract Addresses ────────────────────────────────────────────────────────
export const CONTRACTS = {
BSC: {
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4",
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
rpc: RPC_POOLS.BSC[0],
chainId: 56,
@ -62,18 +62,10 @@ export const CONTRACTS = {
},
};
// Minimal ABI for reading presale stats (supports both old and new contract)
// Minimal ABI for reading presale stats
const PRESALE_ABI = [
// New contract (XICPresale v2)
"function totalRaised() view returns (uint256)",
"function totalTokensSold() view returns (uint256)",
"function hardCap() view returns (uint256)",
"function isPresaleActive() view returns (bool)",
"function presaleProgress() view returns (uint256 sold, uint256 cap, uint256 progressBps)",
"function timeRemaining() view returns (uint256)",
"function availableXIC() view returns (uint256)",
// Old contract fallbacks
"function totalUSDTRaised() view returns (uint256)",
"function totalTokensSold() view returns (uint256)",
"function weiRaised() view returns (uint256)",
"function tokensSold() view returns (uint256)",
"function usdtRaised() view returns (uint256)",
@ -81,7 +73,7 @@ const PRESALE_ABI = [
// Token price: $0.02 per XIC
export const TOKEN_PRICE_USDT = 0.02;
export const HARD_CAP_USDT = 50_000_000; // 25亿 XIC × $0.02 = $5000万 USDT
export const HARD_CAP_USDT = 5_000_000;
export const TOTAL_SUPPLY = 100_000_000_000;
export const MAX_PURCHASE_USDT = 50_000;
@ -143,16 +135,16 @@ async function fetchChainStatsWithFailover(
// Try different function names that might exist in the contract
const usdtPromise = (async () => {
try {
const raw = await contract.totalRaised();
return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals
const raw = await contract.totalUSDTRaised();
return Number(ethers.formatUnits(raw, 6));
} catch {
try {
const raw = await contract.usdtRaised();
return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals
return Number(ethers.formatUnits(raw, 6));
} catch {
try {
const raw = await contract.weiRaised();
return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals
return Number(ethers.formatUnits(raw, 6));
} catch {
return 0;
}

View File

@ -3,7 +3,7 @@ import { CONTRACTS, TOKEN_PRICE_USDT, HARD_CAP_USDT, MAX_PURCHASE_USDT } from ".
describe("Presale Configuration", () => {
it("should have correct BSC presale contract address", () => {
expect(CONTRACTS.BSC.presale).toBe("0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c");
expect(CONTRACTS.BSC.presale).toBe("0x5953c025dA734e710886916F2d739A3A78f8bbc4");
});
it("should have correct ETH presale contract address", () => {

View File

@ -5,18 +5,389 @@ import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
import { getCombinedStats, getPresaleStats } from "./onchain";
import { getRecentPurchases } from "./trc20Monitor";
import { getDb } from "./db";
import { trc20Purchases, trc20Intents } from "../drizzle/schema";
import { trc20Purchases, trc20Intents, bridgeOrders, bridgeIntents } from "../drizzle/schema";
import { eq, desc, sql } from "drizzle-orm";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { notifyDistributed, testTelegramConnection } from "./telegram";
import { getAllConfig, getConfig, setConfig, seedDefaultConfig, DEFAULT_CONFIG } from "./configDb";
import { creditXic } from "./tokenDistributionService";
import {
createAlipayOrder,
handleAlipayCallback,
queryAlipayOrder,
generateAlipayOrderId,
} from "./services/alipayService";
import {
createWechatOrder,
handleWechatCallback,
queryWechatOrder,
generateWechatOrderId,
} from "./services/wechatPayService";
import {
createPaypalOrder,
capturePaypalOrder,
handlePaypalWebhook,
generatePaypalOrderId,
} from "./services/paypalService";
import { fiatOrders, siteSettings } from "../drizzle/schema";
// Default receiving addresses (used when DB is empty — admin must update via admin panel)
const EVM_DEFAULT = "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3";
const DEFAULT_RECEIVING_ADDRESSES: Record<string, string> = {
trc20_receiving_address: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
bsc_receiving_address: EVM_DEFAULT,
eth_receiving_address: EVM_DEFAULT,
polygon_receiving_address: EVM_DEFAULT,
arbitrum_receiving_address: EVM_DEFAULT,
avalanche_receiving_address: EVM_DEFAULT,
};
// Helper: get a site setting by key
async function getSiteSetting(key: string): Promise<string | null> {
const db = await getDb();
if (!db) return null;
const rows = await db.select().from(siteSettings).where(eq(siteSettings.key, key)).limit(1);
return rows[0]?.value ?? null;
}
// Helper: upsert a site setting
async function upsertSiteSetting(key: string, value: string, label?: string, description?: string) {
const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Database unavailable" });
await db.insert(siteSettings).values({ key, value, label: label ?? null, description: description ?? null })
.onDuplicateKeyUpdate({ set: { value, updatedAt: new Date() } });
}
// Admin password from env (fallback for development)
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
// ─── Bridge Router ───────────────────────────────────────────────────────────
const bridgeRouter = router({
// Record a completed cross-chain USDT transfer and credit XIC (idempotent)
recordOrder: publicProcedure
.input(z.object({
txHash: z.string().min(1).max(128),
walletAddress: z.string().min(1).max(64),
fromChainId: z.number().int(),
fromToken: z.string().max(32),
fromAmount: z.string(), // USDT amount
toChainId: z.number().int(),
toToken: z.string().max(32),
toAmount: z.string(), // XIC amount
xicReceiveAddress: z.string().optional(),
chainType: z.enum(["ERC20", "TRC20"]).default("ERC20"),
receivingAddress: z.string().optional(),
}))
.mutation(async ({ input }) => {
const db = await getDb();
if (!db) return { success: false, message: "DB unavailable" };
// Insert bridge order (pending — creditXic will update to confirmed)
try {
await db.insert(bridgeOrders).values({
txHash: input.txHash,
walletAddress: input.walletAddress.toLowerCase(),
fromChainId: input.fromChainId,
fromToken: input.fromToken,
fromAmount: input.fromAmount,
toChainId: input.toChainId,
toToken: input.toToken,
toAmount: input.toAmount,
xicReceiveAddress: input.xicReceiveAddress ?? null,
status: "pending" as const,
});
} catch (e: any) {
if (e?.code !== "ER_DUP_ENTRY") throw e;
}
// Unified credit service (idempotent via transaction_logs)
const usdtAmount = parseFloat(input.fromAmount);
const xicAmount = parseFloat(input.toAmount);
const result = await creditXic({
txHash: input.txHash,
chainType: input.chainType,
fromAddress: input.walletAddress.toLowerCase(),
toAddress: input.receivingAddress ?? "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
usdtAmount,
xicAmount,
xicReceiveAddress: input.xicReceiveAddress,
remark: `Frontend recordOrder (${input.chainType})`,
});
return { success: result.success, alreadyProcessed: result.alreadyProcessed };
}),
// List orders by wallet address (includes pending intents + confirmed orders)
myOrders: publicProcedure
.input(z.object({
walletAddress: z.string().min(1).max(64),
limit: z.number().min(1).max(50).default(20),
}))
.query(async ({ input }) => {
const db = await getDb();
if (!db) return [];
const addr = input.walletAddress.toLowerCase();
// Query confirmed bridge orders
const confirmedRows = await db
.select()
.from(bridgeOrders)
.where(eq(bridgeOrders.walletAddress, addr))
.orderBy(desc(bridgeOrders.createdAt))
.limit(input.limit);
// Query pending bridge intents (by xicReceiveAddress)
const intentRows = await db
.select()
.from(bridgeIntents)
.where(eq(bridgeIntents.xicReceiveAddress, addr))
.orderBy(desc(bridgeIntents.createdAt))
.limit(input.limit);
// Merge: intents first (pending), then confirmed orders
const result = [
...intentRows.map(r => ({
id: r.id,
type: 'intent' as const,
fromChainId: r.fromChainId,
xicReceiveAddress: r.xicReceiveAddress,
expectedUsdt: r.expectedUsdt ? Number(r.expectedUsdt) : null,
matched: r.matched,
status: r.matched ? 'confirmed' as const : 'pending' as const,
createdAt: r.createdAt,
})),
...confirmedRows.map(r => ({
id: r.id,
type: 'order' as const,
txHash: r.txHash,
walletAddress: r.walletAddress,
fromChainId: r.fromChainId,
fromToken: r.fromToken,
fromAmount: Number(r.fromAmount),
toChainId: r.toChainId,
toToken: r.toToken,
toAmount: Number(r.toAmount),
status: r.status,
createdAt: r.createdAt,
})),
];
return result.sort((a, b) => new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime()).slice(0, input.limit);
}),
// Register a bridge intent — user pre-registers before sending USDT
registerIntent: publicProcedure
.input(z.object({
fromChainId: z.number().int(),
senderAddress: z.string().max(64).or(z.literal("")).transform(v => v === "" ? undefined : v).optional(), // optional — filled when wallet connected
xicReceiveAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid EVM address"),
expectedUsdt: z.number().min(0.01).optional(),
}))
.mutation(async ({ input }) => {
const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
await db.insert(bridgeIntents).values({
fromChainId: input.fromChainId,
senderAddress: input.senderAddress ? input.senderAddress.toLowerCase() : null,
xicReceiveAddress: input.xicReceiveAddress,
expectedUsdt: input.expectedUsdt ? String(input.expectedUsdt) : null,
matched: false,
});
return { success: true, message: "Intent registered. Please send USDT now." };
}),
// List recent bridge orders (public)
recentOrders: publicProcedure
.input(z.object({ limit: z.number().min(1).max(50).default(10) }))
.query(async ({ input }) => {
const db = await getDb();
if (!db) return [];
const rows = await db
.select()
.from(bridgeOrders)
.orderBy(desc(bridgeOrders.createdAt))
.limit(input.limit);
return rows.map(r => ({
id: r.id,
txHash: r.txHash,
walletAddress: r.walletAddress,
fromChainId: r.fromChainId,
fromToken: r.fromToken,
fromAmount: Number(r.fromAmount),
toChainId: r.toChainId,
toToken: r.toToken,
toAmount: Number(r.toAmount),
status: r.status,
createdAt: r.createdAt,
}));
}),
});
// ─── Payment Router (Fiat: Alipay / WeChat Pay / PayPal) ─────────────────────
const paymentRouter = router({
// ── Alipay ──────────────────────────────────────────────────────────────
createAlipayOrder: publicProcedure
.input(z.object({
totalAmount: z.string().regex(/^\d+(\.\d{1,2})?$/, "Invalid amount"),
xicReceiveAddress: z.string().min(10),
isMobile: z.boolean().optional().default(false),
}))
.mutation(async ({ input }) => {
const orderId = generateAlipayOrderId();
const result = await createAlipayOrder({
orderId,
subject: "NAC XIC Token Purchase",
totalAmount: input.totalAmount,
xicReceiveAddress: input.xicReceiveAddress,
isMobile: input.isMobile,
});
if (!result.success) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error });
return { orderId, paymentUrl: result.paymentUrl };
}),
queryAlipayOrder: publicProcedure
.input(z.object({ orderId: z.string() }))
.query(async ({ input }) => {
const db = await getDb();
if (db) {
const rows = await db.select().from(fiatOrders).where(eq(fiatOrders.orderId, input.orderId)).limit(1);
if (rows[0]) return { dbStatus: rows[0].status, xicAmount: rows[0].xicAmount };
}
return { dbStatus: "not_found" };
}),
// ── WeChat Pay ───────────────────────────────────────────────────────────
createWechatOrder: publicProcedure
.input(z.object({
totalFen: z.number().int().min(1),
xicReceiveAddress: z.string().min(10),
payType: z.enum(["NATIVE", "H5", "JSAPI"]).optional().default("NATIVE"),
openId: z.string().optional(),
clientIp: z.string().optional(),
}))
.mutation(async ({ input }) => {
const orderId = generateWechatOrderId();
const result = await createWechatOrder({
orderId,
description: "NAC XIC Token Purchase",
totalFen: input.totalFen,
xicReceiveAddress: input.xicReceiveAddress,
payType: input.payType,
openId: input.openId,
clientIp: input.clientIp,
});
if (!result.success) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error });
return {
orderId,
qrCodeUrl: result.qrCodeUrl,
h5Url: result.h5Url,
jsapiParams: result.jsapiParams,
};
}),
queryWechatOrder: publicProcedure
.input(z.object({ orderId: z.string() }))
.query(async ({ input }) => {
const db = await getDb();
if (db) {
const rows = await db.select().from(fiatOrders).where(eq(fiatOrders.orderId, input.orderId)).limit(1);
if (rows[0]) return { dbStatus: rows[0].status, xicAmount: rows[0].xicAmount };
}
return { dbStatus: "not_found" };
}),
// ── PayPal ───────────────────────────────────────────────────────────────
createPaypalOrder: publicProcedure
.input(z.object({
usdAmount: z.string().regex(/^\d+(\.\d{1,2})?$/, "Invalid amount"),
xicReceiveAddress: z.string().min(10),
}))
.mutation(async ({ input }) => {
const orderId = generatePaypalOrderId();
const result = await createPaypalOrder({
orderId,
usdAmount: input.usdAmount,
xicReceiveAddress: input.xicReceiveAddress,
});
if (!result.success) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error });
return { orderId, paypalOrderId: result.paypalOrderId, approveUrl: result.approveUrl };
}),
capturePaypalOrder: publicProcedure
.input(z.object({
paypalOrderId: z.string(),
internalOrderId: z.string(),
}))
.mutation(async ({ input }) => {
const result = await capturePaypalOrder(input.paypalOrderId, input.internalOrderId);
if (!result.success) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error });
return { success: true, captureId: result.captureId };
}),
queryPaypalOrder: publicProcedure
.input(z.object({ orderId: z.string() }))
.query(async ({ input }) => {
const db = await getDb();
if (db) {
const rows = await db.select().from(fiatOrders).where(eq(fiatOrders.orderId, input.orderId)).limit(1);
if (rows[0]) return { dbStatus: rows[0].status, xicAmount: rows[0].xicAmount };
}
return { dbStatus: "not_found" };
}),
// ── Admin: List Fiat Orders ───────────────────────────────────────────────
listFiatOrders: publicProcedure
.input(z.object({
token: z.string(),
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
channel: z.enum(["all", "alipay", "wechat", "paypal"]).default("all"),
status: z.enum(["all", "pending", "paid", "distributed", "refunded", "failed", "expired"]).default("all"),
}))
.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 offset = (input.page - 1) * input.limit;
let query = db.select().from(fiatOrders);
if (input.channel !== "all") {
query = query.where(eq(fiatOrders.channel, input.channel)) as typeof query;
}
if (input.status !== "all") {
query = query.where(eq(fiatOrders.status, input.status)) as typeof query;
}
const rows = await query
.orderBy(desc(fiatOrders.createdAt))
.limit(input.limit)
.offset(offset);
const countResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(fiatOrders);
return {
orders: rows.map(r => ({
id: r.id,
orderId: r.orderId,
channel: r.channel,
currency: r.currency,
originalAmount: Number(r.originalAmount),
usdtEquivalent: Number(r.usdtEquivalent),
xicAmount: Number(r.xicAmount),
xicReceiveAddress: r.xicReceiveAddress,
status: r.status,
payerEmail: r.payerEmail,
distributedAt: r.distributedAt,
createdAt: r.createdAt,
})),
total: Number(countResult[0]?.count || 0),
page: input.page,
limit: input.limit,
};
}),
});
export const appRouter = router({
system: systemRouter,
bridge: bridgeRouter,
payment: paymentRouter,
auth: router({
me: publicProcedure.query(opts => opts.ctx.user),
logout: publicProcedure.mutation(({ ctx }) => {
@ -338,6 +709,99 @@ export const appRouter = router({
return { success: true };
}),
// List bridge orders (cross-chain bridge)
listBridgeOrders: publicProcedure
.input(z.object({
token: z.string(),
page: z.number().int().min(1).default(1),
limit: z.number().int().min(1).max(100).default(20),
status: z.enum(["all", "pending", "confirmed", "distributed", "failed"]).default("all"),
}))
.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 offset = (input.page - 1) * input.limit;
let query = db.select().from(bridgeOrders);
if (input.status !== "all") {
query = query.where(eq(bridgeOrders.status, input.status)) as typeof query;
}
const rows = await query
.orderBy(desc(bridgeOrders.createdAt))
.limit(input.limit)
.offset(offset);
const countResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(bridgeOrders)
.where(input.status !== "all" ? eq(bridgeOrders.status, input.status) : sql`1=1`);
const intents = await db
.select()
.from(bridgeIntents)
.orderBy(desc(bridgeIntents.createdAt))
.limit(50);
return {
orders: rows.map(r => ({
id: r.id,
txHash: r.txHash,
walletAddress: r.walletAddress,
fromChainId: r.fromChainId,
fromToken: r.fromToken,
fromAmount: Number(r.fromAmount),
toChainId: r.toChainId,
toToken: r.toToken,
toAmount: Number(r.toAmount),
xicReceiveAddress: r.xicReceiveAddress,
status: r.status,
confirmedAt: r.confirmedAt,
distributedAt: r.distributedAt,
distributeTxHash: r.distributeTxHash,
blockNumber: r.blockNumber,
createdAt: r.createdAt,
})),
intents: intents.map(i => ({
id: i.id,
fromChainId: i.fromChainId,
senderAddress: i.senderAddress,
xicReceiveAddress: i.xicReceiveAddress,
expectedUsdt: i.expectedUsdt ? Number(i.expectedUsdt) : null,
matched: i.matched,
matchedOrderId: i.matchedOrderId,
createdAt: i.createdAt,
})),
total: Number(countResult[0]?.count || 0),
page: input.page,
limit: input.limit,
};
}),
// Update bridge order status (manual distribution)
updateBridgeOrder: publicProcedure
.input(z.object({
token: z.string(),
orderId: z.number().int(),
status: z.enum(["pending", "confirmed", "distributed", "failed"]),
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(bridgeOrders)
.set({
status: input.status,
distributedAt: input.status === "distributed" ? new Date() : undefined,
distributeTxHash: input.distributeTxHash || null,
updatedAt: new Date(),
})
.where(eq(bridgeOrders.id, input.orderId));
return { success: true };
}),
// Test Telegram connection
testTelegram: publicProcedure
.input(z.object({
@ -358,7 +822,70 @@ export const appRouter = router({
await setConfig("telegramChatId", input.chatId);
return { success: true };
}),
}),
// ─── Settings (Receiving Addresses) ────────────────────────────────────────────────
// Public: read receiving addresses (frontend read-only display)
// Admin: update receiving addresses (only via admin panel)
settings: router({
// Public: get all receiving addresses (read-only for frontend)
getReceivingAddresses: publicProcedure.query(async () => {
const keys = [
"trc20_receiving_address",
"bsc_receiving_address",
"eth_receiving_address",
"polygon_receiving_address",
"arbitrum_receiving_address",
"avalanche_receiving_address",
];
const result: Record<string, string> = {};
for (const key of keys) {
const val = await getSiteSetting(key);
result[key] = val ?? DEFAULT_RECEIVING_ADDRESSES[key] ?? "";
}
return result;
}),
// Admin: update a receiving address (requires admin token)
updateReceivingAddress: publicProcedure
.input(z.object({
token: z.string(),
key: z.enum(["trc20_receiving_address", "bsc_receiving_address", "eth_receiving_address", "polygon_receiving_address", "arbitrum_receiving_address", "avalanche_receiving_address"]),
value: z.string().min(10).max(128),
}))
.mutation(async ({ input }) => {
if (!input.token.startsWith("bmFjLWFkbWlu")) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
}
const labels: Record<string, string> = {
trc20_receiving_address: "TRC20 USDT 接收地址",
bsc_receiving_address: "BSC BEP-20 USDT 接收地址",
eth_receiving_address: "ETH ERC-20 USDT 接收地址",
polygon_receiving_address: "Polygon USDT 接收地址",
arbitrum_receiving_address: "Arbitrum USDT 接收地址",
avalanche_receiving_address: "Avalanche USDT 接收地址",
};
await upsertSiteSetting(
input.key,
input.value,
labels[input.key],
"仅管理员可修改,前端只读显示"
);
console.log(`[Admin] Receiving address updated: ${input.key} = ${input.value}`);
return { success: true };
}),
// Admin: get all site settings
getAllSettings: publicProcedure
.input(z.object({ token: z.string() }))
.query(async ({ input }) => {
if (!input.token.startsWith("bmFjLWFkbWlu")) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
}
const db = await getDb();
if (!db) return [];
return await db.select().from(siteSettings);
}),
}),
});
export type AppRouter = typeof appRouter;

View File

@ -0,0 +1,344 @@
/**
* Alipay Payment Service
*
* Supports: PC Web Payment (alipay.trade.page.pay)
* H5 Mobile Payment (alipay.trade.wap.pay)
* Order Query (alipay.trade.query)
* Refund (alipay.trade.refund)
*
* Configuration (set via environment variables DO NOT hardcode):
* ALIPAY_APP_ID Alipay Open Platform App ID
* ALIPAY_PRIVATE_KEY RSA2 private key (PKCS8, no header/footer, single line)
* ALIPAY_PUBLIC_KEY Alipay public key (for signature verification)
* ALIPAY_NOTIFY_URL Async callback URL (must be publicly accessible)
* ALIPAY_RETURN_URL Sync redirect URL after payment
* ALIPAY_SANDBOX "true" to use sandbox environment
*
* Integration point:
* After verifying the async callback, call tokenDistributionService.creditXic()
* to distribute XIC tokens to the buyer.
*
* Docs: https://opendocs.alipay.com/open/270/105899
*/
import crypto from "crypto";
import { getDb } from "../db";
import { fiatOrders } from "../../drizzle/schema";
import { eq } from "drizzle-orm";
import { creditXic, calcXicAmount } from "../tokenDistributionService";
// ─── Configuration ────────────────────────────────────────────────────────────
// TODO: Replace placeholder values with real credentials from Alipay Open Platform
// https://open.alipay.com/develop/manage
const ALIPAY_CONFIG = {
appId: process.env.ALIPAY_APP_ID || "PLACEHOLDER_ALIPAY_APP_ID",
privateKey: process.env.ALIPAY_PRIVATE_KEY || "PLACEHOLDER_RSA2_PRIVATE_KEY",
alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY || "PLACEHOLDER_ALIPAY_PUBLIC_KEY",
notifyUrl: process.env.ALIPAY_NOTIFY_URL || "https://pre-sale.newassetchain.io/api/payment/alipay/notify",
returnUrl: process.env.ALIPAY_RETURN_URL || "https://pre-sale.newassetchain.io/payment/success",
// Sandbox: https://openapi-sandbox.dl.alipaydev.com/gateway.do
// Production: https://openapi.alipay.com/gateway.do
gatewayUrl: process.env.ALIPAY_SANDBOX === "true"
? "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
: "https://openapi.alipay.com/gateway.do",
sandbox: process.env.ALIPAY_SANDBOX === "true",
};
// ─── Types ────────────────────────────────────────────────────────────────────
export interface AlipayOrderParams {
orderId: string; // our internal order ID
subject: string; // order subject (e.g. "XIC Token Purchase")
totalAmount: string; // CNY amount, e.g. "100.00"
xicReceiveAddress: string; // BSC address to receive XIC
userId?: string;
isMobile?: boolean; // true → H5 payment, false → PC payment
}
export interface AlipayOrderResult {
success: boolean;
paymentUrl?: string; // redirect URL for PC/H5 payment
orderId?: string;
error?: string;
}
export interface AlipayQueryResult {
success: boolean;
tradeStatus?: "WAIT_BUYER_PAY" | "TRADE_CLOSED" | "TRADE_SUCCESS" | "TRADE_FINISHED";
totalAmount?: string;
buyerPayAmount?: string;
tradeNo?: string;
error?: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Generate unique order ID with ALIPAY prefix */
export function generateAlipayOrderId(): string {
const ts = Date.now().toString();
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
return `ALIPAY-${ts}-${rand}`;
}
/** CNY to USD conversion (approximate, replace with real-time rate in production) */
function cnyToUsd(cny: number): number {
const CNY_USD_RATE = 0.138; // TODO: fetch real-time rate from exchange API
return parseFloat((cny * CNY_USD_RATE).toFixed(6));
}
/**
* Build Alipay RSA2 signature.
* Signs the sorted parameter string with RSA2 (SHA256withRSA).
*/
function buildSign(params: Record<string, string>): string {
if (ALIPAY_CONFIG.privateKey === "PLACEHOLDER_RSA2_PRIVATE_KEY") {
console.warn("[Alipay] Using placeholder private key — signature will be invalid");
return "PLACEHOLDER_SIGNATURE";
}
const sortedKeys = Object.keys(params).sort();
const signStr = sortedKeys
.filter(k => k !== "sign" && params[k] !== "" && params[k] !== undefined)
.map(k => `${k}=${params[k]}`)
.join("&");
const privateKey = `-----BEGIN RSA PRIVATE KEY-----\n${ALIPAY_CONFIG.privateKey}\n-----END RSA PRIVATE KEY-----`;
const sign = crypto.createSign("RSA-SHA256");
sign.update(signStr, "utf8");
return sign.sign(privateKey, "base64");
}
/**
* Verify Alipay async callback signature.
* Returns true if signature is valid.
*/
export function verifyAlipaySign(params: Record<string, string>): boolean {
if (ALIPAY_CONFIG.alipayPublicKey === "PLACEHOLDER_ALIPAY_PUBLIC_KEY") {
console.warn("[Alipay] Using placeholder public key — skipping signature verification (SANDBOX MODE)");
return true; // Allow in sandbox/test mode
}
const sign = params.sign;
if (!sign) return false;
const sortedKeys = Object.keys(params).sort();
const signStr = sortedKeys
.filter(k => k !== "sign" && k !== "sign_type" && params[k] !== "")
.map(k => `${k}=${params[k]}`)
.join("&");
const publicKey = `-----BEGIN PUBLIC KEY-----\n${ALIPAY_CONFIG.alipayPublicKey}\n-----END PUBLIC KEY-----`;
const verify = crypto.createVerify("RSA-SHA256");
verify.update(signStr, "utf8");
return verify.verify(publicKey, sign, "base64");
}
// ─── Core Functions ───────────────────────────────────────────────────────────
/**
* Create an Alipay payment order.
* Returns a redirect URL for PC (page pay) or H5 (wap pay).
*/
export async function createAlipayOrder(params: AlipayOrderParams): Promise<AlipayOrderResult> {
const { orderId, subject, totalAmount, xicReceiveAddress, userId, isMobile = false } = params;
const cnyAmount = parseFloat(totalAmount);
const usdEquivalent = cnyToUsd(cnyAmount);
const xicAmount = calcXicAmount(usdEquivalent);
// Build Alipay request parameters
const method = isMobile ? "alipay.trade.wap.pay" : "alipay.trade.page.pay";
const bizContent = JSON.stringify({
out_trade_no: orderId,
product_code: isMobile ? "QUICK_WAP_WAY" : "FAST_INSTANT_TRADE_PAY",
total_amount: totalAmount,
subject,
body: `XIC Token Presale — ${xicAmount} XIC`,
timeout_express: "30m",
// passback_params: encodeURIComponent(JSON.stringify({ xicReceiveAddress })),
});
const commonParams: Record<string, string> = {
app_id: ALIPAY_CONFIG.appId,
method,
format: "JSON",
charset: "utf-8",
sign_type: "RSA2",
timestamp: new Date().toISOString().replace("T", " ").substring(0, 19),
version: "1.0",
notify_url: ALIPAY_CONFIG.notifyUrl,
return_url: ALIPAY_CONFIG.returnUrl,
biz_content: bizContent,
};
const sign = buildSign(commonParams);
const allParams = { ...commonParams, sign };
// Build redirect URL (GET form submit)
const queryString = Object.entries(allParams)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
const paymentUrl = `${ALIPAY_CONFIG.gatewayUrl}?${queryString}`;
try {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
// Save order to database
await db.insert(fiatOrders).values({
orderId,
channel: "alipay",
userId: userId || null,
xicReceiveAddress,
usdtEquivalent: usdEquivalent.toString(),
currency: "CNY",
originalAmount: totalAmount,
xicAmount: xicAmount.toString(),
status: "pending",
paymentUrl,
expiredAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes
});
console.log(`[Alipay] Order created: ${orderId} | CNY ${totalAmount}${xicAmount} XIC → ${xicReceiveAddress}`);
return { success: true, paymentUrl, orderId };
} catch (err) {
console.error("[Alipay] Failed to create order:", err);
return { success: false, error: String(err) };
}
}
/**
* Query Alipay order status via API.
* Used for polling when async callback is not received.
*/
export async function queryAlipayOrder(orderId: string): Promise<AlipayQueryResult> {
const bizContent = JSON.stringify({ out_trade_no: orderId });
const commonParams: Record<string, string> = {
app_id: ALIPAY_CONFIG.appId,
method: "alipay.trade.query",
format: "JSON",
charset: "utf-8",
sign_type: "RSA2",
timestamp: new Date().toISOString().replace("T", " ").substring(0, 19),
version: "1.0",
biz_content: bizContent,
};
const sign = buildSign(commonParams);
try {
const response = await fetch(ALIPAY_CONFIG.gatewayUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: Object.entries({ ...commonParams, sign })
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&"),
});
const data = await response.json() as any;
const result = data["alipay_trade_query_response"];
if (result?.code === "10000") {
return {
success: true,
tradeStatus: result.trade_status,
totalAmount: result.total_amount,
buyerPayAmount: result.buyer_pay_amount,
tradeNo: result.trade_no,
};
}
return { success: false, error: result?.sub_msg || "Query failed" };
} catch (err) {
return { success: false, error: String(err) };
}
}
/**
* Process Alipay async callback (notify).
* Called by the backend route when Alipay POSTs to ALIPAY_NOTIFY_URL.
*
* Flow:
* 1. Verify signature
* 2. Check trade_status === "TRADE_SUCCESS"
* 3. Update fiat_orders status to "paid"
* 4. Call creditXic() to distribute XIC tokens
* 5. Return "success" to Alipay (prevents retry)
*/
export async function handleAlipayCallback(params: Record<string, string>): Promise<{ ok: boolean; message: string }> {
// Step 1: Verify signature
if (!verifyAlipaySign(params)) {
console.error("[Alipay] Callback signature verification failed");
return { ok: false, message: "signature_invalid" };
}
const { trade_status, out_trade_no, trade_no, total_amount, buyer_id } = params;
// Step 2: Only process successful payments
if (trade_status !== "TRADE_SUCCESS" && trade_status !== "TRADE_FINISHED") {
console.log(`[Alipay] Callback ignored — trade_status=${trade_status} for order ${out_trade_no}`);
return { ok: true, message: "ignored" };
}
try {
const db = await getDb();
if (!db) return { ok: false, message: "db_unavailable" };
// Step 3: Find the order
const orders = await db.select().from(fiatOrders)
.where(eq(fiatOrders.orderId, out_trade_no))
.limit(1);
const order = orders[0];
if (!order) {
console.error(`[Alipay] Order not found: ${out_trade_no}`);
return { ok: false, message: "order_not_found" };
}
// Idempotency: skip if already processed
if (order.status === "distributed" || order.status === "paid") {
console.log(`[Alipay] Order ${out_trade_no} already processed, skipping`);
return { ok: true, message: "already_processed" };
}
// Step 4: Update order status to "paid"
await db.update(fiatOrders)
.set({
status: "paid",
gatewayOrderId: trade_no,
callbackPayload: JSON.stringify(params),
updatedAt: new Date(),
})
.where(eq(fiatOrders.orderId, out_trade_no));
// Step 5: Distribute XIC tokens via unified service
const creditResult = await creditXic({
txHash: `ALIPAY-${trade_no}`,
chainType: "ALIPAY",
fromAddress: buyer_id || "alipay_buyer",
toAddress: "alipay_merchant",
usdtAmount: parseFloat(order.usdtEquivalent),
xicAmount: parseFloat(order.xicAmount),
xicReceiveAddress: order.xicReceiveAddress || undefined,
remark: `Alipay order ${out_trade_no}, CNY ${total_amount}`,
});
if (creditResult.success) {
await db.update(fiatOrders)
.set({ status: "distributed", distributedAt: new Date() })
.where(eq(fiatOrders.orderId, out_trade_no));
console.log(`[Alipay] ✅ XIC distributed for order ${out_trade_no}`);
} else {
console.error(`[Alipay] ❌ XIC distribution failed for order ${out_trade_no}:`, creditResult.error);
}
return { ok: true, message: "success" };
} catch (err) {
console.error("[Alipay] Callback processing error:", err);
return { ok: false, message: String(err) };
}
}
/**
* Request Alipay refund.
* TODO: Implement when refund workflow is defined.
*/
export async function refundAlipayOrder(orderId: string, refundAmount: string, reason: string): Promise<{ success: boolean; error?: string }> {
// TODO: Implement alipay.trade.refund API call
// Reference: https://opendocs.alipay.com/open/028sm9
console.warn(`[Alipay] Refund requested for ${orderId} — not yet implemented`);
return { success: false, error: "Refund not yet implemented" };
}

View File

@ -0,0 +1,397 @@
/**
* PayPal Payment Service
*
* Uses PayPal Orders API v2 (REST).
* Supports: Create Order Capture Order Webhook verification
*
* Configuration (set via environment variables DO NOT hardcode):
* PAYPAL_CLIENT_ID PayPal REST API client ID
* PAYPAL_CLIENT_SECRET PayPal REST API client secret
* PAYPAL_WEBHOOK_ID Webhook ID from PayPal Developer Dashboard
* PAYPAL_SANDBOX "true" to use sandbox environment
*
* Integration point:
* After capturing the order (or receiving PAYMENT.CAPTURE.COMPLETED webhook),
* call tokenDistributionService.creditXic() to distribute XIC tokens.
*
* Docs: https://developer.paypal.com/docs/api/orders/v2/
*/
import { getDb } from "../db";
import { fiatOrders } from "../../drizzle/schema";
import { eq } from "drizzle-orm";
import { creditXic, calcXicAmount } from "../tokenDistributionService";
// ─── Configuration ────────────────────────────────────────────────────────────
// TODO: Replace placeholder values with real credentials from PayPal Developer Dashboard
// https://developer.paypal.com/dashboard/applications
const PAYPAL_CONFIG = {
clientId: process.env.PAYPAL_CLIENT_ID || "PLACEHOLDER_PAYPAL_CLIENT_ID",
clientSecret: process.env.PAYPAL_CLIENT_SECRET || "PLACEHOLDER_PAYPAL_CLIENT_SECRET",
webhookId: process.env.PAYPAL_WEBHOOK_ID || "PLACEHOLDER_PAYPAL_WEBHOOK_ID",
// Sandbox: https://api-m.sandbox.paypal.com
// Production: https://api-m.paypal.com
baseUrl: process.env.PAYPAL_SANDBOX === "true"
? "https://api-m.sandbox.paypal.com"
: "https://api-m.paypal.com",
sandbox: process.env.PAYPAL_SANDBOX === "true",
returnUrl: process.env.PAYPAL_RETURN_URL || "https://pre-sale.newassetchain.io/payment/success",
cancelUrl: process.env.PAYPAL_CANCEL_URL || "https://pre-sale.newassetchain.io/payment/cancel",
};
// ─── Types ────────────────────────────────────────────────────────────────────
export interface PaypalOrderParams {
orderId: string;
usdAmount: string; // USD amount, e.g. "100.00"
xicReceiveAddress: string;
userId?: string;
description?: string;
}
export interface PaypalOrderResult {
success: boolean;
paypalOrderId?: string; // PayPal's order ID
approveUrl?: string; // URL to redirect user for approval
orderId?: string; // our internal order ID
error?: string;
}
// ─── OAuth Token Cache ────────────────────────────────────────────────────────
let _accessToken: string | null = null;
let _tokenExpiry = 0;
/**
* Get PayPal OAuth 2.0 access token.
* Tokens are cached until expiry.
*/
async function getAccessToken(): Promise<string> {
if (_accessToken && Date.now() < _tokenExpiry - 60_000) {
return _accessToken;
}
if (PAYPAL_CONFIG.clientId === "PLACEHOLDER_PAYPAL_CLIENT_ID") {
console.warn("[PayPal] Using placeholder credentials — API calls will fail");
return "PLACEHOLDER_ACCESS_TOKEN";
}
const credentials = Buffer.from(`${PAYPAL_CONFIG.clientId}:${PAYPAL_CONFIG.clientSecret}`).toString("base64");
const response = await fetch(`${PAYPAL_CONFIG.baseUrl}/v1/oauth2/token`, {
method: "POST",
headers: {
"Authorization": `Basic ${credentials}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
});
if (!response.ok) {
throw new Error(`[PayPal] Failed to get access token: ${response.status}`);
}
const data = await response.json() as any;
_accessToken = data.access_token;
_tokenExpiry = Date.now() + (data.expires_in * 1000);
return _accessToken!;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Generate unique order ID with PAYPAL prefix */
export function generatePaypalOrderId(): string {
const ts = Date.now().toString();
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
return `PAYPAL-${ts}-${rand}`;
}
// ─── Core Functions ───────────────────────────────────────────────────────────
/**
* Create a PayPal order.
* Returns an approval URL to redirect the user to PayPal for payment.
*
* Flow:
* 1. Create order via PayPal Orders API v2
* 2. Save order to fiat_orders table
* 3. Return approveUrl for frontend redirect
* 4. After user approves, frontend calls capturePaypalOrder()
*/
export async function createPaypalOrder(params: PaypalOrderParams): Promise<PaypalOrderResult> {
const { orderId, usdAmount, xicReceiveAddress, userId, description } = params;
const usdValue = parseFloat(usdAmount);
const xicAmount = calcXicAmount(usdValue);
try {
const accessToken = await getAccessToken();
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
// Build PayPal order request
const orderPayload = {
intent: "CAPTURE",
purchase_units: [
{
reference_id: orderId,
description: description || `NAC XIC Token Purchase — ${xicAmount} XIC`,
custom_id: xicReceiveAddress, // store XIC receive address in custom_id
amount: {
currency_code: "USD",
value: usdAmount,
breakdown: {
item_total: { currency_code: "USD", value: usdAmount },
},
},
items: [
{
name: "XIC Token",
description: `${xicAmount} XIC tokens at $0.02/XIC`,
quantity: "1",
unit_amount: { currency_code: "USD", value: usdAmount },
category: "DIGITAL_GOODS",
},
],
},
],
payment_source: {
paypal: {
experience_context: {
payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED",
brand_name: "NAC XIC Token Presale",
locale: "en-US",
landing_page: "LOGIN",
shipping_preference: "NO_SHIPPING",
user_action: "PAY_NOW",
return_url: `${PAYPAL_CONFIG.returnUrl}?orderId=${orderId}`,
cancel_url: `${PAYPAL_CONFIG.cancelUrl}?orderId=${orderId}`,
},
},
},
};
const response = await fetch(`${PAYPAL_CONFIG.baseUrl}/v2/checkout/orders`, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"PayPal-Request-Id": orderId, // idempotency key
},
body: JSON.stringify(orderPayload),
});
const data = await response.json() as any;
if (!response.ok) {
console.error("[PayPal] Create order error:", data);
return { success: false, error: data.message || "PayPal API error" };
}
// Find approve URL
const approveLink = data.links?.find((l: any) => l.rel === "payer-action" || l.rel === "approve");
const approveUrl = approveLink?.href;
// Save to database
await db.insert(fiatOrders).values({
orderId,
gatewayOrderId: data.id,
channel: "paypal",
userId: userId || null,
xicReceiveAddress,
usdtEquivalent: usdAmount,
currency: "USD",
originalAmount: usdAmount,
xicAmount: xicAmount.toString(),
status: "pending",
paymentUrl: approveUrl || null,
expiredAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
});
console.log(`[PayPal] Order created: ${orderId} (PayPal: ${data.id}) | USD ${usdAmount}${xicAmount} XIC → ${xicReceiveAddress}`);
return { success: true, paypalOrderId: data.id, approveUrl, orderId };
} catch (err) {
console.error("[PayPal] Failed to create order:", err);
return { success: false, error: String(err) };
}
}
/**
* Capture a PayPal order after user approval.
* Called by frontend after user returns from PayPal approval page.
* This is the final step that actually charges the user.
*/
export async function capturePaypalOrder(paypalOrderId: string, internalOrderId: string): Promise<{ success: boolean; captureId?: string; error?: string }> {
try {
const accessToken = await getAccessToken();
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
const response = await fetch(`${PAYPAL_CONFIG.baseUrl}/v2/checkout/orders/${paypalOrderId}/capture`, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"PayPal-Request-Id": `capture-${internalOrderId}`,
},
});
const data = await response.json() as any;
if (!response.ok) {
console.error("[PayPal] Capture error:", data);
return { success: false, error: data.message || "Capture failed" };
}
if (data.status !== "COMPLETED") {
return { success: false, error: `Unexpected status: ${data.status}` };
}
const capture = data.purchase_units?.[0]?.payments?.captures?.[0];
const captureId = capture?.id;
const payerEmail = data.payer?.email_address;
const xicReceiveAddress = data.purchase_units?.[0]?.custom_id;
// Find order in database
const orders = await db.select().from(fiatOrders)
.where(eq(fiatOrders.orderId, internalOrderId))
.limit(1);
const order = orders[0];
if (!order) return { success: false, error: "Order not found" };
if (order.status === "distributed" || order.status === "paid") {
return { success: true, captureId };
}
// Update to paid
await db.update(fiatOrders)
.set({
status: "paid",
payerEmail: payerEmail || null,
callbackPayload: JSON.stringify(data),
updatedAt: new Date(),
})
.where(eq(fiatOrders.orderId, internalOrderId));
// Distribute XIC
const creditResult = await creditXic({
txHash: `PAYPAL-${captureId}`,
chainType: "PAYPAL",
fromAddress: payerEmail || "paypal_payer",
toAddress: "paypal_merchant",
usdtAmount: parseFloat(order.usdtEquivalent),
xicAmount: parseFloat(order.xicAmount),
xicReceiveAddress: order.xicReceiveAddress || xicReceiveAddress || undefined,
remark: `PayPal order ${internalOrderId}, USD ${order.originalAmount}`,
});
if (creditResult.success) {
await db.update(fiatOrders)
.set({ status: "distributed", distributedAt: new Date() })
.where(eq(fiatOrders.orderId, internalOrderId));
console.log(`[PayPal] ✅ XIC distributed for order ${internalOrderId}`);
}
return { success: true, captureId };
} catch (err) {
console.error("[PayPal] Capture error:", err);
return { success: false, error: String(err) };
}
}
/**
* Handle PayPal webhook events.
* PayPal sends PAYMENT.CAPTURE.COMPLETED when payment is confirmed.
* This is a backup to capturePaypalOrder() for cases where the user
* closes the browser before returning to our site.
*
* Docs: https://developer.paypal.com/api/rest/webhooks/
*/
export async function handlePaypalWebhook(
headers: Record<string, string>,
body: any
): Promise<{ ok: boolean; message: string }> {
// TODO: Implement PayPal webhook signature verification
// Reference: https://developer.paypal.com/api/rest/webhooks/rest/#link-eventtypelistforallapps
// For now, process the event directly (add signature verification before production)
const { event_type, resource } = body;
if (event_type !== "PAYMENT.CAPTURE.COMPLETED") {
return { ok: true, message: "ignored" };
}
const captureId = resource?.id;
const customId = resource?.custom_id; // our XIC receive address
const invoiceId = resource?.invoice_id; // our internal order ID (if set)
const payerEmail = resource?.payer?.email_address;
const amount = resource?.amount?.value;
if (!captureId) return { ok: false, message: "missing_capture_id" };
try {
const db = await getDb();
if (!db) return { ok: false, message: "db_unavailable" };
// Find order by gatewayOrderId (PayPal order ID)
// Note: resource.supplementary_data?.related_ids?.order_id contains the PayPal order ID
const paypalOrderId = resource?.supplementary_data?.related_ids?.order_id;
if (!paypalOrderId) return { ok: true, message: "no_order_id" };
const orders = await db.select().from(fiatOrders)
.where(eq(fiatOrders.gatewayOrderId, paypalOrderId))
.limit(1);
const order = orders[0];
if (!order) return { ok: true, message: "order_not_found" };
if (order.status === "distributed" || order.status === "paid") {
return { ok: true, message: "already_processed" };
}
await db.update(fiatOrders)
.set({
status: "paid",
payerEmail: payerEmail || null,
callbackPayload: JSON.stringify(body),
updatedAt: new Date(),
})
.where(eq(fiatOrders.id, order.id));
const creditResult = await creditXic({
txHash: `PAYPAL-${captureId}`,
chainType: "PAYPAL",
fromAddress: payerEmail || "paypal_payer",
toAddress: "paypal_merchant",
usdtAmount: parseFloat(order.usdtEquivalent),
xicAmount: parseFloat(order.xicAmount),
xicReceiveAddress: order.xicReceiveAddress || customId || undefined,
remark: `PayPal webhook capture ${captureId}, USD ${amount}`,
});
if (creditResult.success) {
await db.update(fiatOrders)
.set({ status: "distributed", distributedAt: new Date() })
.where(eq(fiatOrders.id, order.id));
console.log(`[PayPal] ✅ XIC distributed via webhook for order ${order.orderId}`);
}
return { ok: true, message: "success" };
} catch (err) {
console.error("[PayPal] Webhook processing error:", err);
return { ok: false, message: String(err) };
}
}
/**
* Query PayPal order status.
*/
export async function queryPaypalOrder(paypalOrderId: string): Promise<{ success: boolean; status?: string; error?: string }> {
try {
const accessToken = await getAccessToken();
const response = await fetch(`${PAYPAL_CONFIG.baseUrl}/v2/checkout/orders/${paypalOrderId}`, {
headers: { "Authorization": `Bearer ${accessToken}` },
});
const data = await response.json() as any;
if (response.ok) return { success: true, status: data.status };
return { success: false, error: data.message };
} catch (err) {
return { success: false, error: String(err) };
}
}

View File

@ -0,0 +1,388 @@
/**
* WeChat Pay Service
*
* Supports: Native Pay (NATIVE) QR code, for PC/Web
* H5 Pay (H5) for mobile browsers outside WeChat
* JSAPI Pay for WeChat built-in browser (requires openid)
*
* Configuration (set via environment variables DO NOT hardcode):
* WECHAT_APP_ID WeChat Official Account / Mini Program App ID
* WECHAT_MCH_ID WeChat Pay Merchant ID
* WECHAT_API_V3_KEY API v3 key (32 bytes, set in WeChat Pay console)
* WECHAT_CERT_SERIAL_NO API certificate serial number
* WECHAT_PRIVATE_KEY API certificate private key (PEM, single line)
* WECHAT_NOTIFY_URL Async callback URL (must be publicly accessible)
* WECHAT_SANDBOX "true" to use sandbox environment
*
* Integration point:
* After verifying the async callback, call tokenDistributionService.creditXic()
* to distribute XIC tokens to the buyer.
*
* Docs: https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_0.shtml
*/
import crypto from "crypto";
import { getDb } from "../db";
import { fiatOrders } from "../../drizzle/schema";
import { eq } from "drizzle-orm";
import { creditXic, calcXicAmount } from "../tokenDistributionService";
// ─── Configuration ────────────────────────────────────────────────────────────
// TODO: Replace placeholder values with real credentials from WeChat Pay console
// https://pay.weixin.qq.com/index.php/core/account/info
const WECHAT_CONFIG = {
appId: process.env.WECHAT_APP_ID || "PLACEHOLDER_WECHAT_APP_ID",
mchId: process.env.WECHAT_MCH_ID || "PLACEHOLDER_WECHAT_MCH_ID",
apiV3Key: process.env.WECHAT_API_V3_KEY || "PLACEHOLDER_WECHAT_API_V3_KEY_32BYTES",
certSerialNo: process.env.WECHAT_CERT_SERIAL_NO || "PLACEHOLDER_CERT_SERIAL_NO",
privateKey: process.env.WECHAT_PRIVATE_KEY || "PLACEHOLDER_WECHAT_PRIVATE_KEY",
notifyUrl: process.env.WECHAT_NOTIFY_URL || "https://pre-sale.newassetchain.io/api/payment/wechat/notify",
// WeChat Pay API v3 base URL (same for sandbox and production, use different credentials)
baseUrl: "https://api.mch.weixin.qq.com",
sandbox: process.env.WECHAT_SANDBOX === "true",
};
// ─── Types ────────────────────────────────────────────────────────────────────
export interface WechatOrderParams {
orderId: string;
description: string;
totalFen: number; // amount in CNY fen (e.g. 10000 = 100.00 CNY)
xicReceiveAddress: string;
userId?: string;
payType?: "NATIVE" | "H5" | "JSAPI";
openId?: string; // required for JSAPI
clientIp?: string; // required for H5
}
export interface WechatOrderResult {
success: boolean;
qrCodeUrl?: string; // for NATIVE pay
h5Url?: string; // for H5 pay
prepayId?: string; // for JSAPI pay
jsapiParams?: WechatJsapiParams;
orderId?: string;
error?: string;
}
export interface WechatJsapiParams {
appId: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: "RSA";
paySign: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Generate unique order ID with WECHAT prefix */
export function generateWechatOrderId(): string {
const ts = Date.now().toString();
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
return `WECHAT-${ts}-${rand}`;
}
/** CNY fen to USD conversion */
function fenToUsd(fen: number): number {
const CNY_USD_RATE = 0.138; // TODO: fetch real-time rate
return parseFloat(((fen / 100) * CNY_USD_RATE).toFixed(6));
}
/**
* Build WeChat Pay API v3 authorization header.
* Uses RSA-SHA256 signature scheme.
* Docs: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
*/
function buildAuthHeader(method: string, url: string, body: string): string {
if (WECHAT_CONFIG.privateKey === "PLACEHOLDER_WECHAT_PRIVATE_KEY") {
console.warn("[WeChat] Using placeholder private key — auth header will be invalid");
return `WECHATPAY2-SHA256-RSA2048 mchid="${WECHAT_CONFIG.mchId}",nonce_str="placeholder",timestamp="${Math.floor(Date.now() / 1000)}",serial_no="${WECHAT_CONFIG.certSerialNo}",signature="PLACEHOLDER_SIGNATURE"`;
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = crypto.randomBytes(16).toString("hex");
const urlObj = new URL(url);
const canonicalUrl = urlObj.pathname + (urlObj.search || "");
const message = `${method}\n${canonicalUrl}\n${timestamp}\n${nonceStr}\n${body}\n`;
const privateKey = `-----BEGIN PRIVATE KEY-----\n${WECHAT_CONFIG.privateKey}\n-----END PRIVATE KEY-----`;
const sign = crypto.createSign("RSA-SHA256");
sign.update(message);
const signature = sign.sign(privateKey, "base64");
return `WECHATPAY2-SHA256-RSA2048 mchid="${WECHAT_CONFIG.mchId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${WECHAT_CONFIG.certSerialNo}",signature="${signature}"`;
}
/**
* Verify WeChat Pay callback AES-GCM decryption.
* WeChat encrypts the resource field with AES-256-GCM using apiV3Key.
*/
export function decryptWechatCallback(
associatedData: string,
nonce: string,
ciphertext: string
): string | null {
if (WECHAT_CONFIG.apiV3Key === "PLACEHOLDER_WECHAT_API_V3_KEY_32BYTES") {
console.warn("[WeChat] Using placeholder API v3 key — decryption will fail");
return null;
}
try {
const key = Buffer.from(WECHAT_CONFIG.apiV3Key, "utf8");
const ciphertextBuf = Buffer.from(ciphertext, "base64");
const authTag = ciphertextBuf.slice(ciphertextBuf.length - 16);
const data = ciphertextBuf.slice(0, ciphertextBuf.length - 16);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(nonce, "utf8"));
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(associatedData, "utf8"));
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
return decrypted.toString("utf8");
} catch (err) {
console.error("[WeChat] Decryption failed:", err);
return null;
}
}
// ─── Core Functions ───────────────────────────────────────────────────────────
/**
* Create a WeChat Pay order.
* Supports NATIVE (QR), H5, and JSAPI payment types.
*/
export async function createWechatOrder(params: WechatOrderParams): Promise<WechatOrderResult> {
const { orderId, description, totalFen, xicReceiveAddress, userId, payType = "NATIVE", openId, clientIp } = params;
const usdEquivalent = fenToUsd(totalFen);
const xicAmount = calcXicAmount(usdEquivalent);
// Determine API endpoint based on pay type
const endpointMap: Record<string, string> = {
NATIVE: "/v3/pay/transactions/native",
H5: "/v3/pay/transactions/h5",
JSAPI: "/v3/pay/transactions/jsapi",
};
const endpoint = endpointMap[payType];
const url = `${WECHAT_CONFIG.baseUrl}${endpoint}`;
// Build request body
const requestBody: Record<string, any> = {
appid: WECHAT_CONFIG.appId,
mchid: WECHAT_CONFIG.mchId,
description,
out_trade_no: orderId,
notify_url: WECHAT_CONFIG.notifyUrl,
amount: { total: totalFen, currency: "CNY" },
attach: xicReceiveAddress, // pass XIC receive address as attach field
};
if (payType === "JSAPI" && openId) {
requestBody.payer = { openid: openId };
}
if (payType === "H5" && clientIp) {
requestBody.scene_info = {
payer_client_ip: clientIp,
h5_info: { type: "Wap", wap_url: "https://pre-sale.newassetchain.io", wap_name: "NAC XIC Presale" },
};
}
const body = JSON.stringify(requestBody);
const authHeader = buildAuthHeader("POST", url, body);
try {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
// Save order to database first
await db.insert(fiatOrders).values({
orderId,
channel: "wechat",
userId: userId || null,
xicReceiveAddress,
usdtEquivalent: usdEquivalent.toString(),
currency: "CNY",
originalAmount: (totalFen / 100).toFixed(2),
xicAmount: xicAmount.toString(),
status: "pending",
expiredAt: new Date(Date.now() + 30 * 60 * 1000),
});
// Call WeChat Pay API
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": authHeader,
"Accept": "application/json",
},
body,
});
const data = await response.json() as any;
if (!response.ok) {
console.error("[WeChat] API error:", data);
return { success: false, error: data.message || "WeChat API error" };
}
let result: WechatOrderResult = { success: true, orderId };
if (payType === "NATIVE" && data.code_url) {
result.qrCodeUrl = data.code_url;
await db.update(fiatOrders)
.set({ qrCodeUrl: data.code_url })
.where(eq(fiatOrders.orderId, orderId));
} else if (payType === "H5" && data.h5_url) {
result.h5Url = data.h5_url;
await db.update(fiatOrders)
.set({ paymentUrl: data.h5_url })
.where(eq(fiatOrders.orderId, orderId));
} else if (payType === "JSAPI" && data.prepay_id) {
result.prepayId = data.prepay_id;
// Build JSAPI parameters for frontend
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = crypto.randomBytes(16).toString("hex");
const packageStr = `prepay_id=${data.prepay_id}`;
const signMessage = `${WECHAT_CONFIG.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`;
const privateKey = `-----BEGIN PRIVATE KEY-----\n${WECHAT_CONFIG.privateKey}\n-----END PRIVATE KEY-----`;
const sign = crypto.createSign("RSA-SHA256");
sign.update(signMessage);
const paySign = sign.sign(privateKey, "base64");
result.jsapiParams = {
appId: WECHAT_CONFIG.appId,
timeStamp: timestamp,
nonceStr,
package: packageStr,
signType: "RSA",
paySign,
};
}
console.log(`[WeChat] Order created: ${orderId} | CNY ${(totalFen / 100).toFixed(2)}${xicAmount} XIC → ${xicReceiveAddress}`);
return result;
} catch (err) {
console.error("[WeChat] Failed to create order:", err);
return { success: false, error: String(err) };
}
}
/**
* Process WeChat Pay async callback (notify).
* WeChat sends a POST request to WECHAT_NOTIFY_URL when payment is completed.
*
* Flow:
* 1. Decrypt the resource field using AES-256-GCM
* 2. Verify trade_state === "SUCCESS"
* 3. Update fiat_orders status to "paid"
* 4. Call creditXic() to distribute XIC tokens
* 5. Return { code: "SUCCESS" } to WeChat
*/
export async function handleWechatCallback(body: any): Promise<{ ok: boolean; code: string; message: string }> {
const { event_type, resource } = body;
if (event_type !== "TRANSACTION.SUCCESS") {
return { ok: true, code: "SUCCESS", message: "ignored" };
}
// Step 1: Decrypt resource
const decrypted = decryptWechatCallback(
resource.associated_data,
resource.nonce,
resource.ciphertext
);
if (!decrypted) {
// In sandbox/test mode with placeholder keys, parse raw body
console.warn("[WeChat] Decryption failed — attempting to process as plaintext (test mode)");
return { ok: false, code: "FAIL", message: "decryption_failed" };
}
let transaction: any;
try {
transaction = JSON.parse(decrypted);
} catch {
return { ok: false, code: "FAIL", message: "invalid_json" };
}
const { trade_state, out_trade_no, transaction_id, amount, payer, attach } = transaction;
if (trade_state !== "SUCCESS") {
return { ok: true, code: "SUCCESS", message: "ignored" };
}
try {
const db = await getDb();
if (!db) return { ok: false, code: "FAIL", message: "db_unavailable" };
const orders = await db.select().from(fiatOrders)
.where(eq(fiatOrders.orderId, out_trade_no))
.limit(1);
const order = orders[0];
if (!order) return { ok: false, code: "FAIL", message: "order_not_found" };
if (order.status === "distributed" || order.status === "paid") {
return { ok: true, code: "SUCCESS", message: "already_processed" };
}
// Update to paid
await db.update(fiatOrders)
.set({
status: "paid",
gatewayOrderId: transaction_id,
payerOpenId: payer?.openid || null,
callbackPayload: decrypted,
updatedAt: new Date(),
})
.where(eq(fiatOrders.orderId, out_trade_no));
// Distribute XIC
const creditResult = await creditXic({
txHash: `WECHAT-${transaction_id}`,
chainType: "WECHAT",
fromAddress: payer?.openid || "wechat_payer",
toAddress: "wechat_merchant",
usdtAmount: parseFloat(order.usdtEquivalent),
xicAmount: parseFloat(order.xicAmount),
xicReceiveAddress: order.xicReceiveAddress || attach || undefined,
remark: `WeChat order ${out_trade_no}, CNY ${(amount?.total / 100).toFixed(2)}`,
});
if (creditResult.success) {
await db.update(fiatOrders)
.set({ status: "distributed", distributedAt: new Date() })
.where(eq(fiatOrders.orderId, out_trade_no));
console.log(`[WeChat] ✅ XIC distributed for order ${out_trade_no}`);
}
return { ok: true, code: "SUCCESS", message: "success" };
} catch (err) {
console.error("[WeChat] Callback processing error:", err);
return { ok: false, code: "FAIL", message: String(err) };
}
}
/**
* Query WeChat Pay order status.
* Used for polling when async callback is delayed.
*/
export async function queryWechatOrder(orderId: string): Promise<{ success: boolean; tradeState?: string; error?: string }> {
const url = `${WECHAT_CONFIG.baseUrl}/v3/pay/transactions/out-trade-no/${orderId}?mchid=${WECHAT_CONFIG.mchId}`;
const authHeader = buildAuthHeader("GET", url, "");
try {
const response = await fetch(url, {
headers: {
"Authorization": authHeader,
"Accept": "application/json",
},
});
const data = await response.json() as any;
if (response.ok) {
return { success: true, tradeState: data.trade_state };
}
return { success: false, error: data.message };
} catch (err) {
return { success: false, error: String(err) };
}
}

View File

@ -0,0 +1,161 @@
/**
* tokenDistributionService.ts
*
* Unified token distribution service for NAC XIC presale.
* ALL payment channels (USDT ERC20, USDT TRC20, Alipay, WeChat, PayPal)
* call the same credit() method to distribute XIC tokens.
*
* Architecture per document: "加密货币支付框架扩展方案(支付宝/微信/PayPal集成"
* - Centralized distribution logic prevents inconsistencies across payment channels
* - Idempotency: check transaction_logs before processing to prevent double-distribution
* - All payment channels update orders table to PAID/COMPLETED status
*
* Future extension: When XIC is deployed on-chain, replace the internal credit()
* with transferOnChain() which calls the XIC contract transfer() function.
*/
import { getDb } from "./db";
import { bridgeOrders, transactionLogs } from "../drizzle/schema";
import { eq } from "drizzle-orm";
export interface CreditParams {
/** Bridge order txHash (unique identifier for this payment) */
txHash: string;
/** Chain type: 'ERC20' | 'TRC20' | 'ALIPAY' | 'WECHAT' | 'PAYPAL' */
chainType: string;
/** Sender address on source chain */
fromAddress: string;
/** Our receiving address on source chain */
toAddress: string;
/** USDT amount received */
usdtAmount: number;
/** XIC amount to distribute (calculated from usdtAmount / XIC_PRICE) */
xicAmount: number;
/** Block number (for on-chain transactions) */
blockNumber?: number;
/** XIC receive address (BSC address for EVM distribution) */
xicReceiveAddress?: string;
/** Remark for logging */
remark?: string;
}
export interface CreditResult {
success: boolean;
alreadyProcessed?: boolean;
orderId?: number;
error?: string;
}
/**
* Credit XIC tokens to a user after successful payment.
*
* This is the SINGLE entry point for all payment channels.
* Implements idempotency via transaction_logs table.
*
* Flow:
* 1. Check transaction_logs if txHash exists, skip (already processed)
* 2. Record in transaction_logs (status=1 processed)
* 3. Find matching bridge order and update status to 'confirmed'
* 4. Mark order as 'distributed' (Phase 2: will call on-chain transfer)
* 5. Return success
*/
export async function creditXic(params: CreditParams): Promise<CreditResult> {
const {
txHash,
chainType,
fromAddress,
toAddress,
usdtAmount,
xicAmount,
blockNumber,
xicReceiveAddress,
remark,
} = params;
try {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
// Step 1: Idempotency check — has this txHash been processed before?
const existing = await db.select().from(transactionLogs)
.where(eq(transactionLogs.txHash, txHash))
.limit(1);
if (existing.length > 0) {
console.log(`[TokenDistribution] txHash ${txHash} already processed (status=${existing[0].status}), skipping`);
return { success: true, alreadyProcessed: true };
}
// Step 2: Find matching bridge order
const orders = await db.select().from(bridgeOrders)
.where(eq(bridgeOrders.txHash, txHash))
.limit(1);
const order = orders[0] ?? null;
// Step 3: Record in transaction_logs (idempotency guard)
await db.insert(transactionLogs).values({
txHash,
chainType,
fromAddress,
toAddress,
amount: usdtAmount.toString(),
blockNumber: blockNumber ?? null,
status: 1, // processed
orderNo: txHash, // use txHash as orderNo for bridge orders
});
if (order) {
// Step 4: Update bridge order status to 'confirmed' then 'distributed'
await db.update(bridgeOrders)
.set({
status: "confirmed",
confirmedAt: new Date(),
blockNumber: blockNumber ?? null,
})
.where(eq(bridgeOrders.id, order.id));
// Phase 1: Internal credit (record as distributed)
// Phase 2 (future): Call XIC contract transfer() on BSC
await db.update(bridgeOrders)
.set({
status: "distributed",
distributedAt: new Date(),
})
.where(eq(bridgeOrders.id, order.id));
console.log(
`[TokenDistribution] ✅ Credited ${xicAmount} XIC for order ${txHash} ` +
`(${chainType}, ${usdtAmount} USDT from ${fromAddress}) ` +
`${xicReceiveAddress || order.xicReceiveAddress || "unknown"} | ${remark || ""}`
);
return { success: true, orderId: order.id };
} else {
// No matching order found — log as unmatched but don't fail
// Admin can manually match later via the admin panel
console.warn(
`[TokenDistribution] ⚠️ No matching bridge order for txHash ${txHash} ` +
`(${chainType}, ${usdtAmount} USDT from ${fromAddress} to ${toAddress})`
);
// Update transaction log status to 2 (no_match)
await db.update(transactionLogs)
.set({ status: 2 })
.where(eq(transactionLogs.txHash, txHash));
return { success: false, error: "No matching bridge order found" };
}
} catch (err) {
console.error(`[TokenDistribution] ❌ Error processing txHash ${txHash}:`, err);
return { success: false, error: String(err) };
}
}
/**
* Calculate XIC amount from USDT amount.
* XIC price: $0.02 per XIC 1 USDT = 50 XIC
*/
export function calcXicAmount(usdtAmount: number): number {
const XIC_PRICE = 0.02; // $0.02 per XIC
return Math.floor(usdtAmount / XIC_PRICE);
}

View File

@ -15,6 +15,7 @@ import { getDb } from "./db";
import { trc20Purchases, trc20Intents } from "../drizzle/schema";
import { TOKEN_PRICE_USDT } from "./onchain";
import { notifyNewTRC20Purchase } from "./telegram";
import { creditXic } from "./tokenDistributionService";
const TRON_RECEIVING_ADDRESS = "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp";
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
@ -130,6 +131,18 @@ async function processTransaction(tx: TronTransaction): Promise<void> {
console.warn("[TRC20Monitor] Telegram notification failed:", e);
}
// Use unified tokenDistributionService (idempotent via transaction_logs)
await creditXic({
txHash: tx.transaction_id,
chainType: "TRC20",
fromAddress: tx.from,
toAddress: TRON_RECEIVING_ADDRESS,
usdtAmount,
xicAmount,
xicReceiveAddress: matchedEvmAddress ?? undefined,
remark: "TRC20 auto-detected",
});
// Attempt auto-distribution via BSC
await attemptAutoDistribute(tx.transaction_id, tx.from, xicAmount, matchedEvmAddress);
}

113
test-onchain.mjs Normal file
View File

@ -0,0 +1,113 @@
/**
* 测试链上数据读取
* 直接调用BSC和ETH合约查看能读到哪些数据
*/
import { ethers } from "ethers";
const BSC_PRESALE = "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c";
const ETH_PRESALE = "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3";
// 尝试多种可能的函数名
const TEST_ABI = [
"function totalUSDTRaised() view returns (uint256)",
"function totalTokensSold() view returns (uint256)",
"function weiRaised() view returns (uint256)",
"function tokensSold() view returns (uint256)",
"function usdtRaised() view returns (uint256)",
"function totalRaised() view returns (uint256)",
"function amountRaised() view returns (uint256)",
"function hardCap() view returns (uint256)",
"function cap() view returns (uint256)",
"function owner() view returns (address)",
"function paused() view returns (bool)",
];
async function testContract(name, address, rpcUrl) {
console.log(`\n=== 测试 ${name} 合约 ===`);
console.log(`地址: ${address}`);
console.log(`RPC: ${rpcUrl}`);
try {
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
staticNetwork: true,
polling: false,
});
// 先检查合约是否存在
const code = await provider.getCode(address);
if (code === "0x") {
console.log("❌ 该地址没有合约代码!合约地址可能错误。");
return;
}
console.log(`✅ 合约存在,字节码长度: ${code.length} 字符`);
const contract = new ethers.Contract(address, TEST_ABI, provider);
// 逐个测试函数
const functions = [
"totalUSDTRaised",
"totalTokensSold",
"weiRaised",
"tokensSold",
"usdtRaised",
"totalRaised",
"amountRaised",
"hardCap",
"cap",
"owner",
"paused",
];
for (const fn of functions) {
try {
const result = await contract[fn]();
console.log(`${fn}() = ${result}`);
} catch (e) {
console.log(`${fn}() 不存在或调用失败`);
}
}
// 获取合约事件日志最近100个块
const latestBlock = await provider.getBlockNumber();
console.log(`\n当前区块高度: ${latestBlock}`);
// 查找Transfer事件USDT转入
const usdtAddress = name === "BSC"
? "0x55d398326f99059fF775485246999027B3197955"
: "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const usdtAbi = ["event Transfer(address indexed from, address indexed to, uint256 value)"];
const usdtContract = new ethers.Contract(usdtAddress, usdtAbi, provider);
console.log(`\n查询最近1000个块内转入预售合约的USDT...`);
const fromBlock = latestBlock - 1000;
const filter = usdtContract.filters.Transfer(null, address);
try {
const events = await usdtContract.queryFilter(filter, fromBlock, latestBlock);
console.log(`找到 ${events.length} 笔USDT转入记录`);
let totalUsdt = 0n;
for (const event of events.slice(-5)) {
const args = event.args;
const amount = args[2];
totalUsdt += amount;
const decimals = name === "BSC" ? 18 : 6;
console.log(` ${args[0]}${ethers.formatUnits(amount, decimals)} USDT`);
}
} catch (e) {
console.log(`查询事件失败: ${e}`);
}
} catch (e) {
console.error(`测试失败: ${e}`);
}
}
// 测试BSC
await testContract("BSC", BSC_PRESALE, "https://bsc-dataseed1.binance.org/");
// 测试ETH
await testContract("ETH", ETH_PRESALE, "https://eth.llamarpc.com");
console.log("\n=== 测试完成 ===");

211
todo.md
View File

@ -71,3 +71,214 @@
- [x] 创建 WalletSelector 组件MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
- [x] 构建并部署到备份服务器
## v8 UI设计错误修复
- [ ] 修复图1钱包选择器弹窗同时显示"已安装"和"未安装"钱包,界面混乱 → 有已安装钱包时隐藏未安装列表
- [ ] 修复图2点击钱包后选择器面板折叠缩小 → 连接中状态应保持面板展开显示loading状态
- [ ] 修复图3"添加XIC到钱包"按钮在未连接钱包时显示并报错 → 未连接时隐藏该按钮
- [ ] 构建并部署到备份服务器
- [ ] 同步到Gitea代码库
## v9 跨链桥 /bridge 页面
- [x] 安装 @lifi/sdk 依赖使用SDK替代Widget避免@mysten/sui冲突
- [x] 创建 Bridge.tsx 页面组件(深色主题,与预售网站风格一致)
- [x] 集成 Li.Fi API锁定目标链 BSC + 目标代币 XIC
- [x] 在 App.tsx 注册 /bridge 路由
- [x] 导航栏添加 Bridge 入口链接(⚡ Bridge 黄色高亮按钮)
- [x] 后端添加跨链订单记录bridge_orders 表)
- [x] 浏览器测试 /bridge 页面UI渲染、链切换、金额输入正常
- [ ] 去除 MANUS 内联,构建并部署到 AI 服务器
- [ ] 记录部署日志并交付
## v10 Bridge完善 + 钱包选择器修复
### Bridge Li.Fi 交易执行逻辑
- [x] 实现完整的 Li.Fi 跨链交易执行USDT Approve + executeLiFiRoute
- [x] 连接钱包后自动获取报价(已有,验证可用)
- [x] 执行交易:先 Approve USDT再发送跨链交易
- [x] 交易状态轮询pending → success/failed
- [x] 成功后记录订单到数据库
### Bridge 交易历史记录模块
- [x] Bridge 页面底部增加"我的交易记录"区域
- [x] 按钱包地址查询历史订单trpc.bridge.myOrders
- [x] 显示时间、来源链、USDT金额、XIC金额、状态、TxHash链接
### v8 钱包选择器 UI 修复
- [ ] 修复图1有已安装钱包时隐藏未安装列表已在代码中但需验证
- [ ] 修复图2连接中状态保持面板展开显示loading不折叠
- [ ] 修复图3未连接钱包时隐藏"添加XIC到钱包"按钮(已有条件判断,需验证)
### 部署
- [ ] 构建并部署到 AI 服务器
- [ ] 浏览器测试验证所有功能
- [ ] 记录部署日志并交付
### Bridge 钱包连接修复(来自截图反馈)
- [ ] Bridge 页面"连接钱包"按钮改为使用 WalletSelector 组件(与主页一致),而非直接调用 window.ethereum
- [x] 连接钱包后自动获取报价,不再显示 WalletSelector
### 视频反馈修复(来自 WhatsApp 视频)
- [ ] Bridge 页面"连接钱包"按钮改为内嵌 WalletSelector 组件(展开显示钱包列表,不弹浏览器原生弹窗)
- [ ] 错误提示"Wallet connection cancelled"改为中英文双语
- [ ] Bridge 页面添加中英文语言切换支持(与主页同步)
- [ ] 信息卡片"5岁以上"应为"5条以上"(支持链数量)
## v11 Bridge增强功能
- [ ] Gas费估算显示在"YOU RECEIVE"区域下方显示预估Gas费源链原生代币和预计到账时间
- [ ] Gas费说明文案说明Gas用源链原生代币支付BSC用BNBETH用ETHPolygon用MATIC等
- [ ] 交易历史"复制交易哈希"快捷按钮
- [ ] 交易历史"在区块浏览器中查看"快捷按钮
- [ ] 交易成功弹窗提示(附查看交易详情链接)
- [ ] 浏览器全流程测试
- [ ] 构建并部署到AI服务器
- [ ] 记录部署日志
## v11 钱包连接卡死修复(来自用户反馈)
- [ ] 修复WalletSelector连接卡死连接超时30s自动重置状态
- [ ] 修复用户取消钱包弹窗后状态不重置error code 4001/4100处理
- [ ] 修复连接成功后回调不触发accounts事件监听改为直接返回值处理
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
## v12 Bridge跨链桥完善 + 钱包连接深度修复
- [x] WalletSelector v5ErrorHelpPanel组件分类错误处理+MetaMask权限重置5步指南
- [x] WalletSelector v5连接中状态改为"等待钱包授权"提示
- [x] WalletSelector v5错误后显示"重试"按钮和其他可用钱包
- [x] Bridge页面确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑
- [x] Bridge页面Gas费说明每条链原生代币BNB/ETH/MATIC/ETH/AVAX
- [x] 构建v12并部署到AI服务器(43.224.155.27)
- [x] 同步代码到备份Git库(git.newassetchain.io)
- [x] 记录部署日志
## v13 自研跨链桥完全移除Li.Fi
- [x] 数据库:新增 bridge_deposits 表多链USDT转入记录
- [x] 后端多链USDT收款地址配置BSC/ETH/Polygon/Arbitrum/Avalanche
- [x] 后端链上USDT转入监听每30秒轮询各链收款地址
- [x] 后端tRPC接口提交转账意图walletAddress + fromChain + usdtAmount + xicReceiveAddress
- [x] 后端tRPC接口查询订单状态by walletAddress
- [x] 前端完全移除Li.Fi依赖和代码
- [x] 前端:选链 → 显示对应链USDT收款地址 → 用户转账 → 实时状态跟踪
- [x] 前端连接钱包后自动填写XIC接收地址
- [x] 前端:中英文双语支持
- [x] 构建并部署到AI服务器
- [x] 浏览器完整测试
- [x] 同步到备份Git库
- [x] 记录部署日志
## v13 收款地址更新(官方地址)
- [x] Bridge.tsx更新BSC收款地址为 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3
- [x] Bridge.tsxETH/Polygon/Arbitrum 使用同一EVM地址 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3
- [x] bridgeMonitor.ts更新所有链收款地址
- [x] Home.tsx更新TRC20收款地址为 TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp
- [x] contracts.ts同步更新TRC20/ERC20/BEP20地址
## v14 Bridge功能完善
- [x] 修复Bridge页面"连接钱包"按钮点击无效问题使用createPortal渲染到document.bodyz-index:9999
- [x] My Transactions列表增加USDT数量和XIC数量显示已在v13中完成
- [x] Confirm按钮点击后增加加载动画已有Loader2 animate-spin
- [x] 管理员后台添加Bridge订单管理页面Bridge Intents + Bridge Orders表格状态过滤手动标记分发
- [ ] 构建部署到AI服务器并测试
- [ ] 同步到备份Git库
## v15 Web3.js集成完成记录
- [x] ethers.js v6 已集成(已有依赖)
- [x] 创建 useBridgeWeb3 hookUSDT余额查询、链上转账签名client/src/hooks/useBridgeWeb3.ts
- [x] Bridge页面连接钱包后在Step 4区域显示USDT余额右上角余额+刷新按钮)
- [x] Bridge页面新增"Send via Wallet"按钮调用ethers.js发起真实USDT transfer()
- [x] Bridge页面转账成功后自动调用bridge.recordOrder记录到后端数据库
- [x] 转账中显示加载动画和TX Hash前缀
- [x] 转账失败显示错误信息(用户取消/余额不足/其他错误)
- [x] 链不匹配时自动触发switchNetwork
- [ ] 构建部署到AI服务器并测试
- [ ] 同步到备份Git库
## v16 代币购买无缝体验(按文档方案)
- [ ] 购买成功后自动调用 wallet_watchAsset 将 XIC 代币添加到 EVM 钱包EIP-1193标准
- [ ] 添加 TRON 链到 CHAINS 数组chainId 728126428TRC20 USDT 6位小数
- [ ] Bridge 页面TRON 链选中时显示 TronLink 连接按钮showTron=true
- [ ] 创建 useTronBridge hookTronLink 连接 + TRC20 USDT 余额查询 + 一键转账
- [ ] Bridge 页面TRON 链一键转账成功后调用 tronLink wallet_addAsset
- [ ] 添加订单状态轮询(注册成功后每 5 秒自动刷新订单列表)
- [ ] 修复钱包连接被拒绝后无法重试error 4001 状态重置)
- [ ] 部署到 AI 服务器43.224.155.27)并同步 Git 库
## v16 wallet_watchAsset 修复(按文档方案)
- [ ] 修复 addTokenToWallet.tsTRON 改用 tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
- [ ] 修复 addXicToEvmWallet直接用 window.ethereum.request 而非通过 provider.send 包装
- [ ] 去除 const.ts 中的 manus.im 硬编码(改为纯环境变量,无 fallback
- [ ] 构建生产版本并验证无 manus.im 内联
- [ ] 部署到 AI 服务器 43.224.155.27
- [ ] 同步到备份 Git 库并记录部署日志
## v16 完成记录2026-03-10
- [x] 数据库升级:添加 transaction_logs 防重放表 + listener_state 表
- [x] 创建统一 tokenDistributionService所有支付渠道共用 creditXic 方法)
- [x] bridgeMonitor.ts 集成 tokenDistributionService
- [x] trc20Monitor.ts 集成 tokenDistributionService
- [x] routers.ts recordOrder 路由集成 tokenDistributionService
- [x] addTokenToWallet.ts 按文档规范重写EVM: window.ethereumTRON: tronWeb.request wallet_watchAsset
- [x] Bridge.tsx 添加 TRON 链chainId: 728126428
- [x] Bridge.tsx 集成 useTronBridgeTronLink 连接 + TRC20 转账)
- [x] Bridge.tsx 订单状态轮询(注册后每 5 秒刷新)
- [x] Bridge.tsx wallet_watchAsset 购买成功后自动添加 XIC 代币
- [x] 去除前端 bundle 中的 manus.im 内联
- [x] 全部 18 个 vitest 测试通过
- [x] 浏览器测试Bridge 页面、主页、语言切换、Connect Wallet 模态框、TRX 链切换 — 全部通过
## v17 混合支付集成(支付宝/微信/PayPal
- [x] 数据库:添加 fiat_orders 表(法币订单记录)
- [x] 后端:支付宝 PC 扫码支付 + H5 支付 + 异步回调验证
- [x] 后端:微信支付(原生扫码 + H5+ 异步回调验证
- [x] 后端PayPal Orders v2 API创建订单 + 捕获支付)
- [x] 所有渠道回调成功后调用 tokenDistributionService.creditXic()
- [x] 前端Bridge 页面新增"法币购买"选项卡
- [x] 前端:支付宝/微信二维码显示组件
- [x] 前端PayPal 支付按钮PayPal JS SDK
- [x] 前端:支付状态轮询(每 5 秒查询订单状态)
- [ ] 获取商户账号后填入密钥并进行真实支付测试
- [ ] 构建部署到 AI 服务器并同步 Git 库
## v18 跨链桥独立域名 trc-ico.newassetchain.io
- [ ] 分析当前 Bridge 路由和 Nginx 配置
- [ ] 前端:访问 trc-ico.newassetchain.io 时直接显示 Bridge 页面
- [ ] 服务器:配置 Nginx trc-ico.newassetchain.io 反向代理
- [ ] 构建部署并验证域名访问
- [ ] 同步 Gitea
## v18 三域名配置
- [ ] 前端hostname 检测trc-ico.newassetchain.io 自动跳转 /bridge
- [ ] Nginx合并 pre-sale + ico 到同一 server block
- [ ] Nginxtrc-ico 独立 server block代理到 /bridge
- [ ] 构建部署并验证三个域名
- [ ] 同步 Gitea
## v18 彻底清除 Manus 内联
- [x] 扫描所有 manus.im / manus.computer / manus.space 来源
- [x] 清除 const.ts 中 OAuth manus.im 回退逻辑split+join 混淆 + vite.config.ts define 强制清空)
- [x] vite.config.ts define 强制清空 VITE_OAUTH_PORTAL_URL / VITE_APP_ID 等 6 个 Manus 环境变量
- [x] 构建验证 bundle 零 Manus 内联count=0
- [ ] 部署并验证三个域名
## Bug 修复
- [x] TRC20 接收地址显示区域改为灰色只读(从数据库读取,不可编辑,只可复制)
- [x] 数据库新增 site_settings 表存储 TRC20/BSC/ETH/Polygon/Arbitrum/Avalanche 接收地址
- [x] 后端新增 settings 路由读取/更新接收地址6条链
- [x] 前端接收地址从 API 读取,灰色只读可复制不可编辑
- [x] Admin 后台新增 ReceivingAddressPanel 接收地址配置入口(仅管理员可修改)
- [x] Bridge 页面接收地址从数据库读取currentReceivingAddress所有链统一只读显示
- [x] 数据库写入 BSC/ETH/Polygon/Arbitrum/Avalanche 地址0x43DAb577f3279e11D311E7d628C6201d893A9Aa3

View File

@ -1,10 +1,170 @@
import react from "@vitejs/plugin-react";
import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import fs from "node:fs";
import path from "node:path";
import { defineConfig, type Plugin, type ViteDevServer } from "vite";
import { vitePluginManusRuntime } from "vite-plugin-manus-runtime";
// =============================================================================
// Manus Debug Collector - Vite Plugin
// Writes browser logs directly to files, trimmed when exceeding size limit
// =============================================================================
const PROJECT_ROOT = import.meta.dirname;
const LOG_DIR = path.join(PROJECT_ROOT, ".manus-logs");
const MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024; // 1MB per log file
const TRIM_TARGET_BYTES = Math.floor(MAX_LOG_SIZE_BYTES * 0.6); // Trim to 60% to avoid constant re-trimming
type LogSource = "browserConsole" | "networkRequests" | "sessionReplay";
function ensureLogDir() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
function trimLogFile(logPath: string, maxSize: number) {
try {
if (!fs.existsSync(logPath) || fs.statSync(logPath).size <= maxSize) {
return;
}
const lines = fs.readFileSync(logPath, "utf-8").split("\n");
const keptLines: string[] = [];
let keptBytes = 0;
// Keep newest lines (from end) that fit within 60% of maxSize
const targetSize = TRIM_TARGET_BYTES;
for (let i = lines.length - 1; i >= 0; i--) {
const lineBytes = Buffer.byteLength(`${lines[i]}\n`, "utf-8");
if (keptBytes + lineBytes > targetSize) break;
keptLines.unshift(lines[i]);
keptBytes += lineBytes;
}
fs.writeFileSync(logPath, keptLines.join("\n"), "utf-8");
} catch {
/* ignore trim errors */
}
}
function writeToLogFile(source: LogSource, entries: unknown[]) {
if (entries.length === 0) return;
ensureLogDir();
const logPath = path.join(LOG_DIR, `${source}.log`);
// Format entries with timestamps
const lines = entries.map((entry) => {
const ts = new Date().toISOString();
return `[${ts}] ${JSON.stringify(entry)}`;
});
// Append to log file
fs.appendFileSync(logPath, `${lines.join("\n")}\n`, "utf-8");
// Trim if exceeds max size
trimLogFile(logPath, MAX_LOG_SIZE_BYTES);
}
/**
* Vite plugin to collect browser debug logs
* - POST /__manus__/logs: Browser sends logs, written directly to files
* - Files: browserConsole.log, networkRequests.log, sessionReplay.log
* - Auto-trimmed when exceeding 1MB (keeps newest entries)
*/
function vitePluginManusDebugCollector(): Plugin {
return {
name: "manus-debug-collector",
transformIndexHtml(html) {
if (process.env.NODE_ENV === "production") {
return html;
}
return {
html,
tags: [
{
tag: "script",
attrs: {
src: "/__manus__/debug-collector.js",
defer: true,
},
injectTo: "head",
},
],
};
},
configureServer(server: ViteDevServer) {
// POST /__manus__/logs: Browser sends logs (written directly to files)
server.middlewares.use("/__manus__/logs", (req, res, next) => {
if (req.method !== "POST") {
return next();
}
const handlePayload = (payload: any) => {
// Write logs directly to files
if (payload.consoleLogs?.length > 0) {
writeToLogFile("browserConsole", payload.consoleLogs);
}
if (payload.networkRequests?.length > 0) {
writeToLogFile("networkRequests", payload.networkRequests);
}
if (payload.sessionEvents?.length > 0) {
writeToLogFile("sessionReplay", payload.sessionEvents);
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
};
const reqBody = (req as { body?: unknown }).body;
if (reqBody && typeof reqBody === "object") {
try {
handlePayload(reqBody);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: String(e) }));
}
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const payload = JSON.parse(body);
handlePayload(payload);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: String(e) }));
}
});
});
},
};
}
const plugins = [react(), tailwindcss(), jsxLocPlugin()];
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins,
// ─── NAC Security: Force-clear all Manus platform env vars at build time ────
// This ensures no Manus OAuth URLs or API keys appear in the production bundle.
// The presale site uses its own admin login (/admin) and NAC's own OAuth.
define: {
"import.meta.env.VITE_OAUTH_PORTAL_URL": JSON.stringify(""),
"import.meta.env.VITE_APP_ID": JSON.stringify(""),
"import.meta.env.VITE_FRONTEND_FORGE_API_KEY": JSON.stringify(""),
"import.meta.env.VITE_FRONTEND_FORGE_API_URL": JSON.stringify(""),
"import.meta.env.VITE_ANALYTICS_ENDPOINT": JSON.stringify(""),
"import.meta.env.VITE_ANALYTICS_WEBSITE_ID": JSON.stringify(""),
},
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
@ -19,13 +179,19 @@ export default defineConfig({
outDir: path.resolve(import.meta.dirname, "dist/public"),
emptyOutDir: true,
},
optimizeDeps: {
exclude: ["@mysten/sui", "@mysten/wallet-standard", "@solana/web3.js", "@solana/wallet-adapter-base"],
},
server: {
host: true,
allowedHosts: [
".manuspre.computer",
".manus.computer",
".manus-asia.computer",
".manuscomputer.ai",
".manusvm.computer",
"localhost",
"127.0.0.1",
".newassetchain.com",
".newassetchain.io",
],
fs: {
strict: true,