204 lines
11 KiB
TypeScript
204 lines
11 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;
|