Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions scripts/scripts/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* TradeFlow – Database Seed Script
* Run: npm run seed
*
* Generates:
* - 20 mock users with realistic Stellar G-addresses
* - 3 distinct liquidity pools
* - 5,000+ historical swap records spread across a 30-day timeline
*
* Idempotent: clears all existing data before seeding.
* All inserts are wrapped in a single transaction for performance.
*/

import { PrismaClient, Prisma } from "@prisma/client";
import { faker } from "@faker-js/faker";

const prisma = new PrismaClient();

// ─── Config ──────────────────────────────────────────────────────────────────
const SEED_CONFIG = {
users: 20,
pools: 3,
swaps: 5000,
daysBack: 30,
} as const;

// ─── Stellar address generator ────────────────────────────────────────────────
// Real Stellar G-addresses are base32-encoded 32-byte ed25519 public keys.
// We generate deterministic, realistic-looking addresses using faker.
const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

function randomStellarAddress(): string {
// G + 55 base32 chars = 56 chars total (matches real Stellar address length)
let addr = "G";
for (let i = 0; i < 55; i++) {
addr += BASE32_CHARS[Math.floor(Math.random() * BASE32_CHARS.length)];
}
return addr;
}

// ─── Pool definitions ─────────────────────────────────────────────────────────
const POOL_DEFINITIONS = [
{
name: "XLM/USDC",
tokenA: "XLM",
tokenB: "USDC",
basePrice: 0.11, // XLM price in USDC
priceVolatility: 0.05, // ±5% daily drift
liquidityUsd: 2_500_000,
},
{
name: "XLM/BTC",
tokenA: "XLM",
tokenB: "BTC",
basePrice: 0.0000018,
priceVolatility: 0.08,
liquidityUsd: 800_000,
},
{
name: "USDC/EURC",
tokenA: "USDC",
tokenB: "EURC",
basePrice: 0.92,
priceVolatility: 0.01, // Stablecoin pair — low volatility
liquidityUsd: 5_000_000,
},
];

// ─── Helpers ──────────────────────────────────────────────────────────────────

/** Returns a random Date between `daysAgo` days ago and now */
function randomDateInWindow(daysAgo: number): Date {
const now = Date.now();
const windowMs = daysAgo * 24 * 60 * 60 * 1000;
return new Date(now - Math.random() * windowMs);
}

/**
* Simulate a realistic price walk using geometric Brownian motion.
* Returns a price multiplier relative to the pool's base price.
*/
function priceWalk(basePrice: number, volatility: number, daysElapsed: number): number {
// Simple GBM approximation: price * e^(vol * sqrt(dt) * Z)
const dt = daysElapsed / 365;
const Z = faker.number.float({ min: -2, max: 2 });
const drift = Math.exp(volatility * Math.sqrt(dt) * Z);
return Math.max(basePrice * drift, basePrice * 0.5); // floor at 50% of base
}

/**
* Generate a mathematically valid swap:
* - amountIn: random between 10–50,000 units of tokenA
* - amountOut: amountIn * exchangeRate * (1 - fee) with ±0.5% slippage
* - fee: 0.3% (Uniswap V2 standard)
*/
function generateSwapAmounts(
exchangeRate: number
): { amountIn: number; amountOut: number; fee: number; priceImpact: number } {
const FEE_RATE = 0.003;
const amountIn = faker.number.float({ min: 10, max: 50_000, fractionDigits: 6 });
const slippage = faker.number.float({ min: -0.005, max: 0.005 }); // ±0.5%
const amountOut = amountIn * exchangeRate * (1 - FEE_RATE) * (1 + slippage);
const fee = amountIn * FEE_RATE;
const priceImpact = Math.abs(slippage) + (amountIn / 1_000_000) * 0.01; // size-dependent impact
return {
amountIn: parseFloat(amountIn.toFixed(6)),
amountOut: parseFloat(Math.max(amountOut, 0.000001).toFixed(6)),
fee: parseFloat(fee.toFixed(6)),
priceImpact: parseFloat(priceImpact.toFixed(6)),
};
}

// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
console.log("🌱 TradeFlow seed starting...\n");

// ── 1. Idempotent clear ────────────────────────────────────────────────────
console.log("🗑️ Clearing existing data...");
await prisma.$transaction([
prisma.swap.deleteMany(),
prisma.pool.deleteMany(),
prisma.user.deleteMany(),
]);
console.log(" ✓ Tables cleared\n");

