nac-presale/drizzle/schema.ts

220 lines
12 KiB
TypeScript

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;