Compare commits
15 commits
5dd008b8cc
...
dfaf3141a0
| Author | SHA1 | Date | |
|---|---|---|---|
| dfaf3141a0 | |||
|
|
6c54afc88d | ||
|
|
c729913db0 | ||
|
|
398935a0af | ||
|
|
6956149a3d | ||
|
|
edca4d5ceb | ||
|
|
32bfbf816c | ||
|
|
0f13b74ddf | ||
|
|
833f4e8e68 | ||
|
|
2501648d31 | ||
|
|
159d371655 | ||
|
|
d9d85c21b2 | ||
|
|
00d25c7808 | ||
|
|
b93e4957b4 | ||
|
|
3081adc945 |
42 changed files with 4979 additions and 315 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
.github
|
.github
|
||||||
review.js
|
review.js
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
|
|
||||||
1
website/drizzle/0001_heavy_leo.sql
Normal file
1
website/drizzle/0001_heavy_leo.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "notification" ADD COLUMN "link" text;
|
||||||
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;
|
||||||
2199
website/drizzle/meta/0001_snapshot.json
Normal file
2199
website/drizzle/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
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
|
|
@ -8,6 +8,20 @@
|
||||||
"when": 1750863600119,
|
"when": 1750863600119,
|
||||||
"tag": "0000_chief_korath",
|
"tag": "0000_chief_korath",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1752593600974,
|
||||||
|
"tag": "0001_heavy_leo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1752597309305,
|
||||||
|
"tag": "0002_lonely_the_fallen",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
4
website/package-lock.json
generated
4
website/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0.a0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0.a0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.815.0",
|
"@aws-sdk/client-s3": "^3.815.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.815.0",
|
"@aws-sdk/s3-request-presigner": "^3.815.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "2.0.0.a0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|
@ -16,53 +16,54 @@
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@internationalized/date": "^3.8.1",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@lucide/svelte": "^0.482.0",
|
"@lucide/svelte": "^0.539.0",
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^6.1.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.14",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.29.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.1.2",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^24.2.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.21",
|
||||||
"bits-ui": "^2.5.0",
|
"bits-ui": "^2.9.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-kit": "^0.22.0",
|
"dotenv": "^17.2.1",
|
||||||
"prettier": "^3.3.2",
|
"drizzle-kit": "^0.31.4",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"svelte": "^5.0.0",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte": "^5.38.1",
|
||||||
"svelte-sonner": "^1.0.2",
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"tailwind-merge": "^3.0.2",
|
"svelte-check": "^4.3.1",
|
||||||
"tailwind-variants": "^0.2.1",
|
"svelte-confetti": "^2.3.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"svelte-lightweight-charts": "^2.2.0",
|
||||||
"tw-animate-css": "^1.3.0",
|
"svelte-sonner": "^1.0.5",
|
||||||
"typescript": "^5.0.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"vite": "^5.4.11",
|
"tailwind-variants": "^2.1.0",
|
||||||
"vite-plugin-iso-import": "^1.2.0"
|
"tailwindcss": "^4.1.11",
|
||||||
|
"tw-animate-css": "^1.3.6",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vite-plugin-iso-import": "^1.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.815.0",
|
"@aws-sdk/client-s3": "^3.864.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.815.0",
|
"@aws-sdk/s3-request-presigner": "^3.864.0",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@visx/scale": "^3.12.0",
|
"@visx/scale": "^3.12.0",
|
||||||
"apexcharts": "^4.7.0",
|
"apexcharts": "^5.3.3",
|
||||||
"better-auth": "^1.2.8",
|
"better-auth": "^1.3.6",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.44.4",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"lightweight-charts": "^5.0.7",
|
"lightweight-charts": "^5.0.8",
|
||||||
"lucide-svelte": "^0.511.0",
|
"lucide-svelte": "^0.539.0",
|
||||||
"mode-watcher": "^1.0.7",
|
"mode-watcher": "^1.1.0",
|
||||||
"openai": "^4.103.0",
|
"openai": "^5.12.2",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.7",
|
||||||
"redis": "^5.1.0",
|
"redis": "^5.8.1",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.3"
|
||||||
"svelte-apexcharts": "^1.0.2",
|
|
||||||
"svelte-confetti": "^2.3.1",
|
|
||||||
"svelte-lightweight-charts": "^2.2.0"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "*",
|
"@rollup/rollup-linux-x64-gnu": "*",
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="%sveltekit.assets%/android-chrome-192x192.png" />
|
<link rel="icon" type="image/png" sizes="192x192" href="%sveltekit.assets%/android-chrome-192x192.png" />
|
||||||
|
|
||||||
<title>Rugplay</title>
|
<title>CoinStorge</title>
|
||||||
<!-- Global Meta Tags -->
|
<!-- Global Meta Tags -->
|
||||||
<meta name="application-name" content="Rugplay" />
|
<meta name="application-name" content="CoinStorge" />
|
||||||
<meta name="theme-color" content="#fb2c36" />
|
<meta name="theme-color" content="#fb2c36" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Rugplay" />
|
<meta name="apple-mobile-web-app-title" content="CoinStorge" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
|
@ -23,12 +23,10 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
<script defer data-domain="rugplay.com"
|
|
||||||
src="https://analytics.outpoot.com/js/script.outbound-links.pageview-props.tagged-events.js"></script>
|
|
||||||
<script>window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
<script>window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
|
|
||||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7420543404967748"
|
<!-- here go ads -->
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@ if (!publicEnv.PUBLIC_BETTER_AUTH_URL) throw new Error('PUBLIC_BETTER_AUTH_URL i
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
baseURL: publicEnv.PUBLIC_BETTER_AUTH_URL,
|
baseURL: publicEnv.PUBLIC_BETTER_AUTH_URL,
|
||||||
secret: privateEnv.PRIVATE_BETTER_AUTH_SECRET,
|
secret: privateEnv.PRIVATE_BETTER_AUTH_SECRET,
|
||||||
appName: "Rugplay",
|
appName: "CoinStorge",
|
||||||
|
|
||||||
trustedOrigins: [
|
trustedOrigins: [
|
||||||
publicEnv.PUBLIC_BETTER_AUTH_URL,
|
publicEnv.PUBLIC_BETTER_AUTH_URL,
|
||||||
"http://rugplay.com",
|
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
<div class="flex items-center gap-2 px-2 py-2">
|
<div class="flex items-center gap-2 px-2 py-2">
|
||||||
<img src="/rugplay.svg" class="h-5 w-5" alt="twoblade" />
|
<img src="/rugplay.svg" class="h-5 w-5" alt="twoblade" />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-base font-semibold">Rugplay</span>
|
<span class="text-base font-semibold">CoinStorge</span>
|
||||||
{#if $USER_DATA?.isAdmin}
|
{#if $USER_DATA?.isAdmin}
|
||||||
<span class="text-muted-foreground text-xs">| Admin</span>
|
<span class="text-muted-foreground text-xs">| Admin</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@
|
||||||
{:else if cellData.type === 'coin'}
|
{:else if cellData.type === 'coin'}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CoinIcon icon={cellData.icon} symbol={cellData.symbol} size={cellData.size} />
|
<CoinIcon icon={cellData.icon} symbol={cellData.symbol} size={cellData.size} />
|
||||||
<span class="font-medium">{cellData.name}</span>
|
<span class="font-medium max-w-44 truncate">{cellData.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if cellData.type === 'rank'}
|
{:else if cellData.type === 'rank'}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
title = 'Rugplay',
|
title = 'CoinStorge',
|
||||||
description = 'Experience realistic cryptocurrency trading simulation game with AI-powered markets, rug pull mechanics, and virtual currencies. Learn crypto trading without financial risk in this educational game.',
|
description = 'Experience realistic cryptocurrency trading simulation game with AI-powered markets, rug pull mechanics, and virtual currencies. Learn crypto trading without financial risk in this educational game.',
|
||||||
type = 'website',
|
type = 'website',
|
||||||
image = '/apple-touch-icon.png',
|
image = '/apple-touch-icon.png',
|
||||||
imageAlt = 'Rugplay Logo',
|
imageAlt = 'CoinStorge Logo',
|
||||||
keywords = '',
|
keywords = '',
|
||||||
author = 'Outpoot',
|
author = 'Outpoot (with modifications)',
|
||||||
canonicalUrl = '',
|
canonicalUrl = '',
|
||||||
noindex = false,
|
noindex = false,
|
||||||
twitterCard = 'summary_large_image'
|
twitterCard = 'summary_large_image'
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
let canonical = $derived(canonicalUrl || currentUrl);
|
let canonical = $derived(canonicalUrl || currentUrl);
|
||||||
|
|
||||||
let fullImageUrl = $derived(
|
let fullImageUrl = $derived(
|
||||||
image?.startsWith('http') ? image : `${$page?.url?.origin || 'https://rugplay.com'}${image}`
|
image?.startsWith('http') ? image : `${$page?.url?.origin || 'https://localhost'}${image}`
|
||||||
);
|
);
|
||||||
|
|
||||||
let defaultKeywords =
|
let defaultKeywords =
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
<meta property="og:url" content={currentUrl} />
|
<meta property="og:url" content={currentUrl} />
|
||||||
<meta property="og:image" content={fullImageUrl} />
|
<meta property="og:image" content={fullImageUrl} />
|
||||||
<meta property="og:image:alt" content={imageAlt} />
|
<meta property="og:image:alt" content={imageAlt} />
|
||||||
<meta property="og:site_name" content="Rugplay" />
|
<meta property="og:site_name" content="CoinStorge" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
|
|
||||||
<!-- Twitter Card Meta Tags -->
|
<!-- Twitter Card Meta Tags -->
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<Dialog bind:open>
|
<Dialog bind:open>
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Sign in to Rugplay</DialogTitle>
|
<DialogTitle>Sign in to CoinStorge</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Choose a service to sign in with. Your account will be created automatically if you don't
|
Choose a service to sign in with. Your account will be created automatically if you don't
|
||||||
have one.
|
have one.
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,9 @@
|
||||||
const tips: Tip[] = [
|
const tips: Tip[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Welcome to Rugplay!',
|
title: 'Welcome to CoinStorge!',
|
||||||
description:
|
description:
|
||||||
'Rugplay is a cryptocurrency trading simulator where you can practice trading without real financial risk. Start with virtual money, create coins, bet on prediction markets, and most importantly, rugpull!',
|
'CoinStorge is a cryptocurrency trading simulator where you can practice trading without real financial risk. Start with virtual money, create coins, bet on prediction markets, and most importantly, rugpull!',
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
image: '/tips/cover.avif'
|
image: '/tips/cover.avif'
|
||||||
},
|
},
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
id: 4,
|
id: 4,
|
||||||
title: 'AMM - Automated Market Maker',
|
title: 'AMM - Automated Market Maker',
|
||||||
description:
|
description:
|
||||||
'Rugplay uses an AMM system where prices are calculated automatically based on supply and demand. The more you buy, the higher the price goes. The more you sell, the lower it drops. Large trades create "slippage" - the price change during your trade.',
|
'CoinStorge, like Rugplay, uses an AMM system where prices are calculated automatically based on supply and demand. The more you buy, the higher the price goes. The more you sell, the lower it drops. Large trades create "slippage" - the price change during your trade.',
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
image: '/tips/amm.avif'
|
image: '/tips/amm.avif'
|
||||||
},
|
},
|
||||||
|
|
@ -205,11 +205,11 @@
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
{#each tips as tip, index}
|
{#each tips as tip, index}
|
||||||
<button
|
<button
|
||||||
|
aria-label={`Go to page ${index + 1}`}
|
||||||
onclick={() => goToPage(index)}
|
onclick={() => goToPage(index)}
|
||||||
class="h-2 w-2 rounded-full transition-colors {index === currentPage
|
class="h-2 w-2 rounded-full transition-colors {index === currentPage
|
||||||
? 'bg-primary'
|
? 'bg-primary'
|
||||||
: 'bg-muted-foreground/30 hover:bg-muted-foreground/50'}"
|
: 'bg-muted-foreground/30 hover:bg-muted-foreground/50'}"
|
||||||
aria-label={`Go to tip ${index + 1}: ${tip.title}`}
|
|
||||||
aria-current={index === currentPage ? 'page' : undefined}
|
aria-current={index === currentPage ? 'page' : undefined}
|
||||||
></button>
|
></button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { zodResponseFormat } from 'openai/helpers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { OPENROUTER_API_KEY } from '$env/static/private';
|
import { OPENROUTER_API_KEY } from '$env/static/private';
|
||||||
import { db } from './db';
|
import { db } from './db';
|
||||||
import { coin, user, transaction } from './db/schema';
|
import { coin, user, transaction, priceHistory } from './db/schema';
|
||||||
import { eq, desc, sql, gte } from 'drizzle-orm';
|
import { eq, desc, sql, gte } from 'drizzle-orm';
|
||||||
|
|
||||||
if (!OPENROUTER_API_KEY) {
|
if (!OPENROUTER_API_KEY) {
|
||||||
|
|
@ -88,7 +88,14 @@ async function getCoinData(coinSymbol: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get recent trading activity for this coin
|
const [priceStats] = await db
|
||||||
|
.select({
|
||||||
|
maxPrice: sql<number>`MAX(CAST(${priceHistory.price} AS NUMERIC))`,
|
||||||
|
minPrice: sql<number>`MIN(CAST(${priceHistory.price} AS NUMERIC))`,
|
||||||
|
})
|
||||||
|
.from(priceHistory)
|
||||||
|
.where(eq(priceHistory.coinId, coinData.id));
|
||||||
|
|
||||||
const recentTrades = await db
|
const recentTrades = await db
|
||||||
.select({
|
.select({
|
||||||
type: transaction.type,
|
type: transaction.type,
|
||||||
|
|
@ -113,6 +120,10 @@ async function getCoinData(coinSymbol: string) {
|
||||||
poolCoinAmount: Number(coinData.poolCoinAmount),
|
poolCoinAmount: Number(coinData.poolCoinAmount),
|
||||||
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount),
|
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount),
|
||||||
circulatingSupply: Number(coinData.circulatingSupply),
|
circulatingSupply: Number(coinData.circulatingSupply),
|
||||||
|
pricing: {
|
||||||
|
peak: Number(priceStats?.maxPrice || 0),
|
||||||
|
lowest: Number(priceStats?.minPrice || 0),
|
||||||
|
},
|
||||||
recentTrades: recentTrades.map(trade => ({
|
recentTrades: recentTrades.map(trade => ({
|
||||||
...trade,
|
...trade,
|
||||||
quantity: Number(trade.quantity),
|
quantity: Number(trade.quantity),
|
||||||
|
|
@ -234,11 +245,11 @@ export async function validateQuestion(question: string, description?: string):
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
You are evaluating whether a prediction market question is valid and answerable for Rugplay, a cryptocurrency trading simulation platform.
|
You are evaluating whether a prediction market question is valid and answerable for CoinStorge, a cryptocurrency trading simulation platform.
|
||||||
|
|
||||||
Question: "${question}"
|
Question: "${question}"
|
||||||
|
|
||||||
Current Rugplay Market Context:
|
Current CoinStorge Market Context:
|
||||||
- Platform currency: $ (or *BUSS)
|
- Platform currency: $ (or *BUSS)
|
||||||
- Total listed coins: ${marketOverview?.marketStats.totalCoins || 0}
|
- Total listed coins: ${marketOverview?.marketStats.totalCoins || 0}
|
||||||
- Total market cap: $${marketOverview?.marketStats.totalMarketCap.toFixed(2) || '0'}
|
- Total market cap: $${marketOverview?.marketStats.totalMarketCap.toFixed(2) || '0'}
|
||||||
|
|
@ -260,8 +271,8 @@ Determine the optimal resolution date based on the question type:
|
||||||
- If the question explicitly states the date, use that as the resolution date
|
- If the question explicitly states the date, use that as the resolution date
|
||||||
|
|
||||||
Also determine:
|
Also determine:
|
||||||
- Whether this question requires web search (external events, real-world data, non-Rugplay information)
|
- Whether this question requires web search (external events, real-world data, non-CoinStorge information)
|
||||||
- If the question is related to the Rugplay market, and contains what appears to be a coin name, ensure it's properly formatted (e.g. *BTC, *DOGE). Invalid question example: "will BTC reach $100,000 in 1 hour?" (invalid coin format, should be *BTC).
|
- If the question is related to the CoinStorge market, and contains what appears to be a coin name, ensure it's properly formatted (e.g. *BTC, *DOGE). Invalid question example: "will BTC reach $100,000 in 1 hour?" (invalid coin format, should be *BTC).
|
||||||
- Provide a specific resolution date with time (suggest times between 12:00-20:00 UTC for good global coverage) The current date and time is ${new Date().toISOString()}.
|
- Provide a specific resolution date with time (suggest times between 12:00-20:00 UTC for good global coverage) The current date and time is ${new Date().toISOString()}.
|
||||||
|
|
||||||
Note: All coins use *SYMBOL format (e.g., *BTC, *DOGE). All trading is simulated with *BUSS currency.
|
Note: All coins use *SYMBOL format (e.g., *BTC, *DOGE). All trading is simulated with *BUSS currency.
|
||||||
|
|
@ -325,11 +336,11 @@ export async function resolveQuestion(
|
||||||
const rugplayData = customRugplayData || await getRugplayData(question);
|
const rugplayData = customRugplayData || await getRugplayData(question);
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
You are resolving a prediction market question with a definitive YES or NO answer for Rugplay.
|
You are resolving a prediction market question with a definitive YES or NO answer for CoinStorge.
|
||||||
|
|
||||||
Question: "${question}"
|
Question: "${question}"
|
||||||
|
|
||||||
Current Rugplay Platform Data:
|
Current CoinStorge Platform Data:
|
||||||
${rugplayData}
|
${rugplayData}
|
||||||
|
|
||||||
Instructions:
|
Instructions:
|
||||||
|
|
@ -337,10 +348,10 @@ Instructions:
|
||||||
2. Give your confidence level (0-100) in this resolution
|
2. Give your confidence level (0-100) in this resolution
|
||||||
3. Provide clear reasoning for your decision with specific data references
|
3. Provide clear reasoning for your decision with specific data references
|
||||||
4. For coin-specific questions that mention non-existent coins, answer NO (the coin doesn't exist, so it can't reach any price)
|
4. For coin-specific questions that mention non-existent coins, answer NO (the coin doesn't exist, so it can't reach any price)
|
||||||
5. For coin-specific questions about existing coins, reference actual market data from Rugplay
|
5. For coin-specific questions about existing coins, reference actual market data from CoinStorge
|
||||||
6. For external events, use web search if enabled
|
6. For external events, use web search if enabled
|
||||||
|
|
||||||
Context about Rugplay:
|
Context about CoinStorge:
|
||||||
- Cryptocurrency trading simulation platform with fake money (*BUSS)
|
- Cryptocurrency trading simulation platform with fake money (*BUSS)
|
||||||
- All coins use *SYMBOL format (e.g., *BTC, *DOGE, *SHIB)
|
- All coins use *SYMBOL format (e.g., *BTC, *DOGE, *SHIB)
|
||||||
- Features AMM liquidity pools, rug pull mechanics, and real market dynamics
|
- Features AMM liquidity pools, rug pull mechanics, and real market dynamics
|
||||||
|
|
@ -410,7 +421,7 @@ export async function getRugplayData(question?: string): Promise<string> {
|
||||||
coinSpecificData = '\n\nCoin Analysis for Question:';
|
coinSpecificData = '\n\nCoin Analysis for Question:';
|
||||||
|
|
||||||
if (nonExistentCoins.length > 0) {
|
if (nonExistentCoins.length > 0) {
|
||||||
coinSpecificData += `\nNON-EXISTENT COINS: ${nonExistentCoins.map(symbol => `*${symbol}`).join(', ')} - These coins do not exist on the Rugplay platform`;
|
coinSpecificData += `\nNON-EXISTENT COINS: ${nonExistentCoins.map(symbol => `*${symbol}`).join(', ')} - These coins do not exist on the CoinStorge platform`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingCoins.length > 0) {
|
if (existingCoins.length > 0) {
|
||||||
|
|
@ -419,6 +430,8 @@ export async function getRugplayData(question?: string): Promise<string> {
|
||||||
return `
|
return `
|
||||||
*${coin.symbol} (${coin.name}):
|
*${coin.symbol} (${coin.name}):
|
||||||
- Current Price: $${coin.currentPrice.toFixed(8)}
|
- Current Price: $${coin.currentPrice.toFixed(8)}
|
||||||
|
- Peak Price: $${coin.pricing.peak.toFixed(8)}
|
||||||
|
- Lowest Price: $${coin.pricing.lowest.toFixed(8)}
|
||||||
- Market Cap: $${coin.marketCap.toFixed(2)}
|
- Market Cap: $${coin.marketCap.toFixed(2)}
|
||||||
- 24h Change: ${coin.change24h >= 0 ? '+' : ''}${coin.change24h.toFixed(2)}%
|
- 24h Change: ${coin.change24h >= 0 ? '+' : ''}${coin.change24h.toFixed(2)}%
|
||||||
- 24h Volume: $${coin.volume24h.toFixed(2)}
|
- 24h Volume: $${coin.volume24h.toFixed(2)}
|
||||||
|
|
@ -437,7 +450,7 @@ ${coin.recentTrades.slice(0, 3).map(trade =>
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Current Timestamp: ${new Date().toISOString()}
|
Current Timestamp: ${new Date().toISOString()}
|
||||||
Platform: Rugplay - Cryptocurrency Trading Simulation
|
Platform: CoinStorge - Cryptocurrency Trading Simulation
|
||||||
|
|
||||||
Market Overview:
|
Market Overview:
|
||||||
- Total Listed Coins: ${marketOverview?.marketStats.totalCoins || 0}
|
- Total Listed Coins: ${marketOverview?.marketStats.totalCoins || 0}
|
||||||
|
|
@ -460,7 +473,7 @@ Platform Details:
|
||||||
- Coins use *SYMBOL format (e.g., *BTC, *DOGE, *SHIB)${coinSpecificData}
|
- Coins use *SYMBOL format (e.g., *BTC, *DOGE, *SHIB)${coinSpecificData}
|
||||||
`;
|
`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating Rugplay data:', error);
|
console.error('Error generating CoinStorge data:', error);
|
||||||
return `Couldn't retrieve data, please try again later.`;
|
return `Couldn't retrieve data, please try again later.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
142
website/src/lib/server/amm.ts
Normal file
142
website/src/lib/server/amm.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { coin, transaction, priceHistory, userPortfolio } from '$lib/server/db/schema';
|
||||||
|
import { eq, and, gte } from 'drizzle-orm';
|
||||||
|
import { createNotification } from '$lib/server/notification';
|
||||||
|
|
||||||
|
export async function calculate24hMetrics(coinId: number, currentPrice: number) {
|
||||||
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const [priceData] = await db
|
||||||
|
.select({ price: priceHistory.price })
|
||||||
|
.from(priceHistory)
|
||||||
|
.where(and(
|
||||||
|
eq(priceHistory.coinId, coinId),
|
||||||
|
gte(priceHistory.timestamp, twentyFourHoursAgo)
|
||||||
|
))
|
||||||
|
.orderBy(priceHistory.timestamp)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let change24h = 0;
|
||||||
|
if (priceData) {
|
||||||
|
const priceFrom24hAgo = Number(priceData.price);
|
||||||
|
if (priceFrom24hAgo > 0) {
|
||||||
|
change24h = ((currentPrice - priceFrom24hAgo) / priceFrom24hAgo) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeData = await db
|
||||||
|
.select({ totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount })
|
||||||
|
.from(transaction)
|
||||||
|
.where(and(
|
||||||
|
eq(transaction.coinId, coinId),
|
||||||
|
gte(transaction.timestamp, twentyFourHoursAgo)
|
||||||
|
));
|
||||||
|
|
||||||
|
const volume24h = volumeData.reduce((sum, tx) => sum + Number(tx.totalBaseCurrencyAmount), 0);
|
||||||
|
|
||||||
|
return { change24h: Number(change24h.toFixed(4)), volume24h: Number(volume24h.toFixed(4)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeSellTrade(
|
||||||
|
tx: any,
|
||||||
|
coinData: any,
|
||||||
|
userId: number,
|
||||||
|
quantity: number
|
||||||
|
) {
|
||||||
|
const poolCoinAmount = Number(coinData.poolCoinAmount);
|
||||||
|
const poolBaseCurrencyAmount = Number(coinData.poolBaseCurrencyAmount);
|
||||||
|
const currentPrice = Number(coinData.currentPrice);
|
||||||
|
|
||||||
|
if (poolCoinAmount <= 0 || poolBaseCurrencyAmount <= 0) {
|
||||||
|
throw new Error('Liquidity pool is not properly initialized or is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||||
|
const newPoolCoin = poolCoinAmount + quantity;
|
||||||
|
const newPoolBaseCurrency = k / newPoolCoin;
|
||||||
|
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
|
||||||
|
const newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||||
|
|
||||||
|
if (baseCurrencyReceived <= 0 || newPoolBaseCurrency < 1) {
|
||||||
|
const fallbackValue = quantity * currentPrice;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
fallbackValue,
|
||||||
|
newPrice: currentPrice,
|
||||||
|
priceImpact: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||||
|
|
||||||
|
await tx.insert(transaction).values({
|
||||||
|
userId,
|
||||||
|
coinId: coinData.id,
|
||||||
|
type: 'SELL',
|
||||||
|
quantity: quantity.toString(),
|
||||||
|
pricePerCoin: (baseCurrencyReceived / quantity).toString(),
|
||||||
|
totalBaseCurrencyAmount: baseCurrencyReceived.toString(),
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.insert(priceHistory).values({
|
||||||
|
coinId: coinData.id,
|
||||||
|
price: newPrice.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
||||||
|
|
||||||
|
await tx.update(coin)
|
||||||
|
.set({
|
||||||
|
currentPrice: newPrice.toString(),
|
||||||
|
marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(),
|
||||||
|
poolCoinAmount: newPoolCoin.toString(),
|
||||||
|
poolBaseCurrencyAmount: newPoolBaseCurrency.toString(),
|
||||||
|
change24h: metrics.change24h.toString(),
|
||||||
|
volume24h: metrics.volume24h.toString(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(coin.id, coinData.id));
|
||||||
|
|
||||||
|
const isRugPull = priceImpact < -20 && baseCurrencyReceived > 1000;
|
||||||
|
if (isRugPull) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const affectedUsers = await db
|
||||||
|
.select({
|
||||||
|
userId: userPortfolio.userId,
|
||||||
|
quantity: userPortfolio.quantity
|
||||||
|
})
|
||||||
|
.from(userPortfolio)
|
||||||
|
.where(eq(userPortfolio.coinId, coinData.id));
|
||||||
|
|
||||||
|
for (const holder of affectedUsers) {
|
||||||
|
if (holder.userId === userId) continue;
|
||||||
|
|
||||||
|
const holdingValue = Number(holder.quantity) * newPrice;
|
||||||
|
if (holdingValue > 10) {
|
||||||
|
await createNotification(
|
||||||
|
holder.userId.toString(),
|
||||||
|
'RUG_PULL',
|
||||||
|
'Coin rugpulled!',
|
||||||
|
`A coin you owned, ${coinData.name} (*${coinData.symbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`,
|
||||||
|
`/coin/${coinData.symbol}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending rug pull notifications:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
baseCurrencyReceived,
|
||||||
|
newPrice,
|
||||||
|
priceImpact,
|
||||||
|
newPoolCoin,
|
||||||
|
newPoolBaseCurrency,
|
||||||
|
metrics
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
@ -265,6 +267,7 @@ export const notifications = pgTable("notification", {
|
||||||
type: notificationTypeEnum("type").notNull(),
|
type: notificationTypeEnum("type").notNull(),
|
||||||
title: varchar("title", { length: 200 }).notNull(),
|
title: varchar("title", { length: 200 }).notNull(),
|
||||||
message: text("message").notNull(),
|
message: text("message").notNull(),
|
||||||
|
link: text("link"),
|
||||||
isRead: boolean("is_read").notNull().default(false),
|
isRead: boolean("is_read").notNull().default(false),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
}, (table) => {
|
}, (table) => {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export async function resolveExpiredQuestions() {
|
||||||
'HOPIUM',
|
'HOPIUM',
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
`/hopium/${question.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -219,6 +220,7 @@ export async function resolveExpiredQuestions() {
|
||||||
'HOPIUM',
|
'HOPIUM',
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
`/hopium/${question.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ export async function createNotification(
|
||||||
type: NotificationType,
|
type: NotificationType,
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
|
link?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db.insert(notifications).values({
|
await db.insert(notifications).values({
|
||||||
userId: parseInt(userId),
|
userId: parseInt(userId),
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
message
|
message,
|
||||||
|
link
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -27,6 +29,7 @@ export async function createNotification(
|
||||||
notificationType: type,
|
notificationType: type,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
link
|
||||||
};
|
};
|
||||||
|
|
||||||
await redis.publish(channel, JSON.stringify(payload));
|
await redis.publish(channel, JSON.stringify(payload));
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { writable, derived } from 'svelte/store';
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: 'HOPIUM' | 'TRANSFER' | 'RUG_PULL' | 'SYSTEM';
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
link?: string;
|
||||||
data: any;
|
data: any;
|
||||||
isRead: boolean;
|
isRead: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
@ -23,10 +24,10 @@ export async function fetchNotifications(unreadOnly = false) {
|
||||||
if (!response.ok) throw new Error('Failed to fetch notifications');
|
if (!response.ok) throw new Error('Failed to fetch notifications');
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
NOTIFICATIONS.set(data.notifications);
|
NOTIFICATIONS.set(data.notifications);
|
||||||
UNREAD_COUNT.set(data.unreadCount);
|
UNREAD_COUNT.set(data.unreadCount);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch notifications:', error);
|
console.error('Failed to fetch notifications:', error);
|
||||||
|
|
@ -34,23 +35,21 @@ export async function fetchNotifications(unreadOnly = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markNotificationsAsRead(ids: number[]) {
|
export async function markNotificationsAsRead() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/notifications', {
|
const response = await fetch('/api/notifications', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ids, markAsRead: true })
|
body: JSON.stringify({ markAsRead: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to mark notifications as read');
|
if (!response.ok) throw new Error('Failed to mark notifications as read');
|
||||||
|
|
||||||
NOTIFICATIONS.update(notifications =>
|
NOTIFICATIONS.update(notifications =>
|
||||||
notifications.map(notif =>
|
notifications.map(notif => ({ ...notif, isRead: true }))
|
||||||
ids.includes(notif.id) ? { ...notif, isRead: true } : notif
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
UNREAD_COUNT.update(count => Math.max(0, count - ids.length));
|
UNREAD_COUNT.set(0);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to mark notifications as read:', error);
|
console.error('Failed to mark notifications as read:', error);
|
||||||
|
|
@ -58,4 +57,4 @@ export async function markNotificationsAsRead(ids: number[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasUnreadNotifications = derived(UNREAD_COUNT, count => count > 0);
|
export const hasUnreadNotifications = derived(UNREAD_COUNT, count => count > 0);
|
||||||
|
|
@ -197,6 +197,7 @@ function handleWebSocketMessage(event: MessageEvent): void {
|
||||||
type: message.notificationType,
|
type: message.notificationType,
|
||||||
title: message.title,
|
title: message.title,
|
||||||
message: message.message,
|
message: message.message,
|
||||||
|
link: message.link,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
createdAt: message.timestamp,
|
createdAt: message.timestamp,
|
||||||
data: message.amount ? { amount: message.amount } : null
|
data: message.amount ? { amount: message.amount } : null
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export function formatRelativeTime(timestamp: string | Date): string {
|
||||||
|
|
||||||
if (hours < 24) {
|
if (hours < 24) {
|
||||||
const extraMinutes = minutes % 60;
|
const extraMinutes = minutes % 60;
|
||||||
return extraMinutes === 0 ? `${hours}hr` : `${hours}hr ${extraMinutes}m`;
|
return extraMinutes === 0 ? `${hours}h` : `${hours}h ${extraMinutes}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (days < 7) return `${days}d`;
|
if (days < 7) return `${days}d`;
|
||||||
|
|
@ -145,11 +145,11 @@ export function formatRelativeTime(timestamp: string | Date): string {
|
||||||
tempDate.setMonth(tempDate.getMonth() + adjustedMonths);
|
tempDate.setMonth(tempDate.getMonth() + adjustedMonths);
|
||||||
const remainingDays = Math.floor((now.getTime() - tempDate.getTime()) / (1000 * 60 * 60 * 24));
|
const remainingDays = Math.floor((now.getTime() - tempDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
const weeks = Math.floor(remainingDays / 7);
|
const weeks = Math.floor(remainingDays / 7);
|
||||||
return weeks === 0 ? `${adjustedMonths}m` : `${adjustedMonths}m ${weeks}w`;
|
return weeks === 0 ? `${adjustedMonths}mo` : `${adjustedMonths}mo ${weeks}w`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingMonths = adjustedMonths % 12;
|
const remainingMonths = adjustedMonths % 12;
|
||||||
return remainingMonths === 0 ? `${years}y` : `${years}y ${remainingMonths}m`;
|
return remainingMonths === 0 ? `${years}y` : `${years}y ${remainingMonths}mo`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTimeAgo(date: string) {
|
export function formatTimeAgo(date: string) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{status} | Rugplay</title>
|
<title>{status} | CoinStorge</title>
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Rugplay"
|
title="CoinStorge"
|
||||||
description="A realistic crypto trading simulator that lets you experience the risks and mechanics of decentralized exchanges without real financial consequences. Create coins, trade with liquidity pools, and learn about 'rug pulls' in a... relatively safe environment :)"
|
description="A realistic crypto trading simulator that lets you experience the risks and mechanics of decentralized exchanges without real financial consequences. Create coins, trade with liquidity pools, and learn about 'rug pulls' in a... relatively safe environment :)"
|
||||||
keywords="crypto simulation game, trading practice game, rug pull simulation, virtual cryptocurrency game"
|
keywords="crypto simulation game, trading practice game, rug pull simulation, virtual cryptocurrency game"
|
||||||
/>
|
/>
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="mb-2 truncate text-3xl font-bold">
|
<h1 class="mb-2 truncate text-3xl font-bold">
|
||||||
{$USER_DATA ? getTimeBasedGreeting($USER_DATA?.name) : 'Welcome to Rugplay!'}
|
{$USER_DATA ? getTimeBasedGreeting($USER_DATA?.name) : 'Welcome to CoinStorge!'}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-muted-foreground">
|
<p class="text-muted-foreground">
|
||||||
{#if $USER_DATA}
|
{#if $USER_DATA}
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,18 @@
|
||||||
<UserManualModal bind:open={showUserManual} />
|
<UserManualModal bind:open={showUserManual} />
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>About - Rugplay</title>
|
<title>About - CoinStorge</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Learn about Rugplay - a realistic cryptocurrency trading simulation focusing on DeFi risks and mechanics."
|
content="Learn about CoinStorge - a realistic cryptocurrency trading simulation focusing on DeFi risks and mechanics."
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container mx-auto space-y-8 px-4 py-8">
|
<div class="container mx-auto space-y-8 px-4 py-8">
|
||||||
<div class="space-y-4 text-center">
|
<div class="space-y-4 text-center">
|
||||||
<div class="mb-4 flex items-center justify-center gap-2">
|
<div class="mb-4 flex items-center justify-center gap-2">
|
||||||
<img src="/rugplay.svg" class="h-12 w-12" alt="Rugplay" />
|
<img src="/rugplay.svg" class="h-12 w-12" alt="CoinStorge" />
|
||||||
<h1 class="text-4xl font-bold">Rugplay</h1>
|
<h1 class="text-4xl font-bold">CoinStorge</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted-foreground mx-auto max-w-2xl text-lg">
|
<p class="text-muted-foreground mx-auto max-w-2xl text-lg">
|
||||||
A crypto trading simulator where you can practice trading without losing real money. Create
|
A crypto trading simulator where you can practice trading without losing real money. Create
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -5,43 +5,7 @@ import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/serve
|
||||||
import { eq, and, gte } from 'drizzle-orm';
|
import { eq, and, gte } from 'drizzle-orm';
|
||||||
import { redis } from '$lib/server/redis';
|
import { redis } from '$lib/server/redis';
|
||||||
import { createNotification } from '$lib/server/notification';
|
import { createNotification } from '$lib/server/notification';
|
||||||
|
import { calculate24hMetrics, executeSellTrade } from '$lib/server/amm';
|
||||||
async function calculate24hMetrics(coinId: number, currentPrice: number) {
|
|
||||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
// Get price from 24h ago
|
|
||||||
const [priceData] = await db
|
|
||||||
.select({ price: priceHistory.price })
|
|
||||||
.from(priceHistory)
|
|
||||||
.where(and(
|
|
||||||
eq(priceHistory.coinId, coinId),
|
|
||||||
gte(priceHistory.timestamp, twentyFourHoursAgo)
|
|
||||||
))
|
|
||||||
.orderBy(priceHistory.timestamp)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Calculate 24h change
|
|
||||||
let change24h = 0;
|
|
||||||
if (priceData) {
|
|
||||||
const priceFrom24hAgo = Number(priceData.price);
|
|
||||||
if (priceFrom24hAgo > 0) {
|
|
||||||
change24h = ((currentPrice - priceFrom24hAgo) / priceFrom24hAgo) * 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate 24h volume
|
|
||||||
const volumeData = await db
|
|
||||||
.select({ totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount })
|
|
||||||
.from(transaction)
|
|
||||||
.where(and(
|
|
||||||
eq(transaction.coinId, coinId),
|
|
||||||
gte(transaction.timestamp, twentyFourHoursAgo)
|
|
||||||
));
|
|
||||||
|
|
||||||
const volume24h = volumeData.reduce((sum, tx) => sum + Number(tx.totalBaseCurrencyAmount), 0);
|
|
||||||
|
|
||||||
return { change24h: Number(change24h.toFixed(4)), volume24h: Number(volume24h.toFixed(4)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST({ params, request }) {
|
export async function POST({ params, request }) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
|
|
@ -76,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');
|
||||||
|
|
@ -86,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,
|
||||||
|
|
@ -254,24 +243,21 @@ export async function POST({ params, request }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown
|
// Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown
|
||||||
const maxSellable = Math.floor(poolCoinAmount * 0.995);
|
const maxSellable = Math.floor(Number(coinData.poolCoinAmount) * 0.995);
|
||||||
if (amount > maxSellable) {
|
if (amount > maxSellable) {
|
||||||
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
|
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
const sellResult = await executeSellTrade(tx, coinData, userId, amount);
|
||||||
const newPoolCoin = poolCoinAmount + amount;
|
|
||||||
const newPoolBaseCurrency = k / newPoolCoin;
|
|
||||||
const baseCurrencyReceived = poolBaseCurrencyAmount - newPoolBaseCurrency;
|
|
||||||
|
|
||||||
totalCost = baseCurrencyReceived;
|
if (!sellResult.success) {
|
||||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
throw error(400, 'Trade failed - insufficient liquidity or invalid parameters');
|
||||||
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
|
||||||
|
|
||||||
if (newPoolBaseCurrency < 10) {
|
|
||||||
throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalCost = sellResult.baseCurrencyReceived ?? 0;
|
||||||
|
newPrice = sellResult.newPrice;
|
||||||
|
priceImpact = sellResult.priceImpact;
|
||||||
|
|
||||||
if (totalCost <= 0) {
|
if (totalCost <= 0) {
|
||||||
throw error(400, 'Trade amount results in zero base currency received');
|
throw error(400, 'Trade amount results in zero base currency received');
|
||||||
}
|
}
|
||||||
|
|
@ -302,71 +288,15 @@ export async function POST({ params, request }) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.insert(transaction).values({
|
const metrics = sellResult.metrics || await calculate24hMetrics(coinData.id, newPrice);
|
||||||
userId,
|
|
||||||
coinId: coinData.id,
|
|
||||||
type: 'SELL',
|
|
||||||
quantity: amount.toString(),
|
|
||||||
pricePerCoin: (totalCost / amount).toString(),
|
|
||||||
totalBaseCurrencyAmount: totalCost.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.insert(priceHistory).values({
|
|
||||||
coinId: coinData.id,
|
|
||||||
price: newPrice.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
|
||||||
|
|
||||||
await tx.update(coin)
|
|
||||||
.set({
|
|
||||||
currentPrice: newPrice.toString(),
|
|
||||||
marketCap: (Number(coinData.circulatingSupply) * newPrice).toString(),
|
|
||||||
poolCoinAmount: newPoolCoin.toString(),
|
|
||||||
poolBaseCurrencyAmount: newPoolBaseCurrency.toString(),
|
|
||||||
change24h: metrics.change24h.toString(),
|
|
||||||
volume24h: metrics.volume24h.toString(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
})
|
|
||||||
.where(eq(coin.id, coinData.id));
|
|
||||||
|
|
||||||
const isRugPull = priceImpact < -20 && totalCost > 1000;
|
|
||||||
|
|
||||||
// Send rug pull notifications to affected users
|
|
||||||
if (isRugPull) {
|
|
||||||
(async () => {
|
|
||||||
const affectedUsers = await db
|
|
||||||
.select({
|
|
||||||
userId: userPortfolio.userId,
|
|
||||||
quantity: userPortfolio.quantity
|
|
||||||
})
|
|
||||||
.from(userPortfolio)
|
|
||||||
.where(eq(userPortfolio.coinId, coinData.id));
|
|
||||||
|
|
||||||
for (const holder of affectedUsers) {
|
|
||||||
if (holder.userId === userId) continue;
|
|
||||||
|
|
||||||
const holdingValue = Number(holder.quantity) * newPrice;
|
|
||||||
if (holdingValue > 10) {
|
|
||||||
const lossAmount = Number(holder.quantity) * (currentPrice - newPrice);
|
|
||||||
await createNotification(
|
|
||||||
holder.userId.toString(),
|
|
||||||
'RUG_PULL',
|
|
||||||
'Coin rugpulled!',
|
|
||||||
`A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceUpdateData = {
|
const priceUpdateData = {
|
||||||
currentPrice: newPrice,
|
currentPrice: newPrice,
|
||||||
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
marketCap: Number(coinData.circulatingSupply) * newPrice,
|
||||||
change24h: metrics.change24h,
|
change24h: metrics.change24h,
|
||||||
volume24h: metrics.volume24h,
|
volume24h: metrics.volume24h,
|
||||||
poolCoinAmount: newPoolCoin,
|
poolCoinAmount: sellResult.newPoolCoin,
|
||||||
poolBaseCurrencyAmount: newPoolBaseCurrency
|
poolBaseCurrencyAmount: sellResult.newPoolBaseCurrency
|
||||||
};
|
};
|
||||||
|
|
||||||
const tradeData = {
|
const tradeData = {
|
||||||
|
|
@ -408,4 +338,4 @@ export async function POST({ params, request }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export const GET: RequestHandler = async ({ url, request }) => {
|
||||||
type: notifications.type,
|
type: notifications.type,
|
||||||
title: notifications.title,
|
title: notifications.title,
|
||||||
message: notifications.message,
|
message: notifications.message,
|
||||||
|
link: notifications.link,
|
||||||
isRead: notifications.isRead,
|
isRead: notifications.isRead,
|
||||||
createdAt: notifications.createdAt,
|
createdAt: notifications.createdAt,
|
||||||
})
|
})
|
||||||
|
|
@ -54,24 +55,22 @@ export const PATCH: RequestHandler = async ({ request }) => {
|
||||||
if (!session?.user) throw error(401, 'Not authenticated');
|
if (!session?.user) throw error(401, 'Not authenticated');
|
||||||
|
|
||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
const { ids, markAsRead } = await request.json();
|
const { markAsRead } = await request.json();
|
||||||
|
|
||||||
if (!Array.isArray(ids) || typeof markAsRead !== 'boolean') {
|
if (typeof markAsRead !== 'boolean') {
|
||||||
throw error(400, 'Invalid request body');
|
throw error(400, 'Invalid request body');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db
|
if (markAsRead) {
|
||||||
.update(notifications)
|
await db.update(notifications)
|
||||||
.set({ isRead: markAsRead })
|
.set({ isRead: true })
|
||||||
.where(and(
|
.where(eq(notifications.userId, userId));
|
||||||
eq(notifications.userId, userId),
|
}
|
||||||
inArray(notifications.id, ids)
|
|
||||||
));
|
|
||||||
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to update notifications:', e);
|
console.error('Failed to update notifications:', e);
|
||||||
throw error(500, 'Failed to update notifications');
|
throw error(500, 'Failed to update notifications');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -5,6 +5,7 @@ import { user, userPortfolio, transaction, notifications, coin } from '$lib/serv
|
||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils';
|
import { formatValue, getPrestigeCost, getPrestigeName } from '$lib/utils';
|
||||||
|
import { executeSellTrade } from '$lib/server/amm';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
const session = await auth.api.getSession({ headers: request.headers });
|
const session = await auth.api.getSession({ headers: request.headers });
|
||||||
|
|
@ -44,7 +45,10 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
coinId: userPortfolio.coinId,
|
coinId: userPortfolio.coinId,
|
||||||
quantity: userPortfolio.quantity,
|
quantity: userPortfolio.quantity,
|
||||||
currentPrice: coin.currentPrice,
|
currentPrice: coin.currentPrice,
|
||||||
symbol: coin.symbol
|
symbol: coin.symbol,
|
||||||
|
poolCoinAmount: coin.poolCoinAmount,
|
||||||
|
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
|
||||||
|
circulatingSupply: coin.circulatingSupply
|
||||||
})
|
})
|
||||||
.from(userPortfolio)
|
.from(userPortfolio)
|
||||||
.leftJoin(coin, eq(userPortfolio.coinId, coin.id))
|
.leftJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||||
|
|
@ -58,18 +62,37 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
|
||||||
for (const holding of holdings) {
|
for (const holding of holdings) {
|
||||||
const quantity = Number(holding.quantity);
|
const quantity = Number(holding.quantity);
|
||||||
const price = Number(holding.currentPrice);
|
const currentPrice = Number(holding.currentPrice);
|
||||||
const saleValue = quantity * price;
|
|
||||||
totalSaleValue += saleValue;
|
|
||||||
|
|
||||||
await tx.insert(transaction).values({
|
if (Number(holding.poolCoinAmount) <= 0 || Number(holding.poolBaseCurrencyAmount) <= 0) {
|
||||||
coinId: holding.coinId!,
|
const fallbackValue = quantity * currentPrice;
|
||||||
type: 'SELL',
|
totalSaleValue += fallbackValue;
|
||||||
quantity: holding.quantity,
|
|
||||||
pricePerCoin: holding.currentPrice || '0',
|
await tx.insert(transaction).values({
|
||||||
totalBaseCurrencyAmount: saleValue.toString(),
|
userId,
|
||||||
timestamp: new Date()
|
coinId: holding.coinId!,
|
||||||
});
|
type: 'SELL',
|
||||||
|
quantity: holding.quantity,
|
||||||
|
pricePerCoin: holding.currentPrice || '0',
|
||||||
|
totalBaseCurrencyAmount: fallbackValue.toString(),
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sellResult = await executeSellTrade(tx, {
|
||||||
|
id: holding.coinId,
|
||||||
|
poolCoinAmount: holding.poolCoinAmount,
|
||||||
|
poolBaseCurrencyAmount: holding.poolBaseCurrencyAmount,
|
||||||
|
currentPrice: holding.currentPrice,
|
||||||
|
circulatingSupply: holding.circulatingSupply
|
||||||
|
}, userId, quantity);
|
||||||
|
|
||||||
|
if (sellResult.success && sellResult.baseCurrencyReceived) {
|
||||||
|
totalSaleValue += sellResult.baseCurrencyReceived;
|
||||||
|
} else {
|
||||||
|
totalSaleValue += sellResult.fallbackValue || (quantity * currentPrice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx
|
await tx
|
||||||
|
|
@ -94,6 +117,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
type: 'SYSTEM',
|
type: 'SYSTEM',
|
||||||
title: `${prestigeName} Achieved!`,
|
title: `${prestigeName} Achieved!`,
|
||||||
message: `Congratulations! You have successfully reached ${prestigeName}. Your portfolio has been reset, daily reward cooldown has been cleared, and you can now start fresh with your new prestige badge and enhanced daily rewards.`,
|
message: `Congratulations! You have successfully reached ${prestigeName}. Your portfolio has been reset, daily reward cooldown has been cleared, and you can now start fresh with your new prestige badge and enhanced daily rewards.`,
|
||||||
|
link: `/user/${userId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,32 @@ const REWARD_TIERS = [
|
||||||
1500, // Day 2
|
1500, // Day 2
|
||||||
1800, // Day 3
|
1800, // Day 3
|
||||||
2100, // Day 4
|
2100, // Day 4
|
||||||
2500, // Day 5
|
2500, // Day 5
|
||||||
3000, // Day 6
|
3000, // Day 6
|
||||||
3500, // Day 7
|
3500, // Day 7
|
||||||
4000, // Day 8+
|
4000, // Day 8
|
||||||
|
4200, // Day 9
|
||||||
|
4400, // Day 10
|
||||||
|
4600, // Day 11
|
||||||
|
4800, // Day 12
|
||||||
|
5000, // Day 13
|
||||||
|
5200, // Day 14
|
||||||
|
5400, // Day 15
|
||||||
|
5600, // Day 16
|
||||||
|
5800, // Day 17
|
||||||
|
6000, // Day 18
|
||||||
|
6200, // Day 19
|
||||||
|
6400, // Day 20
|
||||||
|
6600, // Day 21
|
||||||
|
6800, // Day 22
|
||||||
|
7000, // Day 23
|
||||||
|
7200, // Day 24
|
||||||
|
7400, // Day 25
|
||||||
|
7600, // Day 26
|
||||||
|
7800, // Day 27
|
||||||
|
8000, // Day 28
|
||||||
|
8200, // Day 29
|
||||||
|
8500 // Day 30+
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRESTIGE_MULTIPLIERS = {
|
const PRESTIGE_MULTIPLIERS = {
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
'TRANSFER',
|
'TRANSFER',
|
||||||
'Money received!',
|
'Money received!',
|
||||||
`You received ${formatValue(amount)} from @${senderData.username}`,
|
`You received ${formatValue(amount)} from @${senderData.username}`,
|
||||||
|
`/user/${senderData.id}`
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -264,6 +265,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
'TRANSFER',
|
'TRANSFER',
|
||||||
'Coins received!',
|
'Coins received!',
|
||||||
`You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`,
|
`You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`,
|
||||||
|
`/coin/${normalizedSymbol}`
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Gambling - Rugplay"
|
title="Gambling - CoinStorge"
|
||||||
description="Play virtual gambling games with simulated currency in Rugplay. Try coinflip, slots, and mines games using virtual money with no real-world value - purely for entertainment."
|
description="Play virtual gambling games with simulated currency in CoinStorge. Try coinflip, slots, and mines games using virtual money with no real-world value - purely for entertainment."
|
||||||
keywords="virtual gambling simulation, coinflip game, slots game, mines game, virtual casino, simulated gambling, entertainment games"
|
keywords="virtual gambling simulation, coinflip game, slots game, mines game, virtual casino, simulated gambling, entertainment games"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
<MessageCircleQuestion class="h-14 w-14" />
|
<MessageCircleQuestion class="h-14 w-14" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-2xl font-semibold">{question.question}</h1>
|
<h1 class="text-2xl font-semibold break-all">{question.question}</h1>
|
||||||
{#if question.status === 'ACTIVE'}
|
{#if question.status === 'ACTIVE'}
|
||||||
<p class="text-muted-foreground mt-1 text-sm">
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
{formatTimeUntil(question.resolutionDate).startsWith('Ended')
|
{formatTimeUntil(question.resolutionDate).startsWith('Ended')
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Privacy Policy - Rugplay"
|
title="Privacy Policy - CoinStorge"
|
||||||
description="Privacy Policy for Rugplay cryptocurrency simulation game. Learn about data collection, account deletion process, virtual currency privacy, and your rights."
|
description="Privacy Policy for CoinStorge cryptocurrency simulation game. Learn about data collection, account deletion process, virtual currency privacy, and your rights."
|
||||||
keywords="privacy policy, data protection, account deletion, virtual currency privacy, simulation game privacy"
|
keywords="privacy policy, data protection, account deletion, virtual currency privacy, simulation game privacy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
it, and what happens when you delete your account.
|
it, and what happens when you delete your account.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Platform Note:</strong> Rugplay is a simulated trading environment using virtual currency
|
<strong>Platform Note:</strong> CoinStorge is a simulated trading environment using virtual currency
|
||||||
("*BUSS" or "$") with no real monetary value.
|
("*BUSS" or "$") with no real monetary value.
|
||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">2.2 Trading and Financial Data (Simulated)</h3>
|
<h3 class="mb-2 text-lg font-medium">2.2 Simulated Trading and Simulated Financial Data</h3>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Transaction history (buy/sell orders, amounts, prices, timestamps)</li>
|
<li>Transaction history (buy/sell orders, amounts, prices, timestamps)</li>
|
||||||
<li>Portfolio holdings and balances</li>
|
<li>Portfolio holdings and balances</li>
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
<li>Platform analytics and improvements</li>
|
<li>Platform analytics and improvements</li>
|
||||||
<li>Resolving disputes and maintaining system integrity</li>
|
<li>Resolving disputes and maintaining system integrity</li>
|
||||||
<li>
|
<li>
|
||||||
Operating and resolving prediction markets, which may involve automated decision-making
|
Operating and resolving prediction markets, which may involve AI-assisted decision-making
|
||||||
as detailed below.
|
as detailed below.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -241,9 +241,6 @@
|
||||||
<li>
|
<li>
|
||||||
<strong>14 days later:</strong> Complete deletion process executed automatically
|
<strong>14 days later:</strong> Complete deletion process executed automatically
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<strong>Cancellation:</strong> Contact support within 14 days to cancel deletion
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,7 +260,7 @@
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<h2 class="mb-4 text-2xl font-semibold">6. Your Data Protection Rights</h2>
|
<h2 class="mb-4 text-2xl font-semibold">6. Your Data Protection Rights</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
Depending on your location, you may have certain rights regarding your personal data,
|
You have rights regarding your personal data,
|
||||||
including:
|
including:
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
|
@ -298,8 +295,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
To exercise these rights, please contact us at <a
|
To exercise these rights, please contact us at <a
|
||||||
href="mailto:{CONTACT_EMAIL}"
|
href="mailto:privacy@ndspir.it"
|
||||||
class="text-primary underline">{CONTACT_EMAIL}</a
|
class="text-primary underline">privacy@ndspir.it</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
@ -327,7 +324,8 @@
|
||||||
|
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<h2 class="mb-4 text-2xl font-semibold">8. Data Sharing</h2>
|
<h2 class="mb-4 text-2xl font-semibold">8. Data Sharing</h2>
|
||||||
<p class="mb-4">We do not sell or share your personal data with third parties, except:</p>
|
<p class="mb-4">We do not sell your personal data.</p>
|
||||||
|
<p class="mb-4">We might be forced to share your personal data with third parties:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>When required by law or legal process</li>
|
<li>When required by law or legal process</li>
|
||||||
<li>To prevent fraud or protect platform security</li>
|
<li>To prevent fraud or protect platform security</li>
|
||||||
|
|
@ -359,8 +357,8 @@
|
||||||
<p class="mb-4">For privacy-related questions or to exercise your rights:</p>
|
<p class="mb-4">For privacy-related questions or to exercise your rights:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>
|
<li>
|
||||||
Email: <a href="mailto:{CONTACT_EMAIL}" class="text-primary underline"
|
Email: <a href="mailto:privacy@ndspir.it" class="text-primary underline"
|
||||||
>{CONTACT_EMAIL}</a
|
>privacy@ndspir.it</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>To cancel account deletion: Contact us immediately at the above email</li>
|
<li>To cancel account deletion: Contact us immediately at the above email</li>
|
||||||
|
|
@ -382,7 +380,7 @@
|
||||||
<strong>Contact:</strong>
|
<strong>Contact:</strong>
|
||||||
<a href="mailto:{CONTACT_EMAIL}" class="text-primary underline">{CONTACT_EMAIL}</a>
|
<a href="mailto:{CONTACT_EMAIL}" class="text-primary underline">{CONTACT_EMAIL}</a>
|
||||||
</p>
|
</p>
|
||||||
<p><strong>Platform:</strong> Rugplay - virtual cryptocurrency trading simulation</p>
|
<p><strong>Platform:</strong> CoinStorge - virtual cryptocurrency trading simulation</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import SEO from '$lib/components/self/SEO.svelte';
|
import SEO from '$lib/components/self/SEO.svelte';
|
||||||
|
|
||||||
const LAST_UPDATED = 'May 29, 2025';
|
const LAST_UPDATED = 'September 1, 2025';
|
||||||
const CONTACT_EMAIL = 'contact@outpoot.com';
|
const CONTACT_EMAIL = '[REDACTED]';
|
||||||
const MINIMUM_AGE = 18;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Terms of Service - Rugplay"
|
title="Terms of Service - CoinStorge"
|
||||||
description="Terms of Service for Rugplay - cryptocurrency trading simulation game. Learn about virtual currency, rug pull mechanics, gambling features, and platform rules."
|
description="Terms of Service for CoinStorge - cryptocurrency trading simulation game. Learn about virtual currency, rug pull mechanics, gambling features, and platform rules."
|
||||||
keywords="terms of service, legal terms, simulation game rules, virtual currency terms, rug pull simulation"
|
keywords="terms of service, legal terms, simulation game rules, virtual currency terms, rug pull simulation"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -36,7 +35,7 @@
|
||||||
<TrendingDown class="h-4 w-4" />
|
<TrendingDown class="h-4 w-4" />
|
||||||
<Alert.Title>Virtual Currency Simulation Only</Alert.Title>
|
<Alert.Title>Virtual Currency Simulation Only</Alert.Title>
|
||||||
<Alert.Description>
|
<Alert.Description>
|
||||||
Rugplay uses only virtual currency (*BUSS or "$") with no real monetary value. All trading,
|
CoinStorge uses only virtual currency (*BUSS or "$") with no real monetary value. All trading,
|
||||||
including rug pulls, is simulated for educational purposes only.
|
including rug pulls, is simulated for educational purposes only.
|
||||||
</Alert.Description>
|
</Alert.Description>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
|
|
@ -47,13 +46,14 @@
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<h2 class="mb-4 text-2xl font-semibold">1. Acceptance of Terms</h2>
|
<h2 class="mb-4 text-2xl font-semibold">1. Acceptance of Terms</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
By accessing and using Rugplay ("the Platform", "we", "us", "our"), you accept and agree
|
By accessing and using CoinStorge ("the Platform", "we", "us", "our"), you accept and agree
|
||||||
to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, you
|
to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, you
|
||||||
may not use the Platform.
|
may not use the Platform.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
These Terms constitute a legally binding agreement between you and Rugplay regarding your
|
These Terms constitute a legally binding agreement between you and CoinStorge regarding your
|
||||||
use of our cryptocurrency trading simulation platform.
|
use of our cryptocurrency trading simulation platform. If you are not of the age required to sign
|
||||||
|
a binding contract, you may not use the Platform.
|
||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
<h2 class="mb-4 text-2xl font-semibold">2. Platform Description</h2>
|
<h2 class="mb-4 text-2xl font-semibold">2. Platform Description</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
Rugplay is a <strong>simulated cryptocurrency trading platform</strong> designed for educational
|
CoinStorge is a <strong>simulated cryptocurrency trading platform</strong> designed for educational
|
||||||
and entertainment purposes. The Platform allows users to:
|
and entertainment purposes. The Platform allows users to:
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
<AlertTriangle class="h-4 w-4" />
|
<AlertTriangle class="h-4 w-4" />
|
||||||
<Alert.Title>No Real Financial Value</Alert.Title>
|
<Alert.Title>No Real Financial Value</Alert.Title>
|
||||||
<Alert.Description>
|
<Alert.Description>
|
||||||
All currency on Rugplay (*BUSS, "$", and created coins) is virtual and has no
|
All currency on CoinStorge (*BUSS, "$", and created coins) is virtual and has no
|
||||||
real-world monetary value. No real cryptocurrency or money is involved in any
|
real-world monetary value. No real cryptocurrency or money is involved in any
|
||||||
transactions.
|
transactions.
|
||||||
</Alert.Description>
|
</Alert.Description>
|
||||||
|
|
@ -91,11 +91,12 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">3.1 Age Requirements</h3>
|
<h3 class="mb-2 text-lg font-medium">3.1 Age Requirements</h3>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
You must be at least {MINIMUM_AGE} years old to use Rugplay due to the presence of gambling-style
|
You must be at least 18 years old to use CoinStorge.
|
||||||
features (coinflip and slots), even though they use only virtual currency.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
While our platform uses virtual currency with no real-world value, we maintain an 18+
|
Due to the presence of gambling-style
|
||||||
|
features (coinflip and slots), even though they use only virtual currency, and while our platform
|
||||||
|
uses only virtual currency with no real-world value, we maintain an 18+
|
||||||
age requirement to ensure responsible engagement with simulated gambling mechanics.
|
age requirement to ensure responsible engagement with simulated gambling mechanics.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,7 +114,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">3.3 Prohibited Users</h3>
|
<h3 class="mb-2 text-lg font-medium">3.3 Prohibited Users</h3>
|
||||||
<p>You may not use Rugplay if you are:</p>
|
<p>You may not use CoinStorge if you are:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Located in a jurisdiction where use is prohibited</li>
|
<li>Located in a jurisdiction where use is prohibited</li>
|
||||||
<li>Previously banned from the Platform</li>
|
<li>Previously banned from the Platform</li>
|
||||||
|
|
@ -132,7 +133,7 @@
|
||||||
<TrendingDown class="h-4 w-4" />
|
<TrendingDown class="h-4 w-4" />
|
||||||
<Alert.Title>Rug Pull Risk Simulation</Alert.Title>
|
<Alert.Title>Rug Pull Risk Simulation</Alert.Title>
|
||||||
<Alert.Description>
|
<Alert.Description>
|
||||||
Rugplay deliberately simulates rug pull scenarios where coin creators or large holders
|
CoinStorge deliberately simulates rug pull scenarios where coin creators or large holders
|
||||||
can crash prices by selling significant holdings. This is a core educational feature.
|
can crash prices by selling significant holdings. This is a core educational feature.
|
||||||
</Alert.Description>
|
</Alert.Description>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
|
|
@ -154,7 +155,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">4.2 Trading Mechanics</h3>
|
<h3 class="mb-2 text-lg font-medium">4.2 Trading Mechanics</h3>
|
||||||
<p class="mb-3">Trading on Rugplay includes realistic mechanics such as:</p>
|
<p class="mb-3">Trading on CoinStorge includes realistic mechanics such as:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<strong>Slippage:</strong> Large trades affect prices based on liquidity pool ratios
|
<strong>Slippage:</strong> Large trades affect prices based on liquidity pool ratios
|
||||||
|
|
@ -202,7 +203,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">5.1 Acceptable Use</h3>
|
<h3 class="mb-2 text-lg font-medium">5.1 Acceptable Use</h3>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
You agree to use Rugplay only for lawful purposes and in accordance with these Terms.
|
You agree to use CoinStorge only for lawful purposes and in accordance with these Terms.
|
||||||
You will not:
|
You will not:
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
|
|
@ -254,13 +255,13 @@
|
||||||
Terms, your investments may be forfeited. This includes:
|
Terms, your investments may be forfeited. This includes:
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
<ul class="ml-6 list-disc space-y-2" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
||||||
<li>Holdings in coins with inappropriate names, symbols, or descriptions</li>
|
<li>Holdings in coins with names, symbols, or descriptions which are inappropriate or prohibited by applicable law</li>
|
||||||
<li>Bets placed on prediction markets with offensive or prohibited content</li>
|
<li>Bets placed on prediction markets with offensive or prohibited content</li>
|
||||||
<li>Investments in content that violates intellectual property rights</li>
|
<li>Investments in content that violates intellectual property rights</li>
|
||||||
<li>Any virtual currency associated with content we remove for Terms violations</li>
|
<li>Any virtual currency associated with content we remove for Terms violations</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
<p class="mt-3" style="color: oklch(0.828 0.189 84.429 / 0.8)">
|
||||||
<strong>No Compensation:</strong> We will not provide alternative compensation or restore
|
<strong>No Compensation:</strong> There is no refund of
|
||||||
virtual balances lost due to investments in prohibited content. You invest at your own
|
virtual balances lost due to investments in prohibited content. You invest at your own
|
||||||
risk.
|
risk.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -313,7 +314,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">7.1 Virtual Gambling Games</h3>
|
<h3 class="mb-2 text-lg font-medium">7.1 Virtual Gambling Games</h3>
|
||||||
<p class="mb-3">Rugplay includes simulated gambling features:</p>
|
<p class="mb-3">CoinStorge includes simulated gambling features:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li><strong>Coinflip:</strong> Binary outcome betting with virtual currency</li>
|
<li><strong>Coinflip:</strong> Binary outcome betting with virtual currency</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -412,7 +413,7 @@
|
||||||
<AlertTriangle class="h-4 w-4" />
|
<AlertTriangle class="h-4 w-4" />
|
||||||
<Alert.Title>Important Legal Disclaimers</Alert.Title>
|
<Alert.Title>Important Legal Disclaimers</Alert.Title>
|
||||||
<Alert.Description>
|
<Alert.Description>
|
||||||
Rugplay is provided "as is" without warranties. We are not liable for virtual losses,
|
CoinStorge is provided "as is" without warranties. We are not liable for virtual losses,
|
||||||
rug pulls, or any platform-related damages.
|
rug pulls, or any platform-related damages.
|
||||||
</Alert.Description>
|
</Alert.Description>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
|
|
@ -448,7 +449,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">9.3 Educational Purpose</h3>
|
<h3 class="mb-2 text-lg font-medium">9.3 Educational Purpose</h3>
|
||||||
<p>Rugplay is designed for educational and entertainment purposes. It is not:</p>
|
<p>CoinStorge is designed for educational and entertainment purposes. It is not:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Financial advice or investment guidance</li>
|
<li>Financial advice or investment guidance</li>
|
||||||
<li>A substitute for professional financial education</li>
|
<li>A substitute for professional financial education</li>
|
||||||
|
|
@ -469,7 +470,6 @@
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Is scheduled 14 days after your request</li>
|
<li>Is scheduled 14 days after your request</li>
|
||||||
<li>Can be cancelled during the 14-day period by contacting support</li>
|
|
||||||
<li>Results in permanent loss of all virtual currency and account data</li>
|
<li>Results in permanent loss of all virtual currency and account data</li>
|
||||||
<li>May leave some anonymized data as described in our Privacy Policy</li>
|
<li>May leave some anonymized data as described in our Privacy Policy</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -504,7 +504,8 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">11.1 Platform Ownership</h3>
|
<h3 class="mb-2 text-lg font-medium">11.1 Platform Ownership</h3>
|
||||||
<p>Rugplay and all related intellectual property are owned by us, including:</p>
|
<p>CoinStorge is based on Rugplay, licensed under CC BY-NC by OutPoot (FaceDev).</p>
|
||||||
|
<p>CoinStorge and all related intellectual property are owned by either us or OutPoot, including:</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Software, code, algorithms, and technical systems</li>
|
<li>Software, code, algorithms, and technical systems</li>
|
||||||
<li>Trademarks, logos, and branding</li>
|
<li>Trademarks, logos, and branding</li>
|
||||||
|
|
@ -517,7 +518,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">11.2 Past Project Assets and Themes</h3>
|
<h3 class="mb-2 text-lg font-medium">11.2 Past Project Assets and Themes</h3>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
Rugplay incorporates intellectual property from creator's past projects, including:
|
CoinStorge incorporates intellectual property from creators' past projects, including:
|
||||||
</p>
|
</p>
|
||||||
<ul class="ml-6 list-disc space-y-2">
|
<ul class="ml-6 list-disc space-y-2">
|
||||||
<li>Characters, artwork, and visual themes from previous projects</li>
|
<li>Characters, artwork, and visual themes from previous projects</li>
|
||||||
|
|
@ -525,7 +526,7 @@
|
||||||
<li>Any derivative works or adaptations of existing intellectual property</li>
|
<li>Any derivative works or adaptations of existing intellectual property</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-muted-foreground mt-3 text-sm">
|
<p class="text-muted-foreground mt-3 text-sm">
|
||||||
All past project assets used in Rugplay are owned by the platform creators or used
|
All past project assets used in CoinStorge are owned by the platform creators or used
|
||||||
with proper authorization.
|
with proper authorization.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -550,7 +551,7 @@
|
||||||
in our Privacy Policy, which is incorporated into these Terms by reference.
|
in our Privacy Policy, which is incorporated into these Terms by reference.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
By using Rugplay, you consent to our data practices as described in the Privacy Policy,
|
By using CoinStorge, you consent to our data practices as described in the Privacy Policy,
|
||||||
including the retention of anonymized data after account deletion.
|
including the retention of anonymized data after account deletion.
|
||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
@ -561,8 +562,8 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-2 text-lg font-medium">13.1 Entire Agreement</h3>
|
<h3 class="mb-2 text-lg font-medium">13.1 Entire Agreement</h3>
|
||||||
<p>
|
<p>
|
||||||
These Terms, along with our Privacy Policy, constitute the entire agreement between
|
These Terms, along with our Privacy Policy — incorporated in these Terms by reference —, constitute the entire agreement between
|
||||||
you and Rugplay regarding use of the Platform.
|
you and CoinStorge regarding use of the Platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -578,27 +579,18 @@
|
||||||
<h3 class="mb-2 text-lg font-medium">13.3 Updates to Terms</h3>
|
<h3 class="mb-2 text-lg font-medium">13.3 Updates to Terms</h3>
|
||||||
<p>
|
<p>
|
||||||
We may update these Terms periodically. Material changes will be communicated via
|
We may update these Terms periodically. Material changes will be communicated via
|
||||||
email and platform notifications. Continued use after changes constitutes acceptance.
|
email and platform notifications, on a best-effort basis. Continued use after changes constitutes acceptance.
|
||||||
|
Failure to communicate does not automatically terminate our Agreement.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 text-lg font-medium">13.4 Contact Information</h3>
|
|
||||||
<p>
|
|
||||||
For questions about these Terms, contact us at:
|
|
||||||
<a href="mailto:{CONTACT_EMAIL}" class="text-primary underline">{CONTACT_EMAIL}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
||||||
<div class="rounded-lg p-4 text-sm" style="background-color: oklch(var(--primary) / 0.1);">
|
<div class="rounded-lg p-4 text-sm" style="background-color: oklch(var(--primary) / 0.1);">
|
||||||
<p class="mb-2"><strong>Last Updated:</strong> {LAST_UPDATED}</p>
|
<p class="mb-2"><strong>Last Updated:</strong> {LAST_UPDATED}</p>
|
||||||
<p class="mb-2">
|
<p class="mb-2"><strong>Platform:</strong> CoinStorge</p>
|
||||||
<strong>Contact:</strong>
|
|
||||||
<a href="mailto:{CONTACT_EMAIL}" class="text-primary underline">{CONTACT_EMAIL}</a>
|
|
||||||
</p>
|
|
||||||
<p class="mb-2"><strong>Platform:</strong> Rugplay</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -445,7 +445,7 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<CoinIcon icon={coin.icon} symbol={coin.symbol} size={8} />
|
<CoinIcon icon={coin.icon} symbol={coin.symbol} size={8} />
|
||||||
<div>
|
<div>
|
||||||
<h3 class="truncate text-lg font-semibold leading-tight">{coin.name}</h3>
|
<h3 class="truncate max-w-44 text-lg font-semibold leading-tight">{coin.name}</h3>
|
||||||
<p class="text-muted-foreground truncate text-sm">*{coin.symbol}</p>
|
<p class="text-muted-foreground truncate text-sm">*{coin.symbol}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import { formatTimeAgo, formatValue } from '$lib/utils';
|
import { formatTimeAgo, formatValue } from '$lib/utils';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import NotificationItem from './NotificationItem.svelte';
|
||||||
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let newNotificationIds = $state<number[]>([]);
|
let newNotificationIds = $state<number[]>([]);
|
||||||
|
|
@ -32,9 +33,7 @@
|
||||||
const unreadIds = ($NOTIFICATIONS || []).filter((n) => !n.isRead).map((n) => n.id);
|
const unreadIds = ($NOTIFICATIONS || []).filter((n) => !n.isRead).map((n) => n.id);
|
||||||
newNotificationIds = unreadIds;
|
newNotificationIds = unreadIds;
|
||||||
|
|
||||||
if (unreadIds.length > 0) {
|
await markNotificationsAsRead();
|
||||||
await markNotificationsAsRead(unreadIds);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to load notifications');
|
toast.error('Failed to load notifications');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -132,13 +131,7 @@
|
||||||
{#each $NOTIFICATIONS as notification, index (notification.id)}
|
{#each $NOTIFICATIONS as notification, index (notification.id)}
|
||||||
{@const IconComponent = getNotificationIcon(notification.type)}
|
{@const IconComponent = getNotificationIcon(notification.type)}
|
||||||
{@const isNewNotification = newNotificationIds.includes(notification.id)}
|
{@const isNewNotification = newNotificationIds.includes(notification.id)}
|
||||||
<button
|
<NotificationItem {notification} isNew={isNewNotification}>
|
||||||
class={getNotificationColorClasses(
|
|
||||||
notification.type,
|
|
||||||
isNewNotification,
|
|
||||||
notification.isRead
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full {getNotificationIconColorClasses(
|
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full {getNotificationIconColorClasses(
|
||||||
notification.type
|
notification.type
|
||||||
|
|
@ -146,7 +139,6 @@
|
||||||
>
|
>
|
||||||
<IconComponent class="h-4 w-4" />
|
<IconComponent class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="mb-1 flex items-center gap-2">
|
<div class="mb-1 flex items-center gap-2">
|
||||||
<h3 class="truncate text-sm font-medium">{notification.title}</h3>
|
<h3 class="truncate text-sm font-medium">{notification.title}</h3>
|
||||||
|
|
@ -188,7 +180,7 @@
|
||||||
{formatTimeAgo(notification.createdAt)}
|
{formatTimeAgo(notification.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</NotificationItem>
|
||||||
|
|
||||||
{#if index < $NOTIFICATIONS.length - 1}
|
{#if index < $NOTIFICATIONS.length - 1}
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
|
||||||
46
website/src/routes/notifications/NotificationItem.svelte
Normal file
46
website/src/routes/notifications/NotificationItem.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Notification {
|
||||||
|
type: 'HOPIUM' | 'TRANSFER' | 'RUG_PULL' | 'SYSTEM';
|
||||||
|
link?: string;
|
||||||
|
isRead: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let notification: Notification;
|
||||||
|
export let isNew = false;
|
||||||
|
|
||||||
|
function getNotificationColorClasses(type: string, isNew: boolean, isRead: boolean) {
|
||||||
|
const base =
|
||||||
|
'hover:bg-muted/50 flex w-full items-start gap-4 rounded-md p-3 text-left transition-all duration-200';
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
return `${base} bg-primary/10`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRead) {
|
||||||
|
const colors = {
|
||||||
|
HOPIUM: 'bg-blue-50/50 dark:bg-blue-950/10',
|
||||||
|
TRANSFER: 'bg-green-50/50 dark:bg-green-950/10',
|
||||||
|
RUG_PULL: 'bg-red-50/50 dark:bg-red-950/10',
|
||||||
|
SYSTEM: 'bg-purple-50/50 dark:bg-purple-950/10'
|
||||||
|
};
|
||||||
|
return `${base} ${colors[type as keyof typeof colors] || 'bg-muted/20'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if notification.link}
|
||||||
|
<a
|
||||||
|
href={notification.link}
|
||||||
|
class={getNotificationColorClasses(notification.type, isNew, notification.isRead)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class={getNotificationColorClasses(notification.type, isNew, notification.isRead)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in a new issue