feat: update database schema for precision + AMM behavior
This commit is contained in:
parent
a278d0c6a5
commit
930d1f41d7
8 changed files with 893 additions and 108 deletions
17
website/drizzle/0002_parched_silver_sable.sql
Normal file
17
website/drizzle/0002_parched_silver_sable.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "initial_supply" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "circulating_supply" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "current_price" SET DATA TYPE numeric(20, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "market_cap" SET DATA TYPE numeric(30, 2);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "volume_24h" SET DATA TYPE numeric(30, 2);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "volume_24h" SET DEFAULT '0.00';--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "change_24h" SET DATA TYPE numeric(10, 4);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "pool_coin_amount" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "pool_base_currency_amount" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "coin" ALTER COLUMN "pool_base_currency_amount" SET DEFAULT '0.00000000';--> statement-breakpoint
|
||||||
|
ALTER TABLE "price_history" ALTER COLUMN "price" SET DATA TYPE numeric(20, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "transaction" ALTER COLUMN "quantity" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "transaction" ALTER COLUMN "price_per_coin" SET DATA TYPE numeric(20, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "transaction" ALTER COLUMN "total_base_currency_amount" SET DATA TYPE numeric(30, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ALTER COLUMN "base_currency_balance" SET DATA TYPE numeric(20, 8);--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ALTER COLUMN "base_currency_balance" SET DEFAULT '10000.00000000';--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_portfolio" ALTER COLUMN "quantity" SET DATA TYPE numeric(30, 8);
|
||||||
709
website/drizzle/meta/0002_snapshot.json
Normal file
709
website/drizzle/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,709 @@
|
||||||
|
{
|
||||||
|
"id": "446a7a86-4e91-4461-829a-1059470307c4",
|
||||||
|
"prevId": "a272e8ea-cd8d-4f01-b826-5c4ea55499c4",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.coin": {
|
||||||
|
"name": "coin",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"name": "symbol",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"creator_id": {
|
||||||
|
"name": "creator_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"initial_supply": {
|
||||||
|
"name": "initial_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"circulating_supply": {
|
||||||
|
"name": "circulating_supply",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"current_price": {
|
||||||
|
"name": "current_price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"market_cap": {
|
||||||
|
"name": "market_cap",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"volume_24h": {
|
||||||
|
"name": "volume_24h",
|
||||||
|
"type": "numeric(30, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.00'"
|
||||||
|
},
|
||||||
|
"change_24h": {
|
||||||
|
"name": "change_24h",
|
||||||
|
"type": "numeric(10, 4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'0.0000'"
|
||||||
|
},
|
||||||
|
"pool_coin_amount": {
|
||||||
|
"name": "pool_coin_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"pool_base_currency_amount": {
|
||||||
|
"name": "pool_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'0.00000000'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"is_listed": {
|
||||||
|
"name": "is_listed",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"coin_creator_id_user_id_fk": {
|
||||||
|
"name": "coin_creator_id_user_id_fk",
|
||||||
|
"tableFrom": "coin",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"creator_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"coin_symbol_unique": {
|
||||||
|
"name": "coin_symbol_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"symbol"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.price_history": {
|
||||||
|
"name": "price_history",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"price_history_coin_id_coin_id_fk": {
|
||||||
|
"name": "price_history_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "price_history",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.transaction": {
|
||||||
|
"name": "transaction",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "transaction_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_per_coin": {
|
||||||
|
"name": "price_per_coin",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"total_base_currency_amount": {
|
||||||
|
"name": "total_base_currency_amount",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"transaction_user_id_user_id_fk": {
|
||||||
|
"name": "transaction_user_id_user_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"transaction_coin_id_coin_id_fk": {
|
||||||
|
"name": "transaction_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "transaction",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"name": "is_admin",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_banned": {
|
||||||
|
"name": "is_banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"base_currency_balance": {
|
||||||
|
"name": "base_currency_balance",
|
||||||
|
"type": "numeric(20, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'10000.00000000'"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"name": "bio",
|
||||||
|
"type": "varchar(160)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.user_portfolio": {
|
||||||
|
"name": "user_portfolio",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"coin_id": {
|
||||||
|
"name": "coin_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "numeric(30, 8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_portfolio_user_id_user_id_fk": {
|
||||||
|
"name": "user_portfolio_user_id_user_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"user_portfolio_coin_id_coin_id_fk": {
|
||||||
|
"name": "user_portfolio_coin_id_coin_id_fk",
|
||||||
|
"tableFrom": "user_portfolio",
|
||||||
|
"tableTo": "coin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"coin_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_portfolio_user_id_coin_id_pk": {
|
||||||
|
"name": "user_portfolio_user_id_coin_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"coin_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.transaction_type": {
|
||||||
|
"name": "transaction_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"BUY",
|
||||||
|
"SELL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,13 @@
|
||||||
"when": 1747991689472,
|
"when": 1747991689472,
|
||||||
"tag": "0001_last_selene",
|
"tag": "0001_last_selene",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748023983269,
|
||||||
|
"tag": "0002_parched_silver_sable",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -26,16 +26,45 @@
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
let numericAmount = $derived(parseFloat(amount) || 0);
|
let numericAmount = $derived(parseFloat(amount) || 0);
|
||||||
let estimatedCost = $derived(numericAmount * coin.currentPrice);
|
let currentPrice = $derived(coin.currentPrice || 0);
|
||||||
|
|
||||||
|
let maxSellableAmount = $derived(
|
||||||
|
type === 'SELL' && coin
|
||||||
|
? Math.min(userHolding, Math.floor(Number(coin.poolCoinAmount) * 0.995))
|
||||||
|
: userHolding
|
||||||
|
);
|
||||||
|
|
||||||
|
let estimatedResult = $derived(calculateEstimate(numericAmount, type, currentPrice));
|
||||||
let hasValidAmount = $derived(numericAmount > 0);
|
let hasValidAmount = $derived(numericAmount > 0);
|
||||||
let userBalance = $derived($USER_DATA ? Number($USER_DATA.baseCurrencyBalance) : 0);
|
let userBalance = $derived($USER_DATA ? Number($USER_DATA.baseCurrencyBalance) : 0);
|
||||||
let hasEnoughFunds = $derived(
|
let hasEnoughFunds = $derived(
|
||||||
type === 'BUY'
|
type === 'BUY' ? numericAmount <= userBalance : numericAmount <= userHolding
|
||||||
? estimatedCost <= userBalance
|
|
||||||
: numericAmount <= userHolding
|
|
||||||
);
|
);
|
||||||
let canTrade = $derived(hasValidAmount && hasEnoughFunds && !loading);
|
let canTrade = $derived(hasValidAmount && hasEnoughFunds && !loading);
|
||||||
|
|
||||||
|
function calculateEstimate(amount: number, tradeType: 'BUY' | 'SELL', price: number) {
|
||||||
|
if (!amount || !price || !coin) return { result: 0 };
|
||||||
|
|
||||||
|
const poolCoin = Number(coin.poolCoinAmount);
|
||||||
|
const poolBase = Number(coin.poolBaseCurrencyAmount);
|
||||||
|
|
||||||
|
if (poolCoin <= 0 || poolBase <= 0) return { result: 0 };
|
||||||
|
|
||||||
|
const k = poolCoin * poolBase;
|
||||||
|
|
||||||
|
if (tradeType === 'BUY') {
|
||||||
|
// AMM formula: how many coins for spending 'amount' dollars
|
||||||
|
const newPoolBase = poolBase + amount;
|
||||||
|
const newPoolCoin = k / newPoolBase;
|
||||||
|
return { result: poolCoin - newPoolCoin };
|
||||||
|
} else {
|
||||||
|
// AMM formula: how many dollars for selling 'amount' coins
|
||||||
|
const newPoolCoin = poolCoin + amount;
|
||||||
|
const newPoolBase = k / newPoolCoin;
|
||||||
|
return { result: poolBase - newPoolBase };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
open = false;
|
open = false;
|
||||||
amount = '';
|
amount = '';
|
||||||
|
|
@ -65,9 +94,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`${type === 'BUY' ? 'Bought' : 'Sold'} successfully!`, {
|
toast.success(`${type === 'BUY' ? 'Bought' : 'Sold'} successfully!`, {
|
||||||
description: type === 'BUY'
|
description:
|
||||||
? `Purchased ${result.coinsBought.toFixed(2)} ${coin.symbol} for $${result.totalCost.toFixed(2)}`
|
type === 'BUY'
|
||||||
: `Sold ${result.coinsSold.toFixed(2)} ${coin.symbol} for $${result.totalReceived.toFixed(2)}`
|
? `Purchased ${result.coinsBought.toFixed(6)} ${coin.symbol} for $${result.totalCost.toFixed(6)}`
|
||||||
|
: `Sold ${result.coinsSold.toFixed(6)} ${coin.symbol} for $${result.totalReceived.toFixed(6)}`
|
||||||
});
|
});
|
||||||
|
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
|
|
@ -83,10 +113,10 @@
|
||||||
|
|
||||||
function setMaxAmount() {
|
function setMaxAmount() {
|
||||||
if (type === 'SELL') {
|
if (type === 'SELL') {
|
||||||
amount = userHolding.toString();
|
amount = maxSellableAmount.toString();
|
||||||
} else if ($USER_DATA) {
|
} else if ($USER_DATA) {
|
||||||
const maxCoins = Math.floor(userBalance / coin.currentPrice * 100) / 100;
|
// For BUY, max is user's balance
|
||||||
amount = maxCoins.toString();
|
amount = userBalance.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -111,56 +141,64 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Amount Input -->
|
<!-- Amount Input -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="amount">Amount ({coin.symbol})</Label>
|
<Label for="amount">
|
||||||
|
{type === 'BUY' ? 'Amount to spend ($)' : `Amount (${coin.symbol})`}
|
||||||
|
</Label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="amount"
|
id="amount"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step={type === 'BUY' ? '0.01' : '1'}
|
||||||
min="0"
|
min="0"
|
||||||
bind:value={amount}
|
bind:value={amount}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" size="sm" onclick={setMaxAmount}>
|
<Button variant="outline" size="sm" onclick={setMaxAmount}>Max</Button>
|
||||||
Max
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{#if type === 'SELL'}
|
{#if type === 'SELL'}
|
||||||
<p class="text-muted-foreground text-xs">
|
<p class="text-muted-foreground text-xs">
|
||||||
Available: {userHolding.toFixed(2)} {coin.symbol}
|
Available: {userHolding.toFixed(6)}
|
||||||
|
{coin.symbol}
|
||||||
|
{#if maxSellableAmount < userHolding}
|
||||||
|
<br />Max sellable: {maxSellableAmount.toFixed(0)} {coin.symbol} (pool limit)
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{:else if $USER_DATA}
|
{:else if $USER_DATA}
|
||||||
<p class="text-muted-foreground text-xs">
|
<p class="text-muted-foreground text-xs">
|
||||||
Balance: ${userBalance.toFixed(2)}
|
Balance: ${userBalance.toFixed(6)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Estimated Cost/Return -->
|
<!-- Estimated Cost/Return with explicit fees -->
|
||||||
{#if hasValidAmount}
|
{#if hasValidAmount}
|
||||||
<div class="bg-muted/50 rounded-lg p-3">
|
<div class="bg-muted/50 rounded-lg p-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm font-medium">
|
<span class="text-sm font-medium">
|
||||||
{type === 'BUY' ? 'Total Cost:' : 'You\'ll Receive:'}
|
{type === 'BUY' ? `${coin.symbol} you'll get:` : "You'll receive:"}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
${estimatedCost.toFixed(2)}
|
{type === 'BUY'
|
||||||
|
? `~${estimatedResult.result.toFixed(6)} ${coin.symbol}`
|
||||||
|
: `~$${estimatedResult.result.toFixed(6)}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if !hasEnoughFunds}
|
<p class="text-muted-foreground mt-1 text-xs">
|
||||||
<Badge variant="destructive" class="mt-2 text-xs">
|
AMM estimation - includes slippage from pool impact
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !hasEnoughFunds && hasValidAmount}
|
||||||
|
<Badge variant="destructive" class="text-xs">
|
||||||
{type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins'}
|
{type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog.Footer class="flex gap-2">
|
<Dialog.Footer class="flex gap-2">
|
||||||
<Button variant="outline" onclick={handleClose} disabled={loading}>
|
<Button variant="outline" onclick={handleClose} disabled={loading}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onclick={handleTrade}
|
onclick={handleTrade}
|
||||||
disabled={!canTrade}
|
disabled={!canTrade}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export const user = pgTable("user", {
|
||||||
isBanned: boolean("is_banned").default(false),
|
isBanned: boolean("is_banned").default(false),
|
||||||
banReason: text("ban_reason"),
|
banReason: text("ban_reason"),
|
||||||
baseCurrencyBalance: decimal("base_currency_balance", {
|
baseCurrencyBalance: decimal("base_currency_balance", {
|
||||||
precision: 19,
|
precision: 20,
|
||||||
scale: 4,
|
scale: 8,
|
||||||
}).notNull().default("10000.0000"), // 10,000 *BUSS
|
}).notNull().default("10000.00000000"), // 10,000 *BUSS
|
||||||
bio: varchar("bio", { length: 160 }).default("Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”"),
|
bio: varchar("bio", { length: 160 }).default("Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”"),
|
||||||
username: varchar("username", { length: 30 }).notNull().unique(),
|
username: varchar("username", { length: 30 }).notNull().unique(),
|
||||||
});
|
});
|
||||||
|
|
@ -63,14 +63,14 @@ export const coin = pgTable("coin", {
|
||||||
symbol: varchar("symbol", { length: 10 }).notNull().unique(),
|
symbol: varchar("symbol", { length: 10 }).notNull().unique(),
|
||||||
icon: text("icon"), // New field for coin icon
|
icon: text("icon"), // New field for coin icon
|
||||||
creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null", }), // Coin can exist even if creator is deleted
|
creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null", }), // Coin can exist even if creator is deleted
|
||||||
initialSupply: decimal("initial_supply", { precision: 28, scale: 8 }).notNull(),
|
initialSupply: decimal("initial_supply", { precision: 30, scale: 8 }).notNull(),
|
||||||
circulatingSupply: decimal("circulating_supply", { precision: 28, scale: 8 }).notNull(),
|
circulatingSupply: decimal("circulating_supply", { precision: 30, scale: 8 }).notNull(),
|
||||||
currentPrice: decimal("current_price", { precision: 19, scale: 8 }).notNull(), // Price in base currency
|
currentPrice: decimal("current_price", { precision: 20, scale: 8 }).notNull(), // Price in base currency
|
||||||
marketCap: decimal("market_cap", { precision: 28, scale: 4 }).notNull(),
|
marketCap: decimal("market_cap", { precision: 30, scale: 2 }).notNull(),
|
||||||
volume24h: decimal("volume_24h", { precision: 28, scale: 4 }).default("0.0000"),
|
volume24h: decimal("volume_24h", { precision: 30, scale: 2 }).default("0.00"),
|
||||||
change24h: decimal("change_24h", { precision: 8, scale: 4 }).default("0.0000"), // Percentage
|
change24h: decimal("change_24h", { precision: 10, scale: 4 }).default("0.0000"), // Percentage
|
||||||
poolCoinAmount: decimal("pool_coin_amount", { precision: 28, scale: 8 }).notNull().default("0.00000000"),
|
poolCoinAmount: decimal("pool_coin_amount", { precision: 30, scale: 8 }).notNull().default("0.00000000"),
|
||||||
poolBaseCurrencyAmount: decimal("pool_base_currency_amount", { precision: 28, scale: 4, }).notNull().default("0.0000"),
|
poolBaseCurrencyAmount: decimal("pool_base_currency_amount", { precision: 30, scale: 8, }).notNull().default("0.00000000"),
|
||||||
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(),
|
||||||
|
|
@ -79,7 +79,7 @@ export const coin = pgTable("coin", {
|
||||||
export const userPortfolio = pgTable("user_portfolio", {
|
export const userPortfolio = pgTable("user_portfolio", {
|
||||||
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||||||
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
quantity: decimal("quantity", { precision: 28, scale: 8 }).notNull(),
|
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
|
||||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => {
|
(table) => {
|
||||||
|
|
@ -94,15 +94,15 @@ export const transaction = pgTable("transaction", {
|
||||||
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||||||
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
type: transactionTypeEnum("type").notNull(),
|
type: transactionTypeEnum("type").notNull(),
|
||||||
quantity: decimal("quantity", { precision: 28, scale: 8 }).notNull(),
|
quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(),
|
||||||
pricePerCoin: decimal("price_per_coin", { precision: 19, scale: 8 }).notNull(),
|
pricePerCoin: decimal("price_per_coin", { precision: 20, scale: 8 }).notNull(),
|
||||||
totalBaseCurrencyAmount: decimal("total_base_currency_amount", { precision: 28, scale: 4 }).notNull(),
|
totalBaseCurrencyAmount: decimal("total_base_currency_amount", { precision: 30, scale: 8 }).notNull(),
|
||||||
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const priceHistory = pgTable("price_history", {
|
export const priceHistory = pgTable("price_history", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }),
|
||||||
price: decimal("price", { precision: 19, scale: 8 }).notNull(),
|
price: decimal("price", { precision: 20, scale: 8 }).notNull(),
|
||||||
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,23 +87,32 @@ export async function POST({ params, request }) {
|
||||||
|
|
||||||
let newPrice: number;
|
let newPrice: number;
|
||||||
let totalCost: number;
|
let totalCost: number;
|
||||||
|
let priceImpact: number = 0;
|
||||||
|
|
||||||
|
if (poolCoinAmount <= 0 || poolBaseCurrencyAmount <= 0) {
|
||||||
|
throw error(400, 'Liquidity pool is not properly initialized or is empty. Trading halted.');
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'BUY') {
|
if (type === 'BUY') {
|
||||||
// Calculate price impact for buying
|
// AMM BUY: amount = dollars to spend
|
||||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||||
const newPoolBaseCurrency = poolBaseCurrencyAmount + (amount * currentPrice);
|
const newPoolBaseCurrency = poolBaseCurrencyAmount + amount;
|
||||||
const newPoolCoin = k / newPoolBaseCurrency;
|
const newPoolCoin = k / newPoolBaseCurrency;
|
||||||
const coinsBought = poolCoinAmount - newPoolCoin;
|
const coinsBought = poolCoinAmount - newPoolCoin;
|
||||||
|
|
||||||
totalCost = amount * currentPrice;
|
totalCost = amount;
|
||||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||||
|
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||||
|
|
||||||
if (userBalance < totalCost) {
|
if (userBalance < totalCost) {
|
||||||
throw error(400, `Insufficient funds. You need $${totalCost.toFixed(2)} but only have $${userBalance.toFixed(2)}`);
|
throw error(400, `Insufficient funds. You need *${totalCost.toFixed(6)} BUSS but only have *${userBalance.toFixed(6)} BUSS`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coinsBought <= 0) {
|
||||||
|
throw error(400, 'Trade amount too small - would result in zero tokens');
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
// Update user balance
|
|
||||||
await tx.update(user)
|
await tx.update(user)
|
||||||
.set({
|
.set({
|
||||||
baseCurrencyBalance: (userBalance - totalCost).toString(),
|
baseCurrencyBalance: (userBalance - totalCost).toString(),
|
||||||
|
|
@ -111,7 +120,6 @@ export async function POST({ params, request }) {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
// Update user portfolio
|
|
||||||
const [existingHolding] = await tx
|
const [existingHolding] = await tx
|
||||||
.select({ quantity: userPortfolio.quantity })
|
.select({ quantity: userPortfolio.quantity })
|
||||||
.from(userPortfolio)
|
.from(userPortfolio)
|
||||||
|
|
@ -140,23 +148,20 @@ export async function POST({ params, request }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record transaction
|
|
||||||
await tx.insert(transaction).values({
|
await tx.insert(transaction).values({
|
||||||
userId,
|
userId,
|
||||||
coinId: coinData.id,
|
coinId: coinData.id,
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
quantity: coinsBought.toString(),
|
quantity: coinsBought.toString(),
|
||||||
pricePerCoin: currentPrice.toString(),
|
pricePerCoin: (totalCost / coinsBought).toString(),
|
||||||
totalBaseCurrencyAmount: totalCost.toString()
|
totalBaseCurrencyAmount: totalCost.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Record price history
|
|
||||||
await tx.insert(priceHistory).values({
|
await tx.insert(priceHistory).values({
|
||||||
coinId: coinData.id,
|
coinId: coinData.id,
|
||||||
price: newPrice.toString()
|
price: newPrice.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate and update 24h metrics
|
|
||||||
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
||||||
|
|
||||||
await tx.update(coin)
|
await tx.update(coin)
|
||||||
|
|
@ -178,11 +183,12 @@ export async function POST({ params, request }) {
|
||||||
coinsBought,
|
coinsBought,
|
||||||
totalCost,
|
totalCost,
|
||||||
newPrice,
|
newPrice,
|
||||||
|
priceImpact,
|
||||||
newBalance: userBalance - totalCost
|
newBalance: userBalance - totalCost
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// SELL logic
|
// AMM SELL: amount = number of coins to sell
|
||||||
const [userHolding] = await db
|
const [userHolding] = await db
|
||||||
.select({ quantity: userPortfolio.quantity })
|
.select({ quantity: userPortfolio.quantity })
|
||||||
.from(userPortfolio)
|
.from(userPortfolio)
|
||||||
|
|
@ -196,7 +202,12 @@ export async function POST({ params, request }) {
|
||||||
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
|
throw error(400, `Insufficient coins. You have ${userHolding ? Number(userHolding.quantity) : 0} but trying to sell ${amount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate price impact for selling
|
// Allow more aggressive selling for rug pull simulation - prevent only mathematical breakdown
|
||||||
|
const maxSellable = Math.floor(poolCoinAmount * 0.995); // 99.5% instead of 99%
|
||||||
|
if (amount > maxSellable) {
|
||||||
|
throw error(400, `Cannot sell more than 99.5% of pool tokens. Max sellable: ${maxSellable} tokens`);
|
||||||
|
}
|
||||||
|
|
||||||
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
const k = poolCoinAmount * poolBaseCurrencyAmount;
|
||||||
const newPoolCoin = poolCoinAmount + amount;
|
const newPoolCoin = poolCoinAmount + amount;
|
||||||
const newPoolBaseCurrency = k / newPoolCoin;
|
const newPoolBaseCurrency = k / newPoolCoin;
|
||||||
|
|
@ -204,10 +215,18 @@ export async function POST({ params, request }) {
|
||||||
|
|
||||||
totalCost = baseCurrencyReceived;
|
totalCost = baseCurrencyReceived;
|
||||||
newPrice = newPoolBaseCurrency / newPoolCoin;
|
newPrice = newPoolBaseCurrency / newPoolCoin;
|
||||||
|
priceImpact = ((newPrice - currentPrice) / currentPrice) * 100;
|
||||||
|
|
||||||
|
// Lower minimum liquidity for more dramatic crashes
|
||||||
|
if (newPoolBaseCurrency < 10) {
|
||||||
|
throw error(400, `Trade would drain pool below minimum liquidity (*10 BUSS). Try selling fewer tokens.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCost <= 0) {
|
||||||
|
throw error(400, 'Trade amount results in zero base currency received');
|
||||||
|
}
|
||||||
|
|
||||||
// Execute sell transaction
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
// Update user balance
|
|
||||||
await tx.update(user)
|
await tx.update(user)
|
||||||
.set({
|
.set({
|
||||||
baseCurrencyBalance: (userBalance + totalCost).toString(),
|
baseCurrencyBalance: (userBalance + totalCost).toString(),
|
||||||
|
|
@ -215,9 +234,8 @@ export async function POST({ params, request }) {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
// Update user portfolio
|
|
||||||
const newQuantity = Number(userHolding.quantity) - amount;
|
const newQuantity = Number(userHolding.quantity) - amount;
|
||||||
if (newQuantity > 0) {
|
if (newQuantity > 0.000001) {
|
||||||
await tx.update(userPortfolio)
|
await tx.update(userPortfolio)
|
||||||
.set({
|
.set({
|
||||||
quantity: newQuantity.toString(),
|
quantity: newQuantity.toString(),
|
||||||
|
|
@ -235,23 +253,20 @@ export async function POST({ params, request }) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record transaction
|
|
||||||
await tx.insert(transaction).values({
|
await tx.insert(transaction).values({
|
||||||
userId,
|
userId,
|
||||||
coinId: coinData.id,
|
coinId: coinData.id,
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
quantity: amount.toString(),
|
quantity: amount.toString(),
|
||||||
pricePerCoin: currentPrice.toString(),
|
pricePerCoin: (totalCost / amount).toString(),
|
||||||
totalBaseCurrencyAmount: totalCost.toString()
|
totalBaseCurrencyAmount: totalCost.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Record price history
|
|
||||||
await tx.insert(priceHistory).values({
|
await tx.insert(priceHistory).values({
|
||||||
coinId: coinData.id,
|
coinId: coinData.id,
|
||||||
price: newPrice.toString()
|
price: newPrice.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate and update 24h metrics - SINGLE coin table update
|
|
||||||
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
const metrics = await calculate24hMetrics(coinData.id, newPrice);
|
||||||
|
|
||||||
await tx.update(coin)
|
await tx.update(coin)
|
||||||
|
|
@ -273,6 +288,7 @@ export async function POST({ params, request }) {
|
||||||
coinsSold: amount,
|
coinsSold: amount,
|
||||||
totalReceived: totalCost,
|
totalReceived: totalCost,
|
||||||
newPrice,
|
newPrice,
|
||||||
|
priceImpact,
|
||||||
newBalance: userBalance + totalCost
|
newBalance: userBalance + totalCost
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,25 +115,12 @@ export async function POST({ request }) {
|
||||||
|
|
||||||
createdCoin = newCoin;
|
createdCoin = newCoin;
|
||||||
|
|
||||||
await tx.insert(userPortfolio).values({
|
|
||||||
userId,
|
|
||||||
coinId: newCoin.id,
|
|
||||||
quantity: FIXED_SUPPLY.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.insert(priceHistory).values({
|
await tx.insert(priceHistory).values({
|
||||||
coinId: newCoin.id,
|
coinId: newCoin.id,
|
||||||
price: STARTING_PRICE.toString()
|
price: STARTING_PRICE.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.insert(transaction).values({
|
|
||||||
userId,
|
|
||||||
coinId: newCoin.id,
|
|
||||||
type: 'BUY',
|
|
||||||
quantity: FIXED_SUPPLY.toString(),
|
|
||||||
pricePerCoin: STARTING_PRICE.toString(),
|
|
||||||
totalBaseCurrencyAmount: (FIXED_SUPPLY * STARTING_PRICE).toString()
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
|
|
@ -147,6 +134,7 @@ export async function POST({ request }) {
|
||||||
feePaid: CREATION_FEE,
|
feePaid: CREATION_FEE,
|
||||||
liquidityDeposited: INITIAL_LIQUIDITY,
|
liquidityDeposited: INITIAL_LIQUIDITY,
|
||||||
initialPrice: STARTING_PRICE,
|
initialPrice: STARTING_PRICE,
|
||||||
supply: FIXED_SUPPLY
|
supply: FIXED_SUPPLY,
|
||||||
|
message: "Coin created! All tokens are in the liquidity pool. Buy some if you want to hold them."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
function generateVolumeData(candlestickData: any[], volumeData: any[]) {
|
function generateVolumeData(candlestickData: any[], volumeData: any[]) {
|
||||||
return candlestickData.map((candle, index) => {
|
return candlestickData.map((candle, index) => {
|
||||||
// Find corresponding volume data for this time period
|
// Find corresponding volume data for this time period
|
||||||
const volumePoint = volumeData.find(v => v.time === candle.time);
|
const volumePoint = volumeData.find((v) => v.time === candle.time);
|
||||||
const volume = volumePoint ? volumePoint.volume : 0;
|
const volume = volumePoint ? volumePoint.volume : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -179,10 +179,14 @@
|
||||||
priceFormat: { type: 'price', precision: 8, minMove: 0.00000001 }
|
priceFormat: { type: 'price', precision: 8, minMove: 0.00000001 }
|
||||||
});
|
});
|
||||||
|
|
||||||
const volumeSeries = chart.addSeries(HistogramSeries, {
|
const volumeSeries = chart.addSeries(
|
||||||
|
HistogramSeries,
|
||||||
|
{
|
||||||
priceFormat: { type: 'volume' },
|
priceFormat: { type: 'volume' },
|
||||||
priceScaleId: 'volume'
|
priceScaleId: 'volume'
|
||||||
}, 1);
|
},
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
const processedChartData = chartData.map((candle) => {
|
const processedChartData = chartData.map((candle) => {
|
||||||
if (candle.open === candle.close) {
|
if (candle.open === candle.close) {
|
||||||
|
|
@ -224,7 +228,9 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatPrice(price: number): string {
|
function formatPrice(price: number): string {
|
||||||
if (price < 0.01) {
|
if (price < 0.000001) {
|
||||||
|
return price.toFixed(8);
|
||||||
|
} else if (price < 0.01) {
|
||||||
return price.toFixed(6);
|
return price.toFixed(6);
|
||||||
} else if (price < 1) {
|
} else if (price < 1) {
|
||||||
return price.toFixed(4);
|
return price.toFixed(4);
|
||||||
|
|
@ -234,17 +240,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMarketCap(value: number): string {
|
function formatMarketCap(value: number): string {
|
||||||
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
|
const num = Number(value);
|
||||||
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
if (isNaN(num)) return '$0.00';
|
||||||
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
|
if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
|
||||||
return `$${value.toFixed(2)}`;
|
if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
|
||||||
|
if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
|
||||||
|
return `$${num.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSupply(value: number): string {
|
function formatSupply(value: number): string {
|
||||||
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
const num = Number(value);
|
||||||
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
if (isNaN(num)) return '0';
|
||||||
if (value >= 1e3) return `${(value / 1e3).toFixed(2)}K`;
|
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
|
||||||
return value.toLocaleString();
|
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
|
||||||
|
if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
|
||||||
|
return num.toLocaleString();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -314,7 +324,7 @@
|
||||||
<TrendingDown class="h-4 w-4 text-red-500" />
|
<TrendingDown class="h-4 w-4 text-red-500" />
|
||||||
{/if}
|
{/if}
|
||||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
|
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
|
||||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,7 +337,7 @@
|
||||||
|
|
||||||
<HoverCard.Root>
|
<HoverCard.Root>
|
||||||
<HoverCard.Trigger
|
<HoverCard.Trigger
|
||||||
class="flex cursor-pointer items-center gap-2 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
|
class="flex cursor-pointer items-center gap-1 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
|
||||||
onclick={() => goto(`/user/${coin.creatorId}`)}
|
onclick={() => goto(`/user/${coin.creatorId}`)}
|
||||||
>
|
>
|
||||||
<Avatar.Root class="h-4 w-4">
|
<Avatar.Root class="h-4 w-4">
|
||||||
|
|
@ -411,7 +421,7 @@
|
||||||
<Card.Title>Trade {coin.symbol}</Card.Title>
|
<Card.Title>Trade {coin.symbol}</Card.Title>
|
||||||
{#if userHolding > 0}
|
{#if userHolding > 0}
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
You own: {userHolding.toFixed(2)}
|
You own: {formatSupply(userHolding)}
|
||||||
{coin.symbol}
|
{coin.symbol}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -465,9 +475,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground text-sm">Base Currency:</span>
|
<span class="text-muted-foreground text-sm">Base Currency:</span>
|
||||||
<span class="font-mono text-sm"
|
<span class="font-mono text-sm">
|
||||||
>${coin.poolBaseCurrencyAmount.toLocaleString()}</span
|
${Number(coin.poolBaseCurrencyAmount).toLocaleString()}
|
||||||
>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -476,13 +486,13 @@
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground text-sm">Total Liquidity:</span>
|
<span class="text-muted-foreground text-sm">Total Liquidity:</span>
|
||||||
<span class="font-mono text-sm"
|
<span class="font-mono text-sm">
|
||||||
>${(coin.poolBaseCurrencyAmount * 2).toLocaleString()}</span
|
${(Number(coin.poolBaseCurrencyAmount) * 2).toLocaleString()}
|
||||||
>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground text-sm">Price Impact:</span>
|
<span class="text-muted-foreground text-sm">Current Price:</span>
|
||||||
<Badge variant="success" class="text-xs">Low</Badge>
|
<span class="font-mono text-sm">${formatPrice(coin.currentPrice)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -549,7 +559,7 @@
|
||||||
<TrendingDown class="h-4 w-4 text-red-500" />
|
<TrendingDown class="h-4 w-4 text-red-500" />
|
||||||
{/if}
|
{/if}
|
||||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
|
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
|
||||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
|
||||||
Reference in a new issue