Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 214 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions facade/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ getrandom.workspace = true
clap = { workspace = true, features = ["derive"] }
rusqlite = { workspace = true, features = ["bundled"] }
regex.workspace = true
sha2.workspace = true
postgres = "=0.19.7"
tokio-postgres = "=0.7.10"
postgres-protocol = "=0.6.7"
postgres-types = "=0.2.8"
32 changes: 32 additions & 0 deletions facade/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# A server to simulate a blockchain for development and test

## Current local storage behavior

`race-facade` now uses:

- sqlite for gameplay/runtime-facing facade state
- Postgres for product-layer guest data when available

Default local dev behavior:

- sqlite db path: `data/facade.sqlite3`
- default Postgres product db url: `postgresql://postgres@localhost/race_poker_product`

Startup priority:

1. `--product-db-url`
2. `RACE_FACADE_PRODUCT_DB_URL`
3. default local Postgres dev db
4. sqlite-only fallback if the default local Postgres db is unavailable

To force sqlite-only mode even when local Postgres exists:

```powershell
$env:RACE_FACADE_DISABLE_DEFAULT_PRODUCT_DB='1'
cargo run -p race-facade
```

To initialize the local Postgres product db:

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\init_product_db.ps1
```
19 changes: 19 additions & 0 deletions facade/scripts/init_product_db.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
param(
[string]$DbName = "race_poker_product",
[string]$User = "postgres",
[string]$AdminDb = "postgres"
)

$ErrorActionPreference = "Stop"

$schemaPath = Join-Path $PSScriptRoot "..\\sql\\product_schema_v1.sql"
$schemaPath = [System.IO.Path]::GetFullPath($schemaPath)

$dbExists = psql -U $User -d $AdminDb -Atc "SELECT 1 FROM pg_database WHERE datname = '$DbName';"
if (-not $dbExists) {
psql -U $User -d $AdminDb -c "CREATE DATABASE $DbName;"
}

psql -U $User -d $DbName -f $schemaPath

Write-Host "Initialized Postgres product DB '$DbName' using schema $schemaPath"
48 changes: 48 additions & 0 deletions facade/sql/product_schema_v1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
CREATE TABLE IF NOT EXISTS guest_account (
guest_id TEXT PRIMARY KEY,
player_addr TEXT NOT NULL UNIQUE,
nick TEXT NOT NULL,
status TEXT NOT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
);

CREATE TABLE IF NOT EXISTS guest_session (
session_id TEXT PRIMARY KEY,
guest_id TEXT NOT NULL REFERENCES guest_account(guest_id),
session_token_hash TEXT NOT NULL UNIQUE,
created_at BIGINT NOT NULL,
expires_at BIGINT NOT NULL,
revoked_at BIGINT
);

CREATE TABLE IF NOT EXISTS user_progress (
guest_id TEXT PRIMARY KEY REFERENCES guest_account(guest_id),
rank_tier TEXT NOT NULL,
xp BIGINT NOT NULL,
level INTEGER NOT NULL,
updated_at BIGINT NOT NULL
);

CREATE TABLE IF NOT EXISTS user_rating (
guest_id TEXT PRIMARY KEY REFERENCES guest_account(guest_id),
rating INTEGER NOT NULL,
rank_bucket TEXT NOT NULL,
updated_at BIGINT NOT NULL
);

CREATE TABLE IF NOT EXISTS user_stats (
guest_id TEXT PRIMARY KEY REFERENCES guest_account(guest_id),
hands_played BIGINT NOT NULL DEFAULT 0,
games_played BIGINT NOT NULL DEFAULT 0,
wins BIGINT NOT NULL DEFAULT 0,
losses BIGINT NOT NULL DEFAULT 0,
last_played_at BIGINT
);

CREATE TABLE IF NOT EXISTS product_event_log (
event_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
guest_id TEXT NOT NULL REFERENCES guest_account(guest_id),
created_at BIGINT NOT NULL
);
222 changes: 215 additions & 7 deletions facade/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
use std::{fs::File, io::Read};
use std::{fs::File, io::Read, path::Path};

use crate::{
db::{
create_game_account, create_game_bundle, create_player_info, create_recipient_account,
create_server_account, create_stake, create_token_account, list_game_accounts,
create_server_account, create_stake, create_token_account, create_guest_account,
create_guest_session, initialize_product_state, list_game_accounts,
increment_user_hands_played, increment_user_losses, increment_user_wins,
insert_product_event_log_entry,
list_token_accounts, prepare_all_tables, read_game_account, read_game_bundle,
read_player_info, read_recipient_account, read_registration_account, read_server_account,
read_token_account, update_game_account, update_player_info, update_recipient_account,
update_stake, read_stake, PlayerInfo, Stake,
read_token_account, read_guest_account_by_guest_id, read_guest_account_by_player_addr,
read_guest_session_by_token_hash, read_user_progress, read_user_rating, read_user_stats,
record_user_joined_game, revoke_guest_session, update_game_account,
update_user_progress, update_user_rating, ProductEventLogEntry,
update_player_info, update_recipient_account, update_stake, read_stake, GuestAccount,
GuestSession, PlayerInfo, Stake, UserProgress, UserRating, UserStats,
},
product_store::ProductStore,
GameSpec,
};
use race_core::types::{
Expand All @@ -19,17 +27,52 @@ use rusqlite::Connection;

pub struct Context {
conn: Connection,
product_store: Option<ProductStore>,
}

impl Default for Context {
fn default() -> Self {
let conn = Connection::open_in_memory().unwrap();
prepare_all_tables(&conn).unwrap();
Context { conn }
Self::in_memory()
}
}

impl Context {
pub fn in_memory() -> Self {
let conn = Connection::open_in_memory().unwrap();
prepare_all_tables(&conn).unwrap();
Context {
conn,
product_store: None,
}
}

#[allow(dead_code)]
pub fn open_sqlite<P: AsRef<Path>>(db_path: P) -> anyhow::Result<Self> {
Self::open_sqlite_with_product_store(db_path, None)
}

pub fn open_sqlite_with_product_store<P: AsRef<Path>>(
db_path: P,
product_db_url: Option<&str>,
) -> anyhow::Result<Self> {
let db_path = db_path.as_ref();
if let Some(parent) = db_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}

let conn = Connection::open(db_path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
prepare_all_tables(&conn)?;
let product_store = match product_db_url {
Some(url) => Some(ProductStore::connect(url)?),
None => None,
};
Ok(Context { conn, product_store })
}

pub fn load_games(&self, spec_paths: &[&str]) -> anyhow::Result<()> {
for spec_path in spec_paths.iter() {
self.add_game(spec_path)?;
Expand Down Expand Up @@ -78,6 +121,13 @@ impl Context {
icon: "https://raw.githubusercontent.com/NutsPokerTeam/token-list/main/assets/mainnet/RACE5fnTKB9obGtCusArTQ6hhdNXAtf3HarvJM17rxJ/logo.svg".into(),
addr: "FACADE_RACE".into(),
})?;
self.add_token(TokenAccount {
name: "Guest Chips".into(),
symbol: "GCHIP".into(),
decimals: 0,
icon: "".into(),
addr: "FACADE_GUEST_CHIPS".into(),
})?;
Ok(())
}

Expand Down Expand Up @@ -164,6 +214,12 @@ impl Context {
Ok(())
}

#[cfg(test)]
pub fn create_stake(&self, stake: &Stake) -> anyhow::Result<()> {
create_stake(&self.conn, stake)?;
Ok(())
}

pub fn create_recipient_account(
&self,
recipient_account: &RecipientAccount,
Expand All @@ -179,6 +235,25 @@ impl Context {
Ok(())
}

pub fn create_guest_account(&mut self, guest_account: &GuestAccount) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.create_guest_account(guest_account)?;
} else {
create_guest_account(&self.conn, guest_account)?;
initialize_product_state(&self.conn, &guest_account.guest_id, guest_account.created_at)?;
}
Ok(())
}

pub fn create_guest_session(&mut self, guest_session: &GuestSession) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.create_guest_session(guest_session)?;
} else {
create_guest_session(&self.conn, guest_session)?;
}
Ok(())
}

pub fn get_game_bundle(&self, addr: &str) -> anyhow::Result<Option<GameBundle>> {
Ok(read_game_bundle(&self.conn, addr)?)
}
Expand All @@ -203,6 +278,26 @@ impl Context {
Ok(read_player_info(&self.conn, player_addr)?)
}

pub fn get_guest_account_by_guest_id(
&mut self,
guest_id: &str,
) -> anyhow::Result<Option<GuestAccount>> {
if let Some(store) = self.product_store.as_mut() {
return store.read_guest_account_by_guest_id(guest_id);
}
Ok(read_guest_account_by_guest_id(&self.conn, guest_id)?)
}

pub fn get_guest_session_by_token_hash(
&mut self,
session_token_hash: &str,
) -> anyhow::Result<Option<GuestSession>> {
if let Some(store) = self.product_store.as_mut() {
return store.read_guest_session_by_token_hash(session_token_hash);
}
Ok(read_guest_session_by_token_hash(&self.conn, session_token_hash)?)
}

#[allow(unused)]
pub fn get_registration_account(
&self,
Expand Down Expand Up @@ -234,6 +329,119 @@ impl Context {
Ok(())
}

pub fn revoke_guest_session(
&mut self,
session_token_hash: &str,
revoked_at: u64,
) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.revoke_guest_session(session_token_hash, revoked_at)?;
} else {
revoke_guest_session(&self.conn, session_token_hash, revoked_at)?;
}
Ok(())
}

pub fn get_user_progress(&mut self, guest_id: &str) -> anyhow::Result<Option<UserProgress>> {
if let Some(store) = self.product_store.as_mut() {
store.read_user_progress(guest_id)
} else {
Ok(read_user_progress(&self.conn, guest_id)?)
}
}

pub fn get_guest_account_by_player_addr(
&mut self,
player_addr: &str,
) -> anyhow::Result<Option<GuestAccount>> {
if let Some(store) = self.product_store.as_mut() {
store.read_guest_account_by_player_addr(player_addr)
} else {
Ok(read_guest_account_by_player_addr(&self.conn, player_addr)?)
}
}

pub fn get_user_rating(&mut self, guest_id: &str) -> anyhow::Result<Option<UserRating>> {
if let Some(store) = self.product_store.as_mut() {
store.read_user_rating(guest_id)
} else {
Ok(read_user_rating(&self.conn, guest_id)?)
}
}

pub fn get_user_stats(&mut self, guest_id: &str) -> anyhow::Result<Option<UserStats>> {
if let Some(store) = self.product_store.as_mut() {
store.read_user_stats(guest_id)
} else {
Ok(read_user_stats(&self.conn, guest_id)?)
}
}

pub fn record_user_joined_game(&mut self, guest_id: &str, now: u64) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.record_user_joined_game(guest_id, now)?;
} else {
record_user_joined_game(&self.conn, guest_id, now)?;
}
Ok(())
}

pub fn record_product_event_once(
&mut self,
entry: ProductEventLogEntry,
) -> anyhow::Result<bool> {
if let Some(store) = self.product_store.as_mut() {
store.insert_product_event_log_entry(&entry)
} else {
Ok(insert_product_event_log_entry(&self.conn, &entry)?)
}
}

pub fn update_user_progress(&mut self, progress: &UserProgress) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.update_user_progress(progress)?;
} else {
update_user_progress(&self.conn, progress)?;
}
Ok(())
}

pub fn increment_user_hands_played(&mut self, guest_id: &str) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.increment_user_hands_played(guest_id)?;
} else {
increment_user_hands_played(&self.conn, guest_id)?;
}
Ok(())
}

pub fn increment_user_wins(&mut self, guest_id: &str) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.increment_user_wins(guest_id)?;
} else {
increment_user_wins(&self.conn, guest_id)?;
}
Ok(())
}

pub fn increment_user_losses(&mut self, guest_id: &str) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.increment_user_losses(guest_id)?;
} else {
increment_user_losses(&self.conn, guest_id)?;
}
Ok(())
}

pub fn update_user_rating(&mut self, rating: &UserRating) -> anyhow::Result<()> {
if let Some(store) = self.product_store.as_mut() {
store.update_user_rating(rating)?;
} else {
update_user_rating(&self.conn, rating)?;
}
Ok(())
}

pub fn update_player_info(&self, player_info: &PlayerInfo) -> anyhow::Result<()> {
update_player_info(&self.conn, &player_info)?;
Ok(())
Expand Down
Loading
Loading