// ── 2. Seed users ──────────────────────────────────────────────────────────
console.log(`👤 Creating ${SEED_CONFIG.users} users...`);
const userData: Prisma.UserCreateManyInput[] = Array.from(
{ length: SEED_CONFIG.users },
() => ({
walletAddress: randomStellarAddress(),
username: faker.internet.username(),
email: faker.internet.email(),
createdAt: randomDateInWindow(60), // users created up to 60 days ago
})
);

await prisma.user.createMany({ data: userData });
const users = await prisma.user.findMany();
console.log(` ✓ ${users.length} users created\n`);

// ── 3. Seed pools ──────────────────────────────────────────────────────────
console.log(`🏊 Creating ${POOL_DEFINITIONS.length} liquidity pools...`);
const poolData: Prisma.PoolCreateManyInput[] = POOL_DEFINITIONS.map((def) => ({
name: def.name,
tokenA: def.tokenA,
tokenB: def.tokenB,
liquidityUsd: def.liquidityUsd,
basePrice: def.basePrice,
createdAt: randomDateInWindow(60),
}));

await prisma.pool.createMany({ data: poolData });
const pools = await prisma.pool.findMany();
console.log(` ✓ Pools: ${pools.map((p) => p.name).join(", ")}\n`);

// ── 4. Seed swaps (single transaction) ─────────────────────────────────────
console.log(`🔄 Generating ${SEED_CONFIG.swaps} swap records...`);

const swapData: Prisma.SwapCreateManyInput[] = Array.from(
{ length: SEED_CONFIG.swaps },
(_, i) => {
const pool = pools[i % pools.length]; // distribute evenly across pools
const user = users[Math.floor(Math.random() * users.length)];
const poolDef = POOL_DEFINITIONS.find((d) => d.name === pool.name)!;

const swapDate = randomDateInWindow(SEED_CONFIG.daysBack);
const daysElapsed =
(Date.now() - swapDate.getTime()) / (1000 * 60 * 60 * 24);
const currentPrice = priceWalk(poolDef.basePrice, poolDef.priceVolatility, daysElapsed);

// Randomly decide direction: tokenA→tokenB or tokenB→tokenA
const direction = Math.random() > 0.5 ? "A_TO_B" : "B_TO_A";
const effectiveRate = direction === "A_TO_B" ? currentPrice : 1 / currentPrice;

const { amountIn, amountOut, fee, priceImpact } = generateSwapAmounts(effectiveRate);

return {
poolId: pool.id,
userId: user.id,
tokenIn: direction === "A_TO_B" ? pool.tokenA : pool.tokenB,
tokenOut: direction === "A_TO_B" ? pool.tokenB : pool.tokenA,
amountIn,
amountOut,
fee,
priceImpact,
executionPrice: effectiveRate,
status: faker.helpers.weightedArrayElement([
{ weight: 92, value: "COMPLETED" },
{ weight: 5, value: "FAILED" },
{ weight: 3, value: "PENDING" },
]),
txHash: `0x${faker.string.hexadecimal({ length: 64, casing: "lower" }).replace("0x", "")}`,
createdAt: swapDate,
};
}
);

// Batch into chunks of 500 for the single transaction
const CHUNK_SIZE = 500;
const chunks: Prisma.SwapCreateManyInput[][] = [];
for (let i = 0; i < swapData.length; i += CHUNK_SIZE) {
chunks.push(swapData.slice(i, i + CHUNK_SIZE));
}

await prisma.$transaction(
chunks.map((chunk) => prisma.swap.createMany({ data: chunk }))
);

const swapCount = await prisma.swap.count();
console.log(` ✓ ${swapCount} swaps inserted\n`);

// ── 5. Summary ─────────────────────────────────────────────────────────────
console.log("✅ Seed complete!\n");
console.log("📊 Summary:");
console.log(` Users : ${await prisma.user.count()}`);
console.log(` Pools : ${await prisma.pool.count()}`);
console.log(` Swaps : ${await prisma.swap.count()}`);

const byPool = await prisma.swap.groupBy({
by: ["poolId"],
_count: { id: true },
});
for (const entry of byPool) {
const pool = pools.find((p) => p.id === entry.poolId);
console.log(` └─ ${pool?.name ?? entry.poolId}: ${entry._count.id} swaps`);
}
}

main()
.catch((e) => {
console.error("❌ Seed failed:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});