lock trading for 1 minute for creator
This commit is contained in:
parent
c729913db0
commit
6c54afc88d
9 changed files with 2324 additions and 8 deletions
2
website/drizzle/0002_lonely_the_fallen.sql
Normal file
2
website/drizzle/0002_lonely_the_fallen.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE "coin" ADD COLUMN "trading_unlocks_at" timestamp;--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ADD COLUMN "is_locked" boolean DEFAULT true NOT NULL;
|
||||||
2212
website/drizzle/meta/0002_snapshot.json
Normal file
2212
website/drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,13 @@
|
||||||
"when": 1752593600974,
|
"when": 1752593600974,
|
||||||
"tag": "0001_heavy_leo",
|
"tag": "0001_heavy_leo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1752597309305,
|
||||||
|
"tag": "0002_lonely_the_fallen",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +96,8 @@ export const coin = pgTable("coin", {
|
||||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
isListed: boolean("is_listed").default(true).notNull(),
|
isListed: boolean("is_listed").default(true).notNull(),
|
||||||
|
tradingUnlocksAt: timestamp("trading_unlocks_at"),
|
||||||
|
isLocked: boolean("is_locked").default(true).notNull(),
|
||||||
}, (table) => {
|
}, (table) => {
|
||||||
return {
|
return {
|
||||||
symbolIdx: index("coin_symbol_idx").on(table.symbol),
|
symbolIdx: index("coin_symbol_idx").on(table.symbol),
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,9 @@ export async function GET({ params, url }) {
|
||||||
creatorName: user.name,
|
creatorName: user.name,
|
||||||
creatorUsername: user.username,
|
creatorUsername: user.username,
|
||||||
creatorBio: user.bio,
|
creatorBio: user.bio,
|
||||||
creatorImage: user.image
|
creatorImage: user.image,
|
||||||
|
tradingUnlocksAt: coin.tradingUnlocksAt,
|
||||||
|
isLocked: coin.isLocked
|
||||||
})
|
})
|
||||||
.from(coin)
|
.from(coin)
|
||||||
.leftJoin(user, eq(coin.creatorId, user.id))
|
.leftJoin(user, eq(coin.creatorId, user.id))
|
||||||
|
|
@ -185,7 +187,9 @@ export async function GET({ params, url }) {
|
||||||
poolCoinAmount: Number(coinData.poolCoinAmount),
|
poolCoinAmount: Number(coinData.poolCoinAmount),
|
||||||
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount),
|
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount),
|
||||||
circulatingSupply: Number(coinData.circulatingSupply),
|
circulatingSupply: Number(coinData.circulatingSupply),
|
||||||
initialSupply: Number(coinData.initialSupply)
|
initialSupply: Number(coinData.initialSupply),
|
||||||
|
tradingUnlocksAt: coinData.tradingUnlocksAt,
|
||||||
|
isLocked: coinData.isLocked
|
||||||
},
|
},
|
||||||
candlestickData,
|
candlestickData,
|
||||||
volumeData,
|
volumeData,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,20 @@ export async function POST({ params, request }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const [coinData] = await tx.select().from(coin).where(eq(coin.symbol, normalizedSymbol)).for('update').limit(1);
|
const [coinData] = await tx.select({
|
||||||
|
id: coin.id,
|
||||||
|
symbol: coin.symbol,
|
||||||
|
name: coin.name,
|
||||||
|
icon: coin.icon,
|
||||||
|
currentPrice: coin.currentPrice,
|
||||||
|
poolCoinAmount: coin.poolCoinAmount,
|
||||||
|
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
|
||||||
|
circulatingSupply: coin.circulatingSupply,
|
||||||
|
isListed: coin.isListed,
|
||||||
|
creatorId: coin.creatorId,
|
||||||
|
tradingUnlocksAt: coin.tradingUnlocksAt,
|
||||||
|
isLocked: coin.isLocked
|
||||||
|
}).from(coin).where(eq(coin.symbol, normalizedSymbol)).for('update').limit(1);
|
||||||
|
|
||||||
if (!coinData) {
|
if (!coinData) {
|
||||||
throw error(404, 'Coin not found');
|
throw error(404, 'Coin not found');
|
||||||
|
|
@ -50,6 +63,18 @@ export async function POST({ params, request }) {
|
||||||
throw error(400, 'This coin is delisted and cannot be traded');
|
throw error(400, 'This coin is delisted and cannot be traded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coinData.isLocked && coinData.tradingUnlocksAt && userId !== coinData.creatorId) {
|
||||||
|
const unlockTime = new Date(coinData.tradingUnlocksAt);
|
||||||
|
if (new Date() < unlockTime) {
|
||||||
|
const remainingSeconds = Math.ceil((unlockTime.getTime() - Date.now()) / 1000);
|
||||||
|
throw error(400, `Trading is locked. Unlocks in ${remainingSeconds} seconds.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.update(coin)
|
||||||
|
.set({ isLocked: false })
|
||||||
|
.where(eq(coin.id, coinData.id));
|
||||||
|
}
|
||||||
|
|
||||||
const [userData] = await tx.select({
|
const [userData] = await tx.select({
|
||||||
baseCurrencyBalance: user.baseCurrencyBalance,
|
baseCurrencyBalance: user.baseCurrencyBalance,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,9 @@ export async function POST({ request }) {
|
||||||
currentPrice: STARTING_PRICE.toString(),
|
currentPrice: STARTING_PRICE.toString(),
|
||||||
marketCap: (FIXED_SUPPLY * STARTING_PRICE).toString(),
|
marketCap: (FIXED_SUPPLY * STARTING_PRICE).toString(),
|
||||||
poolCoinAmount: FIXED_SUPPLY.toString(),
|
poolCoinAmount: FIXED_SUPPLY.toString(),
|
||||||
poolBaseCurrencyAmount: INITIAL_LIQUIDITY.toString()
|
poolBaseCurrencyAmount: INITIAL_LIQUIDITY.toString(),
|
||||||
|
tradingUnlocksAt: new Date(Date.now() + 60 * 1000), // 1 minute from now
|
||||||
|
isLocked: true
|
||||||
}).returning();
|
}).returning();
|
||||||
|
|
||||||
createdCoin = newCoin;
|
createdCoin = newCoin;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@
|
||||||
let shouldSignIn = $state(false);
|
let shouldSignIn = $state(false);
|
||||||
|
|
||||||
let previousCoinSymbol = $state<string | null>(null);
|
let previousCoinSymbol = $state<string | null>(null);
|
||||||
|
let countdown = $state<number | null>(null);
|
||||||
|
let countdownInterval = $state<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const timeframeOptions = [
|
const timeframeOptions = [
|
||||||
{ value: '1m', label: '1 minute' },
|
{ value: '1m', label: '1 minute' },
|
||||||
|
|
@ -89,6 +91,41 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (coin?.isLocked && coin?.tradingUnlocksAt) {
|
||||||
|
const unlockTime = new Date(coin.tradingUnlocksAt).getTime();
|
||||||
|
|
||||||
|
const updateCountdown = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const remaining = Math.max(0, Math.ceil((unlockTime - now) / 1000));
|
||||||
|
countdown = remaining;
|
||||||
|
|
||||||
|
if (remaining === 0 && countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
if (coin) {
|
||||||
|
coin = { ...coin, isLocked: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCountdown();
|
||||||
|
countdownInterval = setInterval(updateCountdown, 1000);
|
||||||
|
} else {
|
||||||
|
countdown = null;
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function loadCoinData() {
|
async function loadCoinData() {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
@ -356,6 +393,17 @@
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCountdown(seconds: number): string {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCreator = $derived(coin && $USER_DATA && coin.creatorId === Number($USER_DATA.id));
|
||||||
|
let isTradingLocked = $derived(coin?.isLocked && countdown !== null && countdown > 0);
|
||||||
|
let canTrade = $derived(!isTradingLocked || isCreator);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
|
|
@ -419,6 +467,11 @@
|
||||||
● LIVE
|
● LIVE
|
||||||
</Badge>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if isTradingLocked}
|
||||||
|
<Badge variant="secondary" class="text-xs">
|
||||||
|
🔒 LOCKED {countdown !== null ? formatCountdown(countdown) : ''}
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
{#if !coin.isListed}
|
{#if !coin.isListed}
|
||||||
<Badge variant="destructive">Delisted</Badge>
|
<Badge variant="destructive">Delisted</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -529,6 +582,15 @@
|
||||||
{coin.symbol}
|
{coin.symbol}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if isTradingLocked}
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
{#if isCreator}
|
||||||
|
🔒 Creator-only period: {countdown !== null ? formatCountdown(countdown) : ''} remaining
|
||||||
|
{:else}
|
||||||
|
🔒 Trading unlocks in: {countdown !== null ? formatCountdown(countdown) : ''}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{#if $USER_DATA}
|
{#if $USER_DATA}
|
||||||
|
|
@ -538,7 +600,7 @@
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
onclick={() => (buyModalOpen = true)}
|
onclick={() => (buyModalOpen = true)}
|
||||||
disabled={!coin.isListed}
|
disabled={!coin.isListed || !canTrade}
|
||||||
>
|
>
|
||||||
<TrendingUp class="h-4 w-4" />
|
<TrendingUp class="h-4 w-4" />
|
||||||
Buy {coin.symbol}
|
Buy {coin.symbol}
|
||||||
|
|
@ -548,7 +610,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
onclick={() => (sellModalOpen = true)}
|
onclick={() => (sellModalOpen = true)}
|
||||||
disabled={!coin.isListed || userHolding <= 0}
|
disabled={!coin.isListed || userHolding <= 0 || !canTrade}
|
||||||
>
|
>
|
||||||
<TrendingDown class="h-4 w-4" />
|
<TrendingDown class="h-4 w-4" />
|
||||||
Sell {coin.symbol}
|
Sell {coin.symbol}
|
||||||
|
|
|
||||||
|
|
@ -238,9 +238,9 @@
|
||||||
<p>• Starting Price: <span class="font-medium">$0.000001 per token</span></p>
|
<p>• Starting Price: <span class="font-medium">$0.000001 per token</span></p>
|
||||||
<p>• You receive <span class="font-medium">100%</span> of the supply</p>
|
<p>• You receive <span class="font-medium">100%</span> of the supply</p>
|
||||||
<p>• Initial Market Cap: <span class="font-medium">$1,000</span></p>
|
<p>• Initial Market Cap: <span class="font-medium">$1,000</span></p>
|
||||||
|
<p>• Trading Lock: <span class="font-medium">1 minute creator-only period</span></p>
|
||||||
<p class="mt-2 text-sm">
|
<p class="mt-2 text-sm">
|
||||||
These settings ensure a fair start for all traders. The price will increase
|
After creation, you'll have 1 minute of exclusive trading time before others can trade. This allows you to purchase your initial supply.
|
||||||
naturally as people buy tokens.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|
|
||||||
Reference in a new issue