Skip to content

Commit 8c620c5

Browse files
committed
real betting & pot management
Signed-off-by: Dikshant <[email protected]>
1 parent 79fa34f commit 8c620c5

File tree

5 files changed

+559
-104
lines changed

5 files changed

+559
-104
lines changed

arkive-lottery/src/error.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ pub enum LotteryError {
3232
#[error("Timeout expired")]
3333
TimeoutExpired,
3434

35+
#[error("Bet already placed by player: {0}")]
36+
BetAlreadyPlaced(Uuid),
37+
38+
#[error("Insufficient balance: need {need} sats, have {available} sats")]
39+
InsufficientBalance { need: u64, available: u64 },
40+
41+
#[error("Bet placement failed: {0}")]
42+
BetPlacementFailed(String),
43+
44+
#[error("Payout failed: {0}")]
45+
PayoutFailed(String),
46+
47+
#[error("Refund failed: {0}")]
48+
RefundFailed(String),
49+
50+
#[error("Pot wallet error: {0}")]
51+
PotWalletError(String),
52+
3553
#[error("Cryptographic error: {0}")]
3654
Crypto(String),
3755

arkive-lottery/src/game.rs

Lines changed: 181 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,45 @@ use uuid::Uuid;
1111
pub enum GameState {
1212
WaitingForPlayers,
1313
WaitingForBets,
14+
BetsCollected,
1415
CommitmentPhase,
1516
RevealPhase,
1617
Completed { winner: Uuid },
1718
Aborted { reason: String },
1819
}
1920

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+
2129
pub struct TwoPlayerGame {
2230
id: Uuid,
2331
bet_amount: Amount,
2432
state: GameState,
2533
players: HashMap<Uuid, Player>,
34+
pot_wallet: Arc<ArkWallet>, // wallet for the pot
35+
collected_bets: HashMap<Uuid, BetInfo>,
2636
commitment_deadline: Option<DateTime<Utc>>,
2737
reveal_deadline: Option<DateTime<Utc>>,
28-
pot_address: String,
38+
total_pot: Amount,
2939
}
3040

3141
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> {
3543
Ok(Self {
3644
id: Uuid::new_v4(),
3745
bet_amount,
3846
state: GameState::WaitingForPlayers,
3947
players: HashMap::new(),
48+
pot_wallet,
49+
collected_bets: HashMap::new(),
4050
commitment_deadline: None,
4151
reveal_deadline: None,
42-
pot_address,
52+
total_pot: Amount::ZERO,
4353
})
4454
}
4555

@@ -59,14 +69,23 @@ impl TwoPlayerGame {
5969
self.players.len()
6070
}
6171

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
6479
}
6580

6681
pub fn players(&self) -> Vec<Uuid> {
6782
self.players.keys().cloned().collect()
6883
}
6984

