forked from lnbits/events
-
Notifications
You must be signed in to change notification settings - Fork 1
/
views_api.py
258 lines (219 loc) · 10.5 KB
/
views_api.py
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
from http import HTTPStatus
from datetime import datetime
import json
import hmac
from fastapi import Depends, Query
from loguru import logger
import shortuuid
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, create_ticket, INVOICE_EXPIRY
from lnbits.core.services import create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import bets4sats_ext
from .tasks import reward_ticket_ids_queue
from .helpers import get_lnurlp_parameters, send_ticket
from .crud import (
cas_competition_state,
create_competition,
delete_competition,
delete_competition_tickets,
delete_ticket,
get_competition,
get_state_competition_tickets,
get_wallet_competition_tickets,
get_competitions,
get_ticket,
get_tickets,
sum_choices_amounts,
update_competition,
update_competition_winners,
)
from .models import CompleteCompetition, CreateCompetition, CreateInvoiceForTicket, UpdateCompetition
# Competitions
@bets4sats_ext.get("/api/v1/competitions")
async def api_competitions(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [competition.dict() for competition in await get_competitions(wallet_ids)]
@bets4sats_ext.post("/api/v1/competitions")
async def api_competition_create(data: CreateCompetition):
choices = json.loads(data.choices)
if not isinstance(choices, list):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Choices must be a list")
if not all(isinstance(choice, dict) and isinstance(choice.get("title"), str) and choice["title"] for choice in choices):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Choices title must be a non-empty string")
if len(choices) < 2:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Must have at least 2 choices")
try:
datetime.strptime(data.closing_datetime, "%Y-%m-%dT%H:%M:%S.%fZ")
except:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Invalid closing_datetime")
competition = await create_competition(data=data)
return competition.dict()
@bets4sats_ext.patch("/api/v1/competitions/{competition_id}")
async def api_competition_update(data: UpdateCompetition, competition_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
if data.amount_tickets is not None and data.amount_tickets < 0:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="amount_tickets cannot be negative")
if data.closing_datetime is not None:
try:
datetime.strptime(data.closing_datetime, "%Y-%m-%dT%H:%M:%S.%fZ")
except:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Invalid closing_datetime")
competition = await get_competition(competition_id)
if not competition:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Competition not found")
if competition.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your competition"
)
if competition.state != "INITIAL":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Cannot change competition after closing")
competition = await update_competition(competition_id, data)
if competition is None:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Cannot update competition when no longer in INITIAL state"
)
return competition.dict()
@bets4sats_ext.post("/api/v1/competitions/{competition_id}/complete")
async def api_competition_complete(data: CompleteCompetition, competition_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
if data.winning_choice < -1:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="winning_choice cannot be below -1")
competition = await get_competition(competition_id)
if not competition:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="competition not found")
if competition.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your competition"
)
cas_result = await cas_competition_state(competition_id, "INITIAL", "COMPLETED_PAYING")
if not cas_result:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="competition already completed")
# refetch competition after cas
competition = await get_competition(competition_id)
choices = json.loads(competition.choices)
if data.winning_choice >= len(choices):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="winning_choice too high")
if data.winning_choice >= 0 and choices[data.winning_choice]["total"] == 0:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="no bet on winning choice")
for choice in choices:
choice["pre_agg_total"] = choice["total"]
choice["total"] = 0
choice_amount_sums = await sum_choices_amounts(competition.id)
for choice_amount_sum in choice_amount_sums:
choices[choice_amount_sum.choice]["total"] = choice_amount_sum.amount_sum
await update_competition_winners(competition_id, json.dumps(choices), data.winning_choice)
unpaid_tickets = await get_state_competition_tickets(competition_id, ["WON_UNPAID", "CANCELLED_UNPAID"])
for ticket in unpaid_tickets:
reward_ticket_ids_queue.put_nowait(ticket.id)
competition = await get_competition(competition_id)
return competition.dict()
@bets4sats_ext.delete("/api/v1/competitions/{competition_id}")
async def api_form_delete(competition_id, wallet: WalletTypeInfo = Depends(get_key_type)):
competition = await get_competition(competition_id)
if not competition:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Competition does not exist."
)
if competition.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your competition.")
await delete_competition(competition_id)
await delete_competition_tickets(competition_id)
return "", HTTPStatus.NO_CONTENT
#########Tickets##########
@bets4sats_ext.get("/api/v1/tickets")
async def api_tickets(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@bets4sats_ext.post("/api/v1/tickets/{competition_id}")
async def api_ticket_make_ticket(competition_id, data: CreateInvoiceForTicket):
competition = await get_competition(competition_id)
if not competition:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Competition does not exist."
)
if competition.state != "INITIAL" or competition.amount_tickets <= 0 or datetime.utcnow() > datetime.strptime(competition.closing_datetime, "%Y-%m-%dT%H:%M:%S.%fZ"):
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Competition is close for new tickets."
)
if data.amount < competition.min_bet or data.amount > competition.max_bet:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Amount must be between Min-Bet and Max-Bet"
)
if data.choice < 0 or data.choice >= len(json.loads(competition.choices)):
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Invalid choice"
)
if data.reward_target:
try:
await get_lnurlp_parameters(data.reward_target)
except Exception as error:
logger.warning(f"Failed to get lnurlp parameters {error}")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Bad lightning address, lnurl-pay or wallet-id"
)
ticket_id = shortuuid.random()
payment_request = None
try:
_payment_hash, payment_request = await create_invoice(
expiry=INVOICE_EXPIRY,
wallet_id=competition.wallet,
amount=data.amount,
memo=f"Bets4SatsTicketId:{competition_id}.{ticket_id}",
extra={"tag": "bets4sats", "reward_target": data.reward_target, "choice": data.choice},
)
await create_ticket(
ticket_id=ticket_id,
wallet=competition.wallet,
competition=competition_id,
amount=data.amount,
reward_target=str(data.reward_target),
choice=int(data.choice)
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return {"ticket_id": ticket_id, "payment_request": payment_request}
@bets4sats_ext.get("/api/v1/tickets/{competition_id}/{ticket_id}")
async def api_ticket_send_ticket(competition_id, ticket_id):
response = await send_ticket(competition_id, ticket_id)
return response
@bets4sats_ext.delete("/api/v1/tickets/{ticket_id}")
async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
if ticket.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
# Competition Tickets
@bets4sats_ext.get("/api/v1/competitiontickets/{competition_id}/{register_id}")
async def api_competition_tickets(competition_id, register_id):
competition = await get_competition(competition_id)
if competition is None or not hmac.compare_digest(competition.register_id, register_id):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Competition does not exist."
)
return [
ticket.dict()
for ticket in await get_wallet_competition_tickets(competition_id)
]
@bets4sats_ext.get("/api/v1/register/ticket/{ticket_id}")
async def api_competition_register_ticket(ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
return ticket.dict()