- Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.py
More file actions
Latest commit
226 lines (210 loc) · 8.18 KB
/
Copy pathdatabase.py
File metadata and controls
226 lines (210 loc) · 8.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
importos
importlogging
importasyncpg
fromtypingimportOptional
# Configure logging for production observability
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO
)
logger=logging.getLogger(__name__)
# Global connection pool
_pool: Optional[asyncpg.Pool] =None
asyncdefget_pool() ->asyncpg.Pool:
"""
Initializes and returns the asyncpg connection pool.
Ensures a singleton pool is used across the application.
"""
global_pool
if_poolisNone:
db_url=os.getenv("DATABASE_URL")
ifnotdb_url:
raiseValueError("Critical Error: DATABASE_URL environment variable is not set.")
try:
# Render managed PostgreSQL typically requires SSL for external connections.
# The sslmode can be configured via the DATABASE_URL DSN.
_pool=awaitasyncpg.create_pool(
dsn=db_url,
min_size=2,
max_size=20,
command_timeout=60,
server_settings={'application_name': 'web3_tg_bot'}
)
logger.info("PostgreSQL connection pool created successfully.")
exceptExceptionase:
logger.error(f"Failed to initialize database pool: {e}")
raise
return_pool
asyncdefinit_db() ->None:
"""
Connects to the database and initializes required tables atomically.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
asyncwithconn.transaction():
awaitconn.execute("""
CREATE TABLE IF NOT EXISTS users (
telegram_id BIGINT PRIMARY KEY,
sol_wallet TEXT,
xp INT DEFAULT 0,
level INT DEFAULT 1,
balance_lamports BIGINT DEFAULT 0,
trust_score INT DEFAULT 100,
streak_count INT DEFAULT 0,
last_check_in TIMESTAMP
);
""")
awaitconn.execute("""
CREATE TABLE IF NOT EXISTS agencies (
agency_id BIGINT PRIMARY KEY,
company_name TEXT,
balance_lamports BIGINT DEFAULT 0,
api_key TEXT,
status TEXT DEFAULT 'pending'
);
""")
awaitconn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
task_id SERIAL PRIMARY KEY,
owner_id BIGINT,
prompt_text TEXT,
reward_lamports BIGINT,
required_consensus INT,
status TEXT DEFAULT 'active'
);
""")
awaitconn.execute("""
CREATE TABLE IF NOT EXISTS submissions (
submission_id SERIAL PRIMARY KEY,
task_id INT REFERENCES tasks(task_id) ON DELETE CASCADE,
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
user_answer TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
awaitconn.execute("""
CREATE TABLE IF NOT EXISTS payouts (
payout_id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
destination_wallet TEXT,
amount_lamports BIGINT,
tx_signature TEXT,
status TEXT
);
""")
logger.info("Database schema initialized successfully.")
exceptExceptionase:
logger.error(f"Error initializing database schema: {e}")
raise
asyncdefregister_user(telegram_id: int, wallet: str) ->Optional[asyncpg.Record]:
"""
Registers a new user or updates their Solana wallet if they already exist.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
query="""
INSERT INTO users (telegram_id, sol_wallet)
VALUES ($1, $2)
ON CONFLICT (telegram_id) DO UPDATE
SET sol_wallet = EXCLUDED.sol_wallet
RETURNING *;
"""
returnawaitconn.fetchrow(query, telegram_id, wallet)
exceptExceptionase:
logger.error(f"Error registering user {telegram_id}: {e}")
raise
asyncdefget_user(telegram_id: int) ->Optional[asyncpg.Record]:
"""
Retrieves a user's record by their Telegram ID.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
query="SELECT * FROM users WHERE telegram_id = $1;"
returnawaitconn.fetchrow(query, telegram_id)
exceptExceptionase:
logger.error(f"Error fetching user {telegram_id}: {e}")
raise
asyncdefupdate_balance(telegram_id: int, amount_lamports: int) ->Optional[int]:
"""
Atomically updates a user's balance (supports both positive and negative amounts).
Returns the new balance.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
query="""
UPDATE users
SET balance_lamports = balance_lamports + $2
WHERE telegram_id = $1
RETURNING balance_lamports;
"""
returnawaitconn.fetchval(query, telegram_id, amount_lamports)
exceptExceptionase:
logger.error(f"Error updating balance for user {telegram_id}: {e}")
raise
asyncdefadd_task(owner_id: int, prompt_text: str, reward_lamports: int, required_consensus: int) ->Optional[int]:
"""
Creates a new task and returns the generated task_id.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
query="""
INSERT INTO tasks (owner_id, prompt_text, reward_lamports, required_consensus)
VALUES ($1, $2, $3, $4)
RETURNING task_id;
"""
returnawaitconn.fetchval(query, owner_id, prompt_text, reward_lamports, required_consensus)
exceptExceptionase:
logger.error(f"Error adding task for owner {owner_id}: {e}")
raise
asyncdefget_available_task(user_id: int) ->Optional[asyncpg.Record]:
"""
Fetches an active task that the user has not yet submitted an answer for.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
query="""
SELECT * FROM tasks
WHERE status = 'active'
AND task_id NOT IN (
SELECT task_id FROM submissions WHERE user_id = $1
)
ORDER BY task_id ASC
LIMIT 1;
"""
returnawaitconn.fetchrow(query, user_id)
exceptExceptionase:
logger.error(f"Error fetching available task for user {user_id}: {e}")
raise
asyncdefsubmit_work(task_id: int, user_id: int, user_answer: str) ->Optional[int]:
"""
Records a user's submission for a task atomically.
Returns the generated submission_id.
"""
pool=awaitget_pool()
try:
asyncwithpool.acquire() asconn:
asyncwithconn.transaction():
query="""
INSERT INTO submissions (task_id, user_id, user_answer)
VALUES ($1, $2, $3)
RETURNING submission_id;
"""
returnawaitconn.fetchval(query, task_id, user_id, user_answer)
exceptExceptionase:
logger.error(f"Error submitting work for task {task_id} by user {user_id}: {e}")
raise
asyncdefclose_db() ->None:
"""
Gracefully closes the database connection pool.
"""
global_pool
if_poolisnotNone:
await_pool.close()
_pool=None
logger.info("Database connection pool closed.")