Add tic-tac-toe dashboard program
This commit is contained in:
@ -27,6 +27,7 @@ use std::time::Instant;
|
|||||||
use storage_program::StorageProgram;
|
use storage_program::StorageProgram;
|
||||||
use system_program::SystemProgram;
|
use system_program::SystemProgram;
|
||||||
use system_transaction::SystemTransaction;
|
use system_transaction::SystemTransaction;
|
||||||
|
use tictactoe_dashboard_program::TicTacToeDashboardProgram;
|
||||||
use tictactoe_program::TicTacToeProgram;
|
use tictactoe_program::TicTacToeProgram;
|
||||||
use timing::{duration_as_us, timestamp};
|
use timing::{duration_as_us, timestamp};
|
||||||
use transaction::Transaction;
|
use transaction::Transaction;
|
||||||
@ -433,6 +434,12 @@ impl Bank {
|
|||||||
{
|
{
|
||||||
return Err(BankError::ProgramRuntimeError(program_index as u8));
|
return Err(BankError::ProgramRuntimeError(program_index as u8));
|
||||||
}
|
}
|
||||||
|
} else if TicTacToeDashboardProgram::check_id(&tx_program_id) {
|
||||||
|
if TicTacToeDashboardProgram::process_transaction(&tx, program_index, program_accounts)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Err(BankError::ProgramRuntimeError(program_index as u8));
|
||||||
|
}
|
||||||
} else if self.loaded_contract(tx_program_id, tx, program_index, program_accounts) {
|
} else if self.loaded_contract(tx_program_id, tx, program_index, program_accounts) {
|
||||||
} else {
|
} else {
|
||||||
return Err(BankError::UnknownContractId(program_index as u8));
|
return Err(BankError::UnknownContractId(program_index as u8));
|
||||||
|
@ -62,6 +62,7 @@ pub mod streamer;
|
|||||||
pub mod system_program;
|
pub mod system_program;
|
||||||
pub mod system_transaction;
|
pub mod system_transaction;
|
||||||
pub mod thin_client;
|
pub mod thin_client;
|
||||||
|
pub mod tictactoe_dashboard_program;
|
||||||
pub mod tictactoe_program;
|
pub mod tictactoe_program;
|
||||||
pub mod timing;
|
pub mod timing;
|
||||||
pub mod tpu;
|
pub mod tpu;
|
||||||
|
169
src/tictactoe_dashboard_program.rs
Normal file
169
src/tictactoe_dashboard_program.rs
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
//! tic-tac-toe dashboard program
|
||||||
|
|
||||||
|
use serde_cbor;
|
||||||
|
use solana_program_interface::account::Account;
|
||||||
|
use solana_program_interface::pubkey::Pubkey;
|
||||||
|
use tictactoe_program::{Error, Game, Result, State, TicTacToeProgram};
|
||||||
|
use transaction::Transaction;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct TicTacToeDashboardProgram {
|
||||||
|
pending: Pubkey, // Latest pending game
|
||||||
|
completed: Vec<Pubkey>, // Last N completed games (0 is the latest)
|
||||||
|
total: usize, // Total number of completed games
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TICTACTOE_DASHBOARD_PROGRAM_ID: [u8; 32] = [
|
||||||
|
4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
impl TicTacToeDashboardProgram {
|
||||||
|
fn deserialize(input: &[u8]) -> Result<TicTacToeDashboardProgram> {
|
||||||
|
if input.len() < 2 {
|
||||||
|
Err(Error::InvalidUserdata)?;
|
||||||
|
}
|
||||||
|
let len = input[0] as usize + (0xFF * input[1] as usize);
|
||||||
|
if len == 0 {
|
||||||
|
Ok(TicTacToeDashboardProgram::default())
|
||||||
|
} else if input.len() < len + 2 {
|
||||||
|
Err(Error::InvalidUserdata)
|
||||||
|
} else {
|
||||||
|
serde_cbor::from_slice(&input[2..(2 + len)]).map_err(|err| {
|
||||||
|
error!("Unable to deserialize game: {:?}", err);
|
||||||
|
Error::InvalidUserdata
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
self: &mut TicTacToeDashboardProgram,
|
||||||
|
game_pubkey: &Pubkey,
|
||||||
|
game: &Game,
|
||||||
|
) -> Result<()> {
|
||||||
|
match game.state {
|
||||||
|
State::Waiting => {
|
||||||
|
self.pending = *game_pubkey;
|
||||||
|
}
|
||||||
|
State::XMove | State::OMove => {
|
||||||
|
// Nothing to do. In progress games are not managed by the dashboard
|
||||||
|
}
|
||||||
|
State::XWon | State::OWon | State::Draw => {
|
||||||
|
if !self.completed.iter().any(|pubkey| pubkey == game_pubkey) {
|
||||||
|
// TODO: Once the PoH high is exposed to programs, it could be used to ensure
|
||||||
|
// that old games are not being re-added and causing |total| to increment
|
||||||
|
// incorrectly.
|
||||||
|
self.total += 1;
|
||||||
|
self.completed.insert(0, *game_pubkey);
|
||||||
|
|
||||||
|
// Only track the last N completed games to
|
||||||
|
// avoid overrunning Account userdata
|
||||||
|
if self.completed.len() > 5 {
|
||||||
|
self.completed.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(self: &TicTacToeDashboardProgram, output: &mut [u8]) -> Result<()> {
|
||||||
|
let self_serialized = serde_cbor::to_vec(self).unwrap();
|
||||||
|
|
||||||
|
if output.len() < 2 + self_serialized.len() {
|
||||||
|
warn!(
|
||||||
|
"{} bytes required to serialize but only have {} bytes",
|
||||||
|
self_serialized.len() + 2,
|
||||||
|
output.len()
|
||||||
|
);
|
||||||
|
return Err(Error::UserdataTooSmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(self_serialized.len() <= 0xFFFF);
|
||||||
|
output[0] = (self_serialized.len() & 0xFF) as u8;
|
||||||
|
output[1] = (self_serialized.len() >> 8) as u8;
|
||||||
|
output[2..(2 + self_serialized.len())].clone_from_slice(&self_serialized);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||||
|
program_id.as_ref() == TICTACTOE_DASHBOARD_PROGRAM_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id() -> Pubkey {
|
||||||
|
Pubkey::new(&TICTACTOE_DASHBOARD_PROGRAM_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_transaction(
|
||||||
|
tx: &Transaction,
|
||||||
|
pix: usize,
|
||||||
|
accounts: &mut [&mut Account],
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("process_transaction: {:?}", tx);
|
||||||
|
// accounts[0] doesn't matter, anybody can cause a dashboard update
|
||||||
|
// accounts[1] must be a Dashboard account
|
||||||
|
// accounts[2] must be a Game account
|
||||||
|
if accounts.len() != 3 {
|
||||||
|
error!("Expected 3 accounts");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
if !Self::check_id(&accounts[1].program_id) {
|
||||||
|
error!("accounts[1] is not a TICTACTOE_DASHBOARD_PROGRAM_ID");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
if accounts[1].userdata.is_empty() {
|
||||||
|
error!("accounts[1] userdata is empty");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dashboard = Self::deserialize(&accounts[1].userdata)?;
|
||||||
|
|
||||||
|
if !TicTacToeProgram::check_id(&accounts[2].program_id) {
|
||||||
|
error!("accounts[2] is not a TICTACTOE_PROGRAM_ID");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
let ttt = TicTacToeProgram::deserialize(&accounts[2].userdata)?;
|
||||||
|
|
||||||
|
match ttt.game {
|
||||||
|
None => Err(Error::NoGame),
|
||||||
|
Some(game) => dashboard.update(tx.key(pix, 2).unwrap(), &game),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
dashboard.serialize(&mut accounts[1].userdata)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn serde() {
|
||||||
|
let mut dashboard1 = TicTacToeDashboardProgram::default();
|
||||||
|
dashboard1.total = 123;
|
||||||
|
|
||||||
|
let mut userdata = vec![0xff; 256];
|
||||||
|
dashboard1.serialize(&mut userdata).unwrap();
|
||||||
|
|
||||||
|
let dashboard2 = TicTacToeDashboardProgram::deserialize(&userdata).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dashboard1, dashboard2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn serde_userdata_too_small() {
|
||||||
|
let dashboard = TicTacToeDashboardProgram::default();
|
||||||
|
let mut userdata = vec![0xff; 1];
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.serialize(&mut userdata),
|
||||||
|
Err(Error::UserdataTooSmall)
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = TicTacToeDashboardProgram::deserialize(&userdata);
|
||||||
|
assert!(err.is_err());
|
||||||
|
assert_eq!(err.err().unwrap(), Error::InvalidUserdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add tests for business logic
|
||||||
|
}
|
@ -24,7 +24,7 @@ impl std::fmt::Display for Error {
|
|||||||
}
|
}
|
||||||
impl std::error::Error for Error {}
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
enum BoardItem {
|
enum BoardItem {
|
||||||
@ -55,7 +55,7 @@ impl Default for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
struct Game {
|
pub struct Game {
|
||||||
player_x: Pubkey,
|
player_x: Pubkey,
|
||||||
player_o: Option<Pubkey>,
|
player_o: Option<Pubkey>,
|
||||||
pub state: State,
|
pub state: State,
|
||||||
@ -164,7 +164,7 @@ enum Command {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct TicTacToeProgram {
|
pub struct TicTacToeProgram {
|
||||||
game: Option<Game>,
|
pub game: Option<Game>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
||||||
@ -172,7 +172,7 @@ pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
impl TicTacToeProgram {
|
impl TicTacToeProgram {
|
||||||
fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> {
|
pub fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> {
|
||||||
let len = input[0] as usize;
|
let len = input[0] as usize;
|
||||||
|
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
|
Reference in New Issue
Block a user