diff --git a/.env b/.env index 1f066e6..a9ef090 100644 --- a/.env +++ b/.env @@ -29,3 +29,13 @@ OPENAI_API_KEY=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 # List models available from Groq # aider --models groq/ SUBSCRIPTION_ID='2217755' + + +# This was inserted by `prisma init`: +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" \ No newline at end of file diff --git a/crypto/sol/modules/storage.py b/crypto/sol/modules/storage.py index e372a54..ad69533 100644 --- a/crypto/sol/modules/storage.py +++ b/crypto/sol/modules/storage.py @@ -2,213 +2,161 @@ import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -import aiosqlite import json from datetime import datetime +import prisma +from prisma import Prisma -DATABASE_FILE = "./app_data.db" +# Initialize the Prisma client +prisma_client = Prisma() async def init_db(): - async with aiosqlite.connect(DATABASE_FILE) as db: - await db.executescript(""" - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT UNIQUE NOT NULL, - api_key TEXT UNIQUE, - plan TEXT DEFAULT 'free' - ); - - CREATE TABLE IF NOT EXISTS wallets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - address TEXT NOT NULL, - secret TEXT NOT NULL, - name TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) - ); - - CREATE TABLE IF NOT EXISTS transactions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - wallet_id INTEGER, - timestamp TEXT, - type TEXT, - sell_currency TEXT, - sell_amount REAL, - sell_value REAL, - buy_currency TEXT, - buy_amount REAL, - buy_value REAL, - details TEXT, - solana_signature TEXT UNIQUE, - FOREIGN KEY (wallet_id) REFERENCES wallets(id) - ); - - CREATE TABLE IF NOT EXISTS holdings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - wallet_id INTEGER, - currency TEXT, - amount REAL, - last_updated TEXT, - is_base BOOLEAN DEFAULT 0, - FOREIGN KEY (wallet_id) REFERENCES wallets(id) - ); - - CREATE TABLE IF NOT EXISTS price_alerts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - currency TEXT, - target_price REAL, - alert_type TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) - ); - - CREATE TABLE IF NOT EXISTS followed_accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - address TEXT, - followed_address TEXT, - name TEXT, - FOREIGN KEY (address) REFERENCES wallets(address), - FOREIGN KEY (followed_address) REFERENCES wallets(address), - FOREIGN KEY (user_id) REFERENCES users(id) - ); - """) - await db.commit() + await prisma_client.connect() async def store_transaction(wallet_id, transaction_type, sell_currency, sell_amount, sell_value, buy_currency, buy_amount, buy_value, solana_signature, details=None): - async with aiosqlite.connect(DATABASE_FILE) as db: - await db.execute(""" - INSERT INTO transactions (wallet_id, timestamp, type, sell_currency, sell_amount, sell_value, buy_currency, buy_amount, buy_value, solana_signature, details) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, (wallet_id, datetime.now().isoformat(), transaction_type, sell_currency, sell_amount, sell_value, buy_currency, buy_amount, buy_value, solana_signature, json.dumps(details or {}))) - await db.commit() - + await prisma_client.transaction.create( + data={ + 'wallet_id': wallet_id, + 'timestamp': datetime.now().isoformat(), + 'type': transaction_type, + 'sell_currency': sell_currency, + 'sell_amount': sell_amount, + 'sell_value': sell_value, + 'buy_currency': buy_currency, + 'buy_amount': buy_amount, + 'buy_value': buy_value, + 'solana_signature': solana_signature, + 'details': json.dumps(details or {}) + } + ) async def update_holdings(wallet_id, currency, amount_change): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute("SELECT amount FROM holdings WHERE wallet_id = ? AND currency = ?", (wallet_id, currency)) - result = await cursor.fetchone() - if result: - new_amount = result[0] + amount_change - await db.execute("UPDATE holdings SET amount = ?, last_updated = ? WHERE wallet_id = ? AND currency = ?", - (new_amount, datetime.now().isoformat(), wallet_id, currency)) - else: - await db.execute("INSERT INTO holdings (wallet_id, currency, amount, last_updated) VALUES (?, ?, ?, ?)", - (wallet_id, currency, amount_change, datetime.now().isoformat())) - await db.commit() + holding = await prisma_client.holding.find_first( + where={ + 'wallet_id': wallet_id, + 'currency': currency + } + ) + if holding: + new_amount = holding.amount + amount_change + await prisma_client.holding.update( + where={'id': holding.id}, + data={ + 'amount': new_amount, + 'last_updated': datetime.now().isoformat() + } + ) + else: + await prisma_client.holding.create( + data={ + 'wallet_id': wallet_id, + 'currency': currency, + 'amount': amount_change, + 'last_updated': datetime.now().isoformat() + } + ) async def get_wallet_holdings(wallet_id): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute("SELECT currency, amount FROM holdings WHERE wallet_id = ?", (wallet_id,)) - return await cursor.fetchall() + return await prisma_client.holding.find_many( + where={'wallet_id': wallet_id}, + select={'currency': True, 'amount': True} + ) async def get_transaction_history(wallet_id, start_date=None, end_date=None, include_closed=False): - async with aiosqlite.connect(DATABASE_FILE) as db: - query = "SELECT * FROM transactions WHERE wallet_id = ?" - params = [wallet_id] - if not include_closed: - query += " AND closed = 0" - if start_date: - query += " AND timestamp >= ?" - params.append(start_date) - if end_date: - query += " AND timestamp <= ?" - params.append(end_date) - query += " ORDER BY timestamp DESC" - cursor = await db.execute(query, params) - return await cursor.fetchall() - -# New utility functions + filters = {'wallet_id': wallet_id} + if not include_closed: + filters['closed'] = False + if start_date: + filters['timestamp'] = {'gte': start_date} + if end_date: + filters['timestamp'] = {'lte': end_date} + + return await prisma_client.transaction.find_many( + where=filters, + order={'timestamp': 'desc'} + ) async def close_transaction(transaction_id): - async with aiosqlite.connect(DATABASE_FILE) as db: - await db.execute("UPDATE transactions SET closed = 1 WHERE id = ?", (transaction_id,)) - await db.commit() + await prisma_client.transaction.update( + where={'id': transaction_id}, + data={'closed': True} + ) async def get_open_transactions(wallet_id, currency): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute(""" - SELECT * FROM transactions - WHERE wallet_id = ? AND buy_currency = ? AND closed = 0 - ORDER BY timestamp ASC - """, (wallet_id, currency)) - return await cursor.fetchall() + return await prisma_client.transaction.find_many( + where={ + 'wallet_id': wallet_id, + 'buy_currency': currency, + 'closed': False + }, + order={'timestamp': 'asc'} + ) async def calculate_current_holdings(wallet_id): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute(""" - SELECT - buy_currency AS currency, - SUM(buy_amount) - COALESCE( - (SELECT SUM(sell_amount) - FROM transactions t2 - WHERE t2.wallet_id = t1.wallet_id - AND t2.sell_currency = t1.buy_currency - AND t2.closed = 0), - 0 - ) AS amount - FROM transactions t1 - WHERE wallet_id = ? AND closed = 0 - GROUP BY buy_currency - HAVING amount > 0 - """, (wallet_id,)) - return await cursor.fetchall() + transactions = await prisma_client.transaction.group_by( + by=['buy_currency'], + where={'wallet_id': wallet_id, 'closed': False}, + _sum={'buy_amount': True, 'sell_amount': True} + ) + return [ + { + 'currency': t.buy_currency, + 'amount': t._sum.buy_amount - (t._sum.sell_amount or 0) + } + for t in transactions if t._sum.buy_amount > (t._sum.sell_amount or 0) + ] STABLECOINS = ['USDC', 'USDT', 'SOL'] async def is_transaction_closed(wallet_id, transaction_id): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute(""" - SELECT t1.buy_currency, t1.buy_amount, - (SELECT SUM(sell_amount) - FROM transactions t2 - WHERE t2.wallet_id = t1.wallet_id - AND t2.sell_currency = t1.buy_currency - AND t2.timestamp > t1.timestamp) AS sold_amount - FROM transactions t1 - WHERE t1.id = ? AND t1.wallet_id = ? - """, (transaction_id, wallet_id)) - result = await cursor.fetchone() - - if result: - buy_currency, buy_amount, sold_amount = result - return sold_amount is not None and sold_amount >= buy_amount - return False + transaction = await prisma_client.transaction.find_unique( + where={'id': transaction_id} + ) + if transaction: + sold_amount = await prisma_client.transaction.aggregate( + _sum={'sell_amount': True}, + where={ + 'wallet_id': wallet_id, + 'sell_currency': transaction.buy_currency, + 'timestamp': {'gt': transaction.timestamp} + } + ) + return sold_amount._sum.sell_amount >= transaction.buy_amount + return False async def close_completed_transactions(wallet_id): - async with aiosqlite.connect(DATABASE_FILE) as db: - cursor = await db.execute(""" - SELECT id FROM transactions - WHERE wallet_id = ? AND closed = 0 AND buy_currency NOT IN (?) - """, (wallet_id, ','.join(STABLECOINS))) - transactions = await cursor.fetchall() - - for (transaction_id,) in transactions: - if await is_transaction_closed(wallet_id, transaction_id): - await close_transaction(transaction_id) + transactions = await prisma_client.transaction.find_many( + where={ + 'wallet_id': wallet_id, + 'closed': False, + 'buy_currency': {'notIn': STABLECOINS} + } + ) + for transaction in transactions: + if await is_transaction_closed(wallet_id, transaction.id): + await close_transaction(transaction.id) async def get_profit_loss(wallet_id, currency, start_date=None, end_date=None): - async with aiosqlite.connect(DATABASE_FILE) as db: - query = """ - SELECT - SUM(CASE WHEN sell_currency = ? THEN sell_value ELSE -buy_value END) as profit_loss - FROM transactions - WHERE wallet_id = ? AND (sell_currency = ? OR buy_currency = ?) - """ - params = [currency, wallet_id, currency, currency] - - if start_date: - query += " AND timestamp >= ?" - params.append(start_date) - if end_date: - query += " AND timestamp <= ?" - params.append(end_date) - - cursor = await db.execute(query, params) - result = await cursor.fetchone() - return result[0] if result else 0 + filters = { + 'wallet_id': wallet_id, + 'OR': [ + {'sell_currency': currency}, + {'buy_currency': currency} + ] + } + if start_date: + filters['timestamp'] = {'gte': start_date} + if end_date: + filters['timestamp'] = {'lte': end_date} + + result = await prisma_client.transaction.aggregate( + _sum={ + 'sell_value': True, + 'buy_value': True + }, + where=filters + ) + return (result._sum.sell_value or 0) - (result._sum.buy_value or 0) # # # # # # USERS diff --git a/package-lock.json b/package-lock.json index a7948b7..c7d4c76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,9 @@ "openai": "^4.50.0", "request": "^2.88.2", "ws": "^8.12.1" + }, + "devDependencies": { + "prisma": "^5.22.0" } }, "node_modules/@prisma/client": { @@ -39,6 +42,51 @@ } } }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, "node_modules/@types/node": { "version": "18.19.34", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", @@ -595,6 +643,20 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -992,6 +1054,25 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index cebd6f9..ff0daf2 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "openai": "^4.50.0", "request": "^2.88.2", "ws": "^8.12.1" + }, + "devDependencies": { + "prisma": "^5.22.0" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d5fd6d1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,88 @@ +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + password String + ownedWallets Wallet[] @relation("UserWallets") + followedWallets WalletFollow[] // Intermediate table relation + +} + +model Wallet { + id Int @id @default(autoincrement()) + publicKey String + privateKey String + userId Int + owner User @relation("UserWallets", fields: [userId], references: [id]) + followers WalletFollow[] // Intermediate table relation + holdings Holding[] + + @@index([userId]) +} + +// Intermediate table for many-to-many follow relationship +model WalletFollow { + id Int @id @default(autoincrement()) + userId Int + walletId Int + user User @relation(fields: [userId], references: [id]) + wallet Wallet @relation(fields: [walletId], references: [id]) + createdAt DateTime @default(now()) + + @@unique([userId, walletId]) // Prevent duplicate follows + @@index([userId]) + @@index([walletId]) +} + +model Holding { + id Int @id @default(autoincrement()) + tokenMint String @unique + tokenName String + ammount Float + totalAcquisitionValueUSD Float + totalSellValueUSD Float + walletId Int + wallet Wallet @relation(fields: [walletId], references: [id]) + transactions Transaction[] + pnl Float @default(0.0) + +} + +enum TransactionStatus { + ORIGINAL + ORIGINAL_FOLLOWED + PENDING + FAILED + SENT + CONFIRMED +} + +model Transaction { + id Int @id @default(autoincrement()) + transactionId String + amount Float + date DateTime + amountUSD Float + holdingId Int + holding Holding @relation(fields: [holdingId], references: [id]) + isClosed Boolean @default(false) + status TransactionStatus + originalId Int? + original Transaction? @relation("OriginalTransaction", fields: [originalId], references: [id]) + relatedTo Transaction[] @relation("OriginalTransaction") + timestamp DateTime @default(now()) +} + +datasource db { + provider = "mysql" + url = "mysql://root:Zelen0ku4e@192.168.0.10:3306/trader" +} + +model Timeseries { + id Int @id @default(autoincrement()) + timestamp DateTime @default(now()) + data String +}