@@ -11,35 +11,45 @@ use uuid::Uuid;
11
11
pub enum GameState {
12
12
WaitingForPlayers ,
13
13
WaitingForBets ,
14
+ BetsCollected ,
14
15
CommitmentPhase ,
15
16
RevealPhase ,
16
17
Completed { winner : Uuid } ,
17
18
Aborted { reason : String } ,
18
19
}
19
20
20
- #[ derive( Debug ) ]
21
+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
22
+ pub struct BetInfo {
23
+ pub player_id : Uuid ,
24
+ pub amount : Amount ,
25
+ pub txid : String ,
26
+ pub timestamp : DateTime < Utc > ,
27
+ }
28
+
21
29
pub struct TwoPlayerGame {
22
30
id : Uuid ,
23
31
bet_amount : Amount ,
24
32
state : GameState ,
25
33
players : HashMap < Uuid , Player > ,
34
+ pot_wallet : Arc < ArkWallet > , // wallet for the pot
35
+ collected_bets : HashMap < Uuid , BetInfo > ,
26
36
commitment_deadline : Option < DateTime < Utc > > ,
27
37
reveal_deadline : Option < DateTime < Utc > > ,
28
- pot_address : String ,
38
+ total_pot : Amount ,
29
39
}
30
40
31
41
impl TwoPlayerGame {
32
- pub async fn new ( bet_amount : Amount ) -> Result < Self > {
33
- let pot_address = format ! ( "game_pot_{}" , Uuid :: new_v4( ) ) ;
34
-
42
+ pub async fn new ( bet_amount : Amount , pot_wallet : Arc < ArkWallet > ) -> Result < Self > {
35
43
Ok ( Self {
36
44
id : Uuid :: new_v4 ( ) ,
37
45
bet_amount,
38
46
state : GameState :: WaitingForPlayers ,
39
47
players : HashMap :: new ( ) ,
48
+ pot_wallet,
49
+ collected_bets : HashMap :: new ( ) ,
40
50
commitment_deadline : None ,
41
51
reveal_deadline : None ,
42
- pot_address ,
52
+ total_pot : Amount :: ZERO ,
43
53
} )
44
54
}
45
55
@@ -59,14 +69,23 @@ impl TwoPlayerGame {
59
69
self . players . len ( )
60
70
}
61
71
62
- pub fn pot_address ( & self ) -> & str {
63
- & self . pot_address
72
+ pub async fn get_pot_address ( & self ) -> Result < String > {
73
+ let ark_addr = self . pot_wallet . get_ark_address ( ) . await ?;
74
+ Ok ( ark_addr. address )
75
+ }
76
+
77
+ pub fn total_pot ( & self ) -> Amount {
78
+ self . total_pot
64
79
}
65
80
66
81
pub fn players ( & self ) -> Vec < Uuid > {
67
82
self . players . keys ( ) . cloned ( ) . collect ( )
68
83
}
69
84
85
+ pub fn get_bet_info ( & self , player_id : Uuid ) -> Option < & BetInfo > {
86
+ self . collected_bets . get ( & player_id)
87
+ }
88
+
70
89
/// Add a player to the game
71
90
pub async fn add_player ( & mut self , wallet : Arc < ArkWallet > ) -> Result < Uuid > {
72
91
if self . players . len ( ) >= 2 {
@@ -94,19 +113,97 @@ impl TwoPlayerGame {
94
113
Ok ( player_id)
95
114
}
96
115
116
+ /// Player places their bet
117
+ pub async fn place_bet ( & mut self , player_id : Uuid ) -> Result < String > {
118
+ if !matches ! ( self . state, GameState :: WaitingForBets ) {
119
+ return Err ( LotteryError :: InvalidState (
120
+ "Not in betting phase" . to_string ( ) ,
121
+ ) ) ;
122
+ }
123
+
124
+ // Check if player already placed bet
125
+ if self . collected_bets . contains_key ( & player_id) {
126
+ return Err ( LotteryError :: InvalidState (
127
+ "Player already placed bet" . to_string ( ) ,
128
+ ) ) ;
129
+ }
130
+
131
+ let player = self
132
+ . players
133
+ . get ( & player_id)
134
+ . ok_or ( LotteryError :: PlayerNotFound ( player_id) ) ?;
135
+
136
+ // Check player has sufficient balance
137
+ let balance = player. wallet ( ) . balance ( ) . await ?;
138
+
139
+ if balance. confirmed < self . bet_amount {
140
+ return Err ( LotteryError :: Internal ( format ! (
141
+ "Insufficient balance: need {} sats, have {} sats" ,
142
+ self . bet_amount. to_sat( ) ,
143
+ balance. confirmed. to_sat( )
144
+ ) ) ) ;
145
+ }
146
+
147
+ // Get pot address
148
+ let pot_address = self . get_pot_address ( ) . await ?;
149
+
150
+ // Send bet to pot
151
+ let txid = player. place_bet ( & pot_address, self . bet_amount ) . await ?;
152
+
153
+ // Record the bet
154
+ let bet_info = BetInfo {
155
+ player_id,
156
+ amount : self . bet_amount ,
157
+ txid : txid. clone ( ) ,
158
+ timestamp : Utc :: now ( ) ,
159
+ } ;
160
+
161
+ self . collected_bets . insert ( player_id, bet_info) ;
162
+ self . total_pot += self . bet_amount ;
163
+
164
+ tracing:: info!(
165
+ "Player {} placed bet of {} sats in game {}: {}" ,
166
+ player_id,
167
+ self . bet_amount. to_sat( ) ,
168
+ self . id,
169
+ txid
170
+ ) ;
171
+
172
+ // Check if both players have bet
173
+ if self . collected_bets . len ( ) == 2 {
174
+ self . state = GameState :: BetsCollected ;
175
+ tracing:: info!( "All bets collected for game {}" , self . id) ;
176
+ }
177
+
178
+ Ok ( txid)
179
+ }
180
+
97
181
/// Start the commitment phase after both players have placed bets
98
182
pub async fn start_commitment_phase ( & mut self ) -> Result < ( ) > {
99
183
if self . players . len ( ) != 2 {
100
184
return Err ( LotteryError :: GameNotReady ) ;
101
185
}
102
186
103
- if !matches ! ( self . state, GameState :: WaitingForBets ) {
187
+ if !matches ! ( self . state, GameState :: BetsCollected ) {
104
188
return Err ( LotteryError :: InvalidState (
105
- "Not ready for commitment phase " . to_string ( ) ,
189
+ "Bets not collected yet " . to_string ( ) ,
106
190
) ) ;
107
191
}
108
192
109
- // commitment deadline 5 minutes from now
193
+ // Verify pot wallet has received the bets
194
+ let pot_balance = self . pot_wallet . balance ( ) . await ?;
195
+ let expected_pot = self . bet_amount * 2u64 ;
196
+
197
+ if pot_balance. confirmed < expected_pot {
198
+ tracing:: warn!(
199
+ "Pot balance {} less than expected {}. Waiting for confirmations..." ,
200
+ pot_balance. confirmed. to_sat( ) ,
201
+ expected_pot. to_sat( )
202
+ ) ;
203
+ // Could wait for confirmations or proceed with pending balance
204
+ }
205
+
206
+ // Set commitment deadline (5 minutes from now)
110
207
self . commitment_deadline = Some ( Utc :: now ( ) + Duration :: minutes ( 5 ) ) ;
111
208
self . state = GameState :: CommitmentPhase ;
112
209
@@ -148,7 +245,7 @@ impl TwoPlayerGame {
148
245
149
246
/// Start the reveal phase
150
247
async fn start_reveal_phase ( & mut self ) -> Result < ( ) > {
151
- // reveal deadline 5 minutes from now
248
+ // Set reveal deadline ( 5 minutes from now)
152
249
self . reveal_deadline = Some ( Utc :: now ( ) + Duration :: minutes ( 5 ) ) ;
153
250
self . state = GameState :: RevealPhase ;
154
251
@@ -186,7 +283,7 @@ impl TwoPlayerGame {
186
283
Ok ( ( ) )
187
284
}
188
285
189
- /// winner is determined using XOR of revealed secrets
286
+ /// Determine winner using XOR of revealed secrets
190
287
async fn determine_winner ( & mut self ) -> Result < ( ) > {
191
288
let player_ids: Vec < Uuid > = self . players . keys ( ) . cloned ( ) . collect ( ) ;
192
289
if player_ids. len ( ) != 2 {
@@ -217,41 +314,92 @@ impl TwoPlayerGame {
217
314
218
315
tracing:: info!( "Game {} completed. Winner: {}" , self . id, winner_id) ;
219
316
220
- // [TODO] transfer the pot to the winner
317
+ // Payout the winner
221
318
self . payout_winner ( winner_id) . await ?;
222
319
223
320
Ok ( ( ) )
224
321
}
225
322
226
- /// Payout the winner
323
+ /// Payout the winner with actual Ark transaction
227
324
async fn payout_winner ( & self , winner_id : Uuid ) -> Result < ( ) > {
228
- let total_pot = self . bet_amount * 2u64 ;
325
+ let winner = self
326
+ . players
327
+ . get ( & winner_id)
328
+ . ok_or ( LotteryError :: PlayerNotFound ( winner_id) ) ?;
329
+
330
+ // Get winner's Ark address
331
+ let winner_address = winner. wallet ( ) . get_ark_address ( ) . await ?;
332
+
333
+ // Send entire pot to winner
334
+ let payout_amount = self . total_pot ;
229
335
230
336
tracing:: info!(
231
- "Game {} payout: Player {} wins {} sats " ,
232
- self . id ,
337
+ "Paying out {} sats to winner {} at address {} " ,
338
+ payout_amount . to_sat ( ) ,
233
339
winner_id,
234
- total_pot . to_sat ( )
340
+ winner_address . address
235
341
) ;
236
342
237
- // [TODO] Ark Tx to transfer pot to winner
238
- // 1. Getting winner's Ark addr
239
- // 2. Creating Tx from pot addr to winner
240
- // 3. Broadcasting the Tx
343
+ // Send from pot wallet to winner
344
+ let txid = self
345
+ . pot_wallet
346
+ . send_ark ( & winner_address. address , payout_amount)
347
+ . await ?;
348
+
349
+ tracing:: info!(
350
+ "Game {} payout completed. Winner {} received {} sats: {}" ,
351
+ self . id,
352
+ winner_id,
353
+ payout_amount. to_sat( ) ,
354
+ txid
355
+ ) ;
241
356
242
357
Ok ( ( ) )
243
358
}
244
359
245
- /// Abort the game
360
+ /// Abort the game and refund bets
246
361
async fn abort_game ( & mut self , reason : String ) -> Result < ( ) > {
247
362
self . state = GameState :: Aborted {
248
363
reason : reason. clone ( ) ,
249
364
} ;
250
365
251
366
tracing:: warn!( "Game {} aborted: {}" , self . id, reason) ;
252
367
253
- // [TODO] refund
254
- // Return bets to players if they were placed
368
+ // Refund bets to players
369
+ self . refund_bets ( ) . await ?;
370
+
371
+ Ok ( ( ) )
372
+ }
373
+
374
+ /// Refund bets to all players
375
+ async fn refund_bets ( & self ) -> Result < ( ) > {
376
+ for ( player_id, bet_info) in & self . collected_bets {
377
+ let player = self
378
+ . players
379
+ . get ( player_id)
380
+ . ok_or ( LotteryError :: PlayerNotFound ( * player_id) ) ?;
381
+
382
+ let player_address = player. wallet ( ) . get_ark_address ( ) . await ?;
383
+
384
+ tracing:: info!(
385
+ "Refunding {} sats to player {} at address {}" ,
386
+ bet_info. amount. to_sat( ) ,
387
+ player_id,
388
+ player_address. address
389
+ ) ;
390
+
391
+ let txid = self
392
+ . pot_wallet
393
+ . send_ark ( & player_address. address , bet_info. amount )
394
+ . await ?;
395
+
396
+ tracing:: info!(
397
+ "Refunded {} sats to player {}: {}" ,
398
+ bet_info. amount. to_sat( ) ,
399
+ player_id,
400
+ txid
401
+ ) ;
402
+ }
255
403
256
404
Ok ( ( ) )
257
405
}
@@ -343,9 +491,10 @@ impl TwoPlayerGame {
343
491
bet_amount : self . bet_amount ,
344
492
state : self . state . clone ( ) ,
345
493
player_count : self . players . len ( ) ,
346
- pot_address : self . pot_address . clone ( ) ,
494
+ total_pot : self . total_pot ,
347
495
commitment_deadline : self . commitment_deadline ,
348
496
reveal_deadline : self . reveal_deadline ,
497
+ collected_bets : self . collected_bets . clone ( ) ,
349
498
}
350
499
}
351
500
@@ -354,7 +503,9 @@ impl TwoPlayerGame {
354
503
}
355
504
356
505
pub fn can_place_bet ( & self , player_id : Uuid ) -> bool {
357
- matches ! ( self . state, GameState :: WaitingForBets ) && self . players . contains_key ( & player_id)
506
+ matches ! ( self . state, GameState :: WaitingForBets )
507
+ && self . players . contains_key ( & player_id)
508
+ && !self . collected_bets . contains_key ( & player_id)
358
509
}
359
510
360
511
pub fn can_commit ( & self , player_id : Uuid ) -> bool {
@@ -381,7 +532,8 @@ pub struct GameInfo {
381
532
pub bet_amount : Amount ,
382
533
pub state : GameState ,
383
534
pub player_count : usize ,
384
- pub pot_address : String ,
535
+ pub total_pot : Amount ,
385
536
pub commitment_deadline : Option < DateTime < Utc > > ,
386
537
pub reveal_deadline : Option < DateTime < Utc > > ,
538
+ pub collected_bets : HashMap < Uuid , BetInfo > ,
387
539
}
0 commit comments