85+
pub fn get_bet_info(&self, player_id: Uuid) -> Option<&BetInfo> {
86+
self.collected_bets.get(&player_id)
87+
}
88+
7089
/// Add a player to the game
7190
pub async fn add_player(&mut self, wallet: Arc<ArkWallet>) -> Result<Uuid> {
7291
if self.players.len() >= 2 {
@@ -94,19 +113,97 @@ impl TwoPlayerGame {
94113
Ok(player_id)
95114
}
96115

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+
97181
/// Start the commitment phase after both players have placed bets
98182
pub async fn start_commitment_phase(&mut self) -> Result<()> {
99183
if self.players.len() != 2 {
100184
return Err(LotteryError::GameNotReady);
101185
}
102186

103-
if !matches!(self.state, GameState::WaitingForBets) {
187+
if !matches!(self.state, GameState::BetsCollected) {
104188
return Err(LotteryError::InvalidState(
105-
"Not ready for commitment phase".to_string(),
189+
"Bets not collected yet".to_string(),
106190
));
107191
}
108192

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)
110207
self.commitment_deadline = Some(Utc::now() + Duration::minutes(5));
111208
self.state = GameState::CommitmentPhase;
112209

@@ -148,7 +245,7 @@ impl TwoPlayerGame {
148245

149246
/// Start the reveal phase
150247
async fn start_reveal_phase(&mut self) -> Result<()> {
151-
// reveal deadline 5 minutes from now
248+
// Set reveal deadline (5 minutes from now)
152249
self.reveal_deadline = Some(Utc::now() + Duration::minutes(5));
153250
self.state = GameState::RevealPhase;
154251

@@ -186,7 +283,7 @@ impl TwoPlayerGame {
186283
Ok(())
187284
}
188285

189-
/// winner is determined using XOR of revealed secrets
286+
/// Determine winner using XOR of revealed secrets
190287
async fn determine_winner(&mut self) -> Result<()> {
191288
let player_ids: Vec<Uuid> = self.players.keys().cloned().collect();
192289
if player_ids.len() != 2 {
@@ -217,41 +314,92 @@ impl TwoPlayerGame {
217314

218315
tracing::info!("Game {} completed. Winner: {}", self.id, winner_id);
219316

220-
// [TODO] transfer the pot to the winner
317+
// Payout the winner
221318
self.payout_winner(winner_id).await?;
222319

223320
Ok(())
224321
}
225322

226-
/// Payout the winner
323+
/// Payout the winner with actual Ark transaction
227324
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;
229335

230336
tracing::info!(
231-
"Game {} payout: Player {} wins {} sats",
232-
self.id,
337+
"Paying out {} sats to winner {} at address {}",
338+
payout_amount.to_sat(),
233339
winner_id,
234-
total_pot.to_sat()
340+
winner_address.address
235341
);
236342

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+
);
241356

242357
Ok(())
243358
}
244359

245-
/// Abort the game
360+
/// Abort the game and refund bets
246361
async fn abort_game(&mut self, reason: String) -> Result<()> {
247362
self.state = GameState::Aborted {
248363
reason: reason.clone(),
249364
};
250365

251366
tracing::warn!("Game {} aborted: {}", self.id, reason);
252367

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+
}
255403

256404
Ok(())
257405
}
@@ -343,9 +491,10 @@ impl TwoPlayerGame {
343491
bet_amount: self.bet_amount,
344492
state: self.state.clone(),
345493
player_count: self.players.len(),
346-
pot_address: self.pot_address.clone(),
494+
total_pot: self.total_pot,
347495
commitment_deadline: self.commitment_deadline,
348496
reveal_deadline: self.reveal_deadline,
497+
collected_bets: self.collected_bets.clone(),
349498
}
350499
}
351500

@@ -354,7 +503,9 @@ impl TwoPlayerGame {
354503
}
355504

356505
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)
358509
}
359510

360511
pub fn can_commit(&self, player_id: Uuid) -> bool {
@@ -381,7 +532,8 @@ pub struct GameInfo {
381532
pub bet_amount: Amount,
382533
pub state: GameState,
383534
pub player_count: usize,
384-
pub pot_address: String,
535+
pub total_pot: Amount,
385536
pub commitment_deadline: Option<DateTime<Utc>>,
386537
pub reveal_deadline: Option<DateTime<Utc>>,
538+
pub collected_bets: HashMap<Uuid, BetInfo>,
387539
}

arkive-lottery/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ pub mod player;
1010

1111
pub use commitment::{Commitment, CommitmentScheme, HashCommitment};
1212
pub use error::{LotteryError, Result};
13-
pub use game::{GameState, TwoPlayerGame};
13+
pub use game::{BetInfo, GameState, TwoPlayerGame};
1414
pub use player::{Player, PlayerState};
1515

16-
use arkive_core::Amount;
16+
use arkive_core::{Amount, ArkWallet};
1717
use std::sync::Arc;
1818
use uuid::Uuid;
1919

20-
/// Create a new 2-player lottery game
21-
pub async fn create_game(bet_amount: Amount) -> Result<TwoPlayerGame> {
22-
TwoPlayerGame::new(bet_amount).await
20+
/// Create a new 2-player lottery game with a dedicated pot wallet
21+
pub async fn create_game(bet_amount: Amount, pot_wallet: Arc<ArkWallet>) -> Result<TwoPlayerGame> {
22+
TwoPlayerGame::new(bet_amount, pot_wallet).await
2323
}
2424

2525
/// Join an existing game as the second player
26-
pub async fn join_game(_game_id: Uuid, wallet: arkive_core::ArkWallet) -> Result<Player> {
27-
Player::new(Arc::new(wallet)).await
26+
pub async fn join_game(_game_id: Uuid, wallet: Arc<ArkWallet>) -> Result<Player> {
27+
Player::new(wallet).await
2828
}

0 commit comments

Comments
 (0)