diff --git a/README.md b/README.md index 9b78c7f..50c6ec8 100644 --- a/README.md +++ b/README.md @@ -4,66 +4,93 @@ ### Parameter - Game settings and contsraint -- `MaxPlayers`: Int 2 to Int 9 -- `MinBuyIn`: AdaInt -- `BigBlind`: AdaInt -- `SmallBlind`: AdaInt -- `TableDealer`: Also fee collector address -- `IsTablePrivateOrOpen`: Bool +- `TableDealer`: PubKeyHash + The only pubKeyHash able to sign leave table transactions. ### Datum -- `ActiveGame`: - - `ActivePlayersList`: [Player] +- `ActiveOpenGame`: + + - `ActivePlayersList`: [Address] + - `TotalDeposit`: Int + - `MaxPlayers`: Int + - `MinBuyInLovelace`: Int + +- `ActivePrivateGame`: + - `ActivePlayersList`: [Address] + - `TotalDeposit`: Int - `MaxPlayers`: Int - `MinBuyInLovelace`: Int - - `Player`: PlayerAddress && Balance + - `AuthorizedPlayers`: [PubKeyHash] ### Redeemer -- `JoinTable`: xxxxxxxxxxxxxxxxx -- `LeaveTable`: xxxxxxxxxxxxxxxxx -- `TopUpBalance`: xxxxxxxxxxxxxxxx +- `JoinTable`: + + - Address + - DepositAmount + +- `LeaveTable`: + + - Address + - WithdrawalAmount + +- `TopUpBalance`: + - Address + - DepositAmount ### User Action 1. A player creates a table - Each table is a validator script - - Sets big/small blind - - Sets Minimum buy-in - - Sets maximum amount of players (minimum 2, up to 9) - - Sets private/open table - - If private, set which addresses are allowed to join + - Note: + - Sets big/small blind + - Sets Minimum buy-in + - Sets maximum amount of players (minimum 2, up to 9) + - Sets private/open table + - If private, set which addresses are allowed to join 2. A player join the table - Redeemer `JoinTable` + - Script Context, looking at Transaction: + + - Only 1 input UTXO from current script address + - Only 1 output to current script address + - Update Datum ActivePlayersList and TotalDeposit + - Check input values and output matches with supposed change in TotalDeposit. + - Check whether table is private or not - - If private, is player address authorized to join - - - - Check player address and balance + - If private, check if member in `AuthorizedPlayers` in datum - Check that table is not full - Check player deposit > Minimum buy-in - - Deposit ADA in validator - - Update Datum ActivePlayersList + + - Note: + - Check player address and balance + - Build Tx: + - Player signs Tx + - Deposit ADA in validator 3. A player tops up balance - Redeemer `TopUpBalance` - - Check balance at player address - Check that top up > minimum buy-in - Build Tx: - Player signs Tx - Deposit ADA in validator - - Update Datum ActivePlayersList + - Update Datum ActivePlayersList and TotalDeposit - Update in-game balance + - Note: Check balance at player address + 4. A player leaves the table - Redeemer `LeaveTable` - - Check that player in active player list + - Check that player in activePlayer list - Check player in-game balance - Match in-game balance with transaction validation + - Update Datum ActivePlayersList and TotalDeposit - Build Tx: - - Player signs Tx + - Admin signs Tx - Withdraw ADA - - Update Datum ActivePlayersList + +Note: Player does not need to sign `LeaveTable` Tx. This way, we can control the flow and kick inactive players out ### Notable Points diff --git a/aiken.lock b/aiken.lock index 33fb760..4dddde5 100644 --- a/aiken.lock +++ b/aiken.lock @@ -8,7 +8,7 @@ source = "github" [[requirements]] name = "sidan-lab/aiken-utils" -version = "0.0.1-beta" +version = "0.0.4-beta" source = "github" [[packages]] @@ -19,7 +19,7 @@ source = "github" [[packages]] name = "sidan-lab/aiken-utils" -version = "0.0.1-beta" +version = "0.0.4-beta" requirements = [] source = "github" diff --git a/aiken.toml b/aiken.toml index e823fec..7e8c8cb 100644 --- a/aiken.toml +++ b/aiken.toml @@ -15,5 +15,5 @@ source = "github" [[dependencies]] name = "sidan-lab/aiken-utils" -version = "0.0.1-beta" +version = "0.0.4-beta" source = "github" diff --git a/validators/poker_game.ak b/validators/poker_game.ak index 373bcf0..6798ba7 100644 --- a/validators/poker_game.ak +++ b/validators/poker_game.ak @@ -1,14 +1,25 @@ -use aiken/transaction.{ScriptContext, Spend} +use aiken/list +use aiken/transaction.{InlineDatum, ScriptContext, Spend, Transaction, find_input} use aiken/transaction/credential.{Address} +use sidan_utils/inputs.{inputs_at} +use sidan_utils/outputs.{outputs_at} +use sidan_utils/value.{value_geq} as sidan_value +use aiken/transaction/value.{Value, add, lovelace_of} type GameDatum { - ActiveGame { + ActiveOpenGame { + active_players_list: List
, + total_deposit: Int, + max_players: Int, + min_buy_in_lovelace: Int, + } + + ActivePrivateGame { + active_players_list: List
, + authorized_players_list: List
, + total_deposit: Int, max_players: Int, min_buy_in_lovelace: Int, - big_blind: Int, - small_blind: Int, - table_dealer: Address, - current_players_and_balance: List
, } } @@ -25,23 +36,192 @@ validator(fee_collector: Address) { context: ScriptContext, ) { let ScriptContext { purpose, transaction } = context - expect Spend(_) = purpose + let Transaction {inputs, reference_inputs, outputs, fee, redeemers, datums, ..} = transaction + expect Spend(utxo) = purpose when redeemer is { + JoinTable { new_player } -> { - let is_min_buy_in_met = False - let is_table_full = False - let is_player_already_at_table = False - let is_datum_updated = False + expect Some(own_input) = find_input(inputs, utxo) + let own_address = own_input.output.address + when (inputs_at(inputs, own_address), outputs_at(outputs, own_address)) is { + ([only_input_from_script], [only_output_to_script]) -> { + let input_value = only_input_from_script.output.value + let output_value = only_output_to_script.value + + when datum is { + ActiveOpenGame {active_players_list, total_deposit, max_players, min_buy_in_lovelace} -> { + + expect InlineDatum(output_inline_datum) = only_output_to_script.datum + expect ActiveOpenGame { .. } : GameDatum = output_inline_datum + expect parsed_output_datum : GameDatum = output_inline_datum + // let lovelace_input_value : Int = lovelace_of(input_value) + let lovelace_output_value : Int = lovelace_of(output_value) + // All conditions that must be satisfied + and { + does_table_have_seats(active_players_list, max_players), + is_player_already_at_table(active_players_list, new_player), + is_min_buy_in_met(min_buy_in_lovelace, input_value, output_value), + is_datum_updated_open(datum, parsed_output_datum, lovelace_output_value, new_player) + } + } - is_min_buy_in_met && is_room_having_vacancy && is_player_already_at_table + ActivePrivateGame{active_players_list, authorized_players_list, total_deposit, max_players, min_buy_in_lovelace} -> { + expect InlineDatum(output_inline_datum) = only_output_to_script.datum + expect ActivePrivateGame { .. } : GameDatum = output_inline_datum + expect parsed_output_datum : GameDatum = output_inline_datum + // let lovelace_input_value : Int = lovelace_of(input_value) + let lovelace_output_value : Int = lovelace_of(output_value) + + // All conditions that must be satisfied + and { + is_player_authorized(authorized_players_list, new_player), + does_table_have_seats(active_players_list, max_players), + is_player_already_at_table(active_players_list, new_player), + is_min_buy_in_met(min_buy_in_lovelace, input_value, output_value), + is_datum_updated_private(datum, parsed_output_datum, lovelace_output_value, new_player) + } + } + } + } + _ -> False + } } + + + + TopUp { new_balance } -> { + expect Some(own_input) = find_input(inputs, utxo) + let own_address = own_input.output.address + when (inputs_at(inputs, own_address), outputs_at(outputs, own_address)) is { + ([input], [output]) -> { + let input_value = input.output.value + let output_value = output.value + + when datum is { + ActiveOpenGame {min_buy_in_lovelace, ..} -> { + + expect InlineDatum(output_inline_datum) = output.datum + expect ActiveOpenGame { .. } : GameDatum = output_inline_datum + expect parsed_output_datum : GameDatum = output_inline_datum + // let lovelace_input_value : Int = lovelace_of(input_value) + let lovelace_output_value : Int = lovelace_of(output_value) + // All conditions that must be satisfied + and { + is_min_buy_in_met(min_buy_in_lovelace, input_value, output_value), + is_datum_updated_topup_open(datum, parsed_output_datum, lovelace_output_value, new_balance) + } + } + + ActivePrivateGame{min_buy_in_lovelace, ..} -> { + expect InlineDatum(output_inline_datum) = output.datum + expect ActivePrivateGame { .. } : GameDatum = output_inline_datum + expect parsed_output_datum : GameDatum = output_inline_datum + // let lovelace_input_value : Int = lovelace_of(input_value) + let lovelace_output_value : Int = lovelace_of(output_value) + + // All conditions that must be satisfied + and { + is_min_buy_in_met(min_buy_in_lovelace, input_value, output_value), + is_datum_updated_topup_private(datum, parsed_output_datum, lovelace_output_value, new_balance) + } + } + } + } + _ -> False + } + } + LeaveTable { player_to_remove } -> { - let is_player_removed = False - let is_money_returned = False + // let is_player_removed = False + // let is_money_returned = False - is_player_removed && is_money_returned + // is_player_removed && is_money_returned + False } } } } + + + + +///// Supporting functions for JoinTable redeemer + +// Static checking + +fn is_player_authorized(authorized_players_list, new_player) -> Bool { + list.has(authorized_players_list, new_player) +} + +fn is_player_already_at_table(active_players_list, new_player) -> Bool { + list.has(active_players_list, new_player) +} + +fn does_table_have_seats(active_players_list, max_players) -> Bool { + list.length(active_players_list) < max_players +} + + +// Dynamic checking +fn is_min_buy_in_met(min_buy_in_lovelace : Int, input_value: Value, output_value: Value) -> Bool { + let is_buy_in_provided = value_geq(output_value |> add("", "", min_buy_in_lovelace), input_value) + is_buy_in_provided + } + +fn is_datum_updated_open(input_datum: GameDatum, output_datum: GameDatum, output_value: Int, new_player: Address) -> Bool { + // We are only updating the active_players_list and total_deposit + expect ActiveOpenGame {active_players_list: input_active_players_list, .. } : GameDatum = input_datum + expect ActiveOpenGame {active_players_list: output_active_players_list, total_deposit: output_total_deposit, ..} : GameDatum = output_datum + + + let is_new_player_added = list.length(input_active_players_list) + 1 == list.length(output_active_players_list) + && list.has(output_active_players_list, new_player) + + let is_total_deposit_updated = output_total_deposit == output_value + // && output_total_deposit - input_total_deposit == output_value - input_value + + is_new_player_added && is_total_deposit_updated +} + +fn is_datum_updated_private(input_datum: GameDatum, output_datum: GameDatum, output_value: Int, new_player: Address) -> Bool { + // We are only updating the active_players_list and total_deposit + expect ActivePrivateGame {active_players_list: input_active_players_list, ..} : GameDatum = input_datum + expect ActivePrivateGame {active_players_list: output_active_players_list, total_deposit: output_total_deposit, ..} : GameDatum = output_datum + + + let is_new_player_added = list.length(input_active_players_list) + 1 == list.length(output_active_players_list) + && list.has(output_active_players_list, new_player) + + let is_total_deposit_updated = output_total_deposit == output_value + // && output_total_deposit - input_total_deposit == output_value - input_value + + is_new_player_added && is_total_deposit_updated +} + + +///// Supporting functions for TopUp redeemer +fn is_datum_updated_topup_open(input_datum: GameDatum, output_datum: GameDatum, output_value: Int, new_balance: Int) { + expect ActiveOpenGame { total_deposit: input_total_deposit, ..} : GameDatum = input_datum + expect ActiveOpenGame { total_deposit: output_total_deposit, ..} : GameDatum = output_datum + + let is_total_deposit_updated = output_total_deposit == output_value + // && output_total_deposit - input_total_deposit == output_value - input_value + // && new_balance - output_value == input_value + + is_total_deposit_updated +} + +fn is_datum_updated_topup_private(input_datum: GameDatum, output_datum: GameDatum, output_value: Int, new_balance: Int) { + expect ActivePrivateGame {total_deposit: input_total_deposit, ..} : GameDatum = input_datum + expect ActivePrivateGame {total_deposit: output_total_deposit, ..} : GameDatum = output_datum + + let is_total_deposit_updated = output_total_deposit == output_value + // && output_total_deposit - input_total_deposit == output_value - input_value + // && new_balance - output_value == input_value + + is_total_deposit_updated +} + + +///// Supporting functions for LeaveTable redeemer