import { bigint, boolean, decimal, int, mysqlEnum, mysqlTable, text, timestamp, varchar, } from "drizzle-orm/mysql-core"; /** * Core user table backing auth flow. * Extend this file with additional tables as your product grows. * Columns use camelCase to match both database fields and generated types. */ export const users = mysqlTable("users", { /** * Surrogate primary key. Auto-incremented numeric value managed by the database. * Use this for relations between tables. */ id: int("id").autoincrement().primaryKey(), /** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */ openId: varchar("openId", { length: 64 }).notNull().unique(), name: text("name"), email: varchar("email", { length: 320 }), loginMethod: varchar("loginMethod", { length: 64 }), role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(), }); export type User = typeof users.$inferSelect; export type InsertUser = typeof users.$inferInsert; // TRC20 purchase records — monitored from TRON network export const trc20Purchases = mysqlTable("trc20_purchases", { id: int("id").autoincrement().primaryKey(), txHash: varchar("txHash", { length: 128 }).notNull().unique(), fromAddress: varchar("fromAddress", { length: 64 }).notNull(), usdtAmount: decimal("usdtAmount", { precision: 20, scale: 6 }).notNull(), xicAmount: decimal("xicAmount", { precision: 30, scale: 6 }).notNull(), blockNumber: bigint("blockNumber", { mode: "number" }), status: mysqlEnum("status", ["pending", "confirmed", "distributed", "failed"]) .default("pending") .notNull(), distributedAt: timestamp("distributedAt"), distributeTxHash: varchar("distributeTxHash", { length: 128 }), evmAddress: varchar("evmAddress", { length: 64 }), // EVM address provided by buyer for token distribution createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }); export type Trc20Purchase = typeof trc20Purchases.$inferSelect; export type InsertTrc20Purchase = typeof trc20Purchases.$inferInsert; // Presale stats cache — refreshed from on-chain every 60 seconds export const presaleStatsCache = mysqlTable("presale_stats_cache", { id: int("id").autoincrement().primaryKey(), chain: varchar("chain", { length: 16 }).notNull(), usdtRaised: decimal("usdtRaised", { precision: 30, scale: 6 }).default("0"), tokensSold: decimal("tokensSold", { precision: 30, scale: 6 }).default("0"), weiRaised: decimal("weiRaised", { precision: 30, scale: 6 }).default("0"), lastUpdated: timestamp("lastUpdated").defaultNow().notNull(), }); export type PresaleStatsCache = typeof presaleStatsCache.$inferSelect; // TRC20 purchase intents — user pre-registers EVM address before sending USDT // When TRC20 Monitor detects a TX from the same TRON address, it auto-fills evmAddress export const trc20Intents = mysqlTable("trc20_intents", { id: int("id").autoincrement().primaryKey(), tronAddress: varchar("tronAddress", { length: 64 }), // TRON sender address (optional, for matching) evmAddress: varchar("evmAddress", { length: 64 }).notNull(), // BSC/ETH address to receive XIC expectedUsdt: decimal("expectedUsdt", { precision: 20, scale: 6 }), // Expected USDT amount (optional) matched: boolean("matched").default(false).notNull(), // Whether this intent has been matched to a purchase matchedPurchaseId: int("matchedPurchaseId"), // ID of matched trc20_purchases record createdAt: timestamp("createdAt").defaultNow().notNull(), }); export type Trc20Intent = typeof trc20Intents.$inferSelect; export type InsertTrc20Intent = typeof trc20Intents.$inferInsert; // Presale configuration — editable by admin from the admin panel // Each row is a key-value pair (e.g. presaleEndDate, tokenPrice, hardCap, etc.) export const presaleConfig = mysqlTable("presale_config", { id: int("id").autoincrement().primaryKey(), key: varchar("key", { length: 64 }).notNull().unique(), value: text("value").notNull(), label: varchar("label", { length: 128 }), // Human-readable label for admin UI description: varchar("description", { length: 256 }), // Help text type: varchar("type", { length: 32 }).default("text"), // text | number | date | boolean | url updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }); export type PresaleConfig = typeof presaleConfig.$inferSelect; 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;