From 96b3ffec68c61df0ae9cb73dce08d31bdf7bace4 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Thu, 21 Nov 2024 13:06:34 +0200 Subject: [PATCH] ping-pong - framework alignment --- Cargo.lock | 15 +- Cargo.toml | 5 +- .../ping-pong-egld/interactor/.gitignore | 9 + .../ping-pong-egld/interactor/Cargo.toml | 31 ++ .../ping-pong-egld/interactor/config.toml | 5 + .../ping-pong-egld/interactor/src/interact.rs | 375 ++++++++++++++++++ .../interactor/src/interact_cli.rs | 81 ++++ .../interactor/src/interact_config.rs | 49 +++ .../interactor/src/interact_main.rs | 6 + .../interactor/src/interact_state.rs | 50 +++ .../interactor/src/proxy_ping_pong_egld.rs | 263 ++++++++++++ .../interactor/tests/interact_cs_test.rs | 138 +++++++ contracts/ping-pong-egld/src/ping_pong.rs | 89 ++++- contracts/ping-pong-egld/src/types.rs | 20 + contracts/ping-pong-egld/src/user_status.rs | 9 - .../tests/ping_pong_egld_scenario_rs_test.rs | 1 + contracts/ping-pong-egld/wasm/Cargo.lock | 31 +- contracts/ping-pong-egld/wasm/Cargo.toml | 6 + contracts/ping-pong-egld/wasm/src/lib.rs | 7 +- 19 files changed, 1150 insertions(+), 40 deletions(-) create mode 100644 contracts/ping-pong-egld/interactor/.gitignore create mode 100644 contracts/ping-pong-egld/interactor/Cargo.toml create mode 100644 contracts/ping-pong-egld/interactor/config.toml create mode 100644 contracts/ping-pong-egld/interactor/src/interact.rs create mode 100644 contracts/ping-pong-egld/interactor/src/interact_cli.rs create mode 100644 contracts/ping-pong-egld/interactor/src/interact_config.rs create mode 100644 contracts/ping-pong-egld/interactor/src/interact_main.rs create mode 100644 contracts/ping-pong-egld/interactor/src/interact_state.rs create mode 100644 contracts/ping-pong-egld/interactor/src/proxy_ping_pong_egld.rs create mode 100644 contracts/ping-pong-egld/interactor/tests/interact_cs_test.rs create mode 100644 contracts/ping-pong-egld/src/types.rs delete mode 100644 contracts/ping-pong-egld/src/user_status.rs diff --git a/Cargo.lock b/Cargo.lock index 28498c10..089fd037 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "basic-interact" +name = "basic-interactor" version = "0.0.0" dependencies = [ "adder", @@ -1954,6 +1954,19 @@ dependencies = [ "multiversx-sc-scenario", ] +[[package]] +name = "ping-pong-egld-interact" +version = "0.0.0" +dependencies = [ + "clap", + "multiversx-sc", + "multiversx-sc-snippets", + "ping-pong-egld", + "serde", + "tokio", + "toml", +] + [[package]] name = "ping-pong-egld-meta" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5fb5dd18..630cc5d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "contracts/adder", "contracts/adder/meta", - "contracts/adder/interact", + "contracts/adder/interactor", "contracts/esdt-transfer-with-fee", "contracts/esdt-transfer-with-fee/meta", "contracts/bonding-curve-contract", @@ -70,6 +70,7 @@ members = [ "contracts/paymaster/interactor", "contracts/ping-pong-egld", "contracts/ping-pong-egld/meta", + "contracts/ping-pong-egld/interactor", "contracts/proxy-deployer", "contracts/proxy-deployer/meta", "contracts/proxy-pause", @@ -81,5 +82,5 @@ members = [ "contracts/token-release", "contracts/token-release/meta", "contracts/mvx-game-sc", - "contracts/mvx-game-sc/meta" + "contracts/mvx-game-sc/meta", ] diff --git a/contracts/ping-pong-egld/interactor/.gitignore b/contracts/ping-pong-egld/interactor/.gitignore new file mode 100644 index 00000000..ac5c74ec --- /dev/null +++ b/contracts/ping-pong-egld/interactor/.gitignore @@ -0,0 +1,9 @@ +# Pem files are used for interactions, but shouldn't be committed +*.pem +*.json + +# Temporary storage of deployed contract address, so we can preserve the context between executions. +state.toml + +# Trace file of interactor tooling +interactor_trace.scen.json diff --git a/contracts/ping-pong-egld/interactor/Cargo.toml b/contracts/ping-pong-egld/interactor/Cargo.toml new file mode 100644 index 00000000..beec9ed5 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ping-pong-egld-interact" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" +publish = false + +[[bin]] +name = "ping-pong-egld-interact" +path = "src/interact_main.rs" + +[lib] +path = "src/interact.rs" + +[dependencies] +clap = { version = "4.4.7", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.6" +tokio = { version = "1.24" } + +[dependencies.ping-pong-egld] +path = ".." + +[dependencies.multiversx-sc-snippets] +version = "0.54.3" + +[dependencies.multiversx-sc] +version = "0.54.3" + +[features] +chain-simulator-tests = [] diff --git a/contracts/ping-pong-egld/interactor/config.toml b/contracts/ping-pong-egld/interactor/config.toml new file mode 100644 index 00000000..aca74870 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/config.toml @@ -0,0 +1,5 @@ +chain_type = 'simulator' +gateway_uri = 'http://localhost:8085' + +# chain_type = 'real' +# gateway_uri = 'https://devnet-gateway.multiversx.com' diff --git a/contracts/ping-pong-egld/interactor/src/interact.rs b/contracts/ping-pong-egld/interactor/src/interact.rs new file mode 100644 index 00000000..4ba9f501 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/interact.rs @@ -0,0 +1,375 @@ +mod interact_cli; +mod interact_config; +mod interact_state; +mod proxy_ping_pong_egld; + +use crate::interact_state::State; +use clap::Parser; +pub use interact_config::Config; + +use multiversx_sc_snippets::imports::*; +use proxy_ping_pong_egld::{ContractState, UserStatus}; + +const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; + +const PING_PONG_CODE: MxscPath = MxscPath::new("../output/ping-pong-egld.mxsc.json"); + +pub async fn ping_pong_egld_cli() { + env_logger::init(); + + let config = Config::load_config(); + + let mut interact = PingPongEgldInteract::init(config).await; + + let cli = interact_cli::InteractCli::parse(); + match &cli.command { + Some(interact_cli::InteractCliCommand::Deploy(args)) => { + interact + .deploy( + args.ping_amount.clone(), + args.duration_in_seconds, + args.opt_activation_timestamp, + OptionalValue::from(args.max_funds.clone()), + ) + .await; + } + Some(interact_cli::InteractCliCommand::Upgrade(args)) => { + interact + .upgrade( + args.ping_amount.clone(), + args.duration_in_seconds, + args.opt_activation_timestamp, + OptionalValue::from(args.max_funds.clone()), + ) + .await + } + Some(interact_cli::InteractCliCommand::Ping(args)) => { + let sender = interact.ping_pong_owner_address.clone(); + interact + .ping(args.cost.unwrap_or_default(), None, &sender) + .await + } + Some(interact_cli::InteractCliCommand::Pong) => { + let sender = interact.ping_pong_owner_address.clone(); + interact.pong(None, &sender).await; + } + Some(interact_cli::InteractCliCommand::PongAll) => { + let sender = interact.ping_pong_owner_address.clone(); + interact.pong_all(None, &sender).await; + } + Some(interact_cli::InteractCliCommand::GetUserAddresses) => { + let user_addresses = interact.get_user_addresses().await; + println!("User addresses: "); + for address in user_addresses { + print!("{address} "); + } + } + Some(interact_cli::InteractCliCommand::GetContractState) => { + let contract_state = interact.get_contract_state().await; + println!("Contract state: ping_amount -> {:#?} | deadline -> {:#?} | activation_timestamp -> {:#?} | max_funds -> {:#?} | pong_all_last_user -> {:#?}", + contract_state.ping_amount, + contract_state.deadline, + contract_state.activation_timestamp, + contract_state.max_funds, + contract_state.pong_all_last_user); + } + Some(interact_cli::InteractCliCommand::GetPingAmount) => { + let ping_amount = interact.get_ping_amount().await; + println!("Ping amount: {}", ping_amount); + } + Some(interact_cli::InteractCliCommand::GetDeadline) => { + let deadline = interact.get_deadline().await; + println!("Deadline: {}", deadline); + } + Some(interact_cli::InteractCliCommand::GetActivationTimestamp) => { + let activation_timestamp = interact.get_activation_timestamp().await; + println!("Activation timestamp: {}", activation_timestamp); + } + Some(interact_cli::InteractCliCommand::GetMaxFunds) => { + let max_funds = interact.get_max_funds().await; + match max_funds { + Some(funds) => println!("Max funds: {}", funds), + None => println!("Max funds: none"), + } + } + Some(interact_cli::InteractCliCommand::GetUserStatus(args)) => { + let user_status = interact.get_user_status(args.id).await; + match user_status { + UserStatus::New => println!("User status: unknown"), + UserStatus::Registered => println!("User status: `ping`-ed"), + UserStatus::Withdrawn => println!("User status: `pong`-ed"), + } + } + Some(interact_cli::InteractCliCommand::PongAllLastUser) => { + let pong_all_last_user = interact.pong_all_last_user().await; + println!("Pong all last user: {pong_all_last_user}"); + } + None => {} + } +} + +pub struct PingPongEgldInteract { + pub interactor: Interactor, + pub ping_pong_owner_address: Bech32Address, + pub wallet_address: Bech32Address, + pub state: State, +} + +impl PingPongEgldInteract { + pub async fn init(config: Config) -> Self { + let mut interactor = Interactor::new(config.gateway_uri()) + .await + .use_chain_simulator(config.use_chain_simulator()) + .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) + .await; + + interactor.set_current_dir_from_workspace("contracts/ping-pong-egld/interactor"); + let ping_pong_owner_address = interactor.register_wallet(test_wallets::eve()).await; + let wallet_address = interactor.register_wallet(test_wallets::mallory()).await; + + // generate blocks until ESDTSystemSCAddress is enabled + interactor.generate_blocks_until_epoch(1).await.unwrap(); + + Self { + interactor, + ping_pong_owner_address: ping_pong_owner_address.into(), + wallet_address: wallet_address.into(), + state: State::load_state(), + } + } + + pub async fn set_state(&mut self) { + println!("wallet address: {}", self.wallet_address); + self.interactor + .retrieve_account(&self.ping_pong_owner_address) + .await; + self.interactor.retrieve_account(&self.wallet_address).await; + } + + pub async fn deploy( + &mut self, + ping_amount: RustBigUint, + duration_in_seconds: u64, + opt_activation_timestamp: Option, + max_funds: OptionalValue, + ) -> (u64, String) { + self.set_state().await; + + let (new_address, status, message) = self + .interactor + .tx() + .from(&self.ping_pong_owner_address) + .gas(30_000_000u64) + .typed(proxy_ping_pong_egld::PingPongProxy) + .init( + ping_amount, + duration_in_seconds, + opt_activation_timestamp, + max_funds, + ) + .code(PING_PONG_CODE) + .returns(ReturnsNewBech32Address) + .returns(ReturnsStatus) + .returns(ReturnsMessage) + .run() + .await; + + println!("new address: {new_address}"); + self.state.set_ping_pong_egld_address(new_address); + + (status, message) + } + + pub async fn upgrade( + &mut self, + ping_amount: RustBigUint, + duration_in_seconds: u64, + opt_activation_timestamp: Option, + max_funds: OptionalValue, + ) { + let response = self + .interactor + .tx() + .to(self.state.current_ping_pong_egld_address()) + .from(&self.wallet_address) + .gas(30_000_000u64) + .typed(proxy_ping_pong_egld::PingPongProxy) + .upgrade( + ping_amount, + duration_in_seconds, + opt_activation_timestamp, + max_funds, + ) + .code(PING_PONG_CODE) + .returns(ReturnsNewAddress) + .run() + .await; + + println!("Result: {response:?}"); + } + + pub async fn ping(&mut self, egld_amount: u64, message: Option<&str>, sender: &Bech32Address) { + let _data: IgnoreValue = IgnoreValue; + + let response = self + .interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_egld_address()) + .gas(30_000_000u64) + .typed(proxy_ping_pong_egld::PingPongProxy) + .ping(_data) + .egld(egld_amount) + .returns(ReturnsHandledOrError::new()) + .run() + .await; + + match response { + Ok(_) => println!("Ping successful!"), + Err(err) => { + println!("Ping failed with message: {}", err.message); + assert_eq!(message.unwrap_or_default(), err.message); + } + } + } + + pub async fn pong(&mut self, message: Option<&str>, sender: &Bech32Address) { + let response = self + .interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_egld_address()) + .gas(30_000_000u64) + .typed(proxy_ping_pong_egld::PingPongProxy) + .pong() + .returns(ReturnsHandledOrError::new()) + .run() + .await; + + match response { + Ok(_) => println!("Pong successful!"), + Err(err) => { + println!("Pong failed with message: {}", err.message); + assert_eq!(message.unwrap_or_default(), err.message); + } + } + } + + pub async fn pong_all(&mut self, message: Option, sender: &Bech32Address) { + let response = self + .interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_egld_address()) + .gas(30_000_000u64) + .typed(proxy_ping_pong_egld::PingPongProxy) + .pong_all() + .returns(ReturnsHandledOrError::new()) + .run() + .await; + + match response { + Ok(_) => println!("Pong All successful!"), + Err(err) => { + println!("Pong All failed with message: {}", err.message); + assert_eq!(message.unwrap_or_default(), err.message); + } + } + } + + pub async fn get_user_addresses(&mut self) -> Vec { + let response = self + .interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .get_user_addresses() + .returns(ReturnsResult) + .run() + .await; + + let mut response_vec: Vec = Vec::new(); + for r in response.to_vec().into_vec() { + response_vec.push(r.as_managed_buffer().to_string()); + } + + response_vec + } + + pub async fn get_contract_state(&mut self) -> ContractState { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .get_contract_state() + .returns(ReturnsResult) + .run() + .await + } + + pub async fn get_ping_amount(&mut self) -> RustBigUint { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .ping_amount() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_deadline(&mut self) -> u64 { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .deadline() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_activation_timestamp(&mut self) -> u64 { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .activation_timestamp() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_max_funds(&mut self) -> Option { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .max_funds() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_user_status(&mut self, user_id: usize) -> UserStatus { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .user_status(user_id) + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn pong_all_last_user(&mut self) -> usize { + self.interactor + .query() + .to(self.state.current_ping_pong_egld_address()) + .typed(proxy_ping_pong_egld::PingPongProxy) + .pong_all_last_user() + .returns(ReturnsResultUnmanaged) + .run() + .await + } +} diff --git a/contracts/ping-pong-egld/interactor/src/interact_cli.rs b/contracts/ping-pong-egld/interactor/src/interact_cli.rs new file mode 100644 index 00000000..700f6949 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/interact_cli.rs @@ -0,0 +1,81 @@ +use clap::{Args, Parser, Subcommand}; +use multiversx_sc_snippets::imports::RustBigUint; + +/// Ping Pong Interact CLI +#[derive(Default, PartialEq, Eq, Debug, Parser)] +#[command(version, about)] +#[command(propagate_version = true)] +pub struct InteractCli { + #[command(subcommand)] + pub command: Option, +} + +/// Ping Pong Interact CLI Commands +#[derive(Clone, PartialEq, Eq, Debug, Subcommand)] +pub enum InteractCliCommand { + #[command(name = "deploy", about = "Deploy contract.")] + Deploy(DeployArgs), + #[command(name = "upgrade", about = "Upgrade contract.")] + Upgrade(DeployArgs), + #[command( + name = "ping", + about = "User sends some EGLD to be locked in the contract for a period of time." + )] + Ping(PingArgs), + #[command(name = "pong", about = "User can take back funds from the contract.")] + Pong, + #[command(name = "pong-all", about = "Send back funds to all users who pinged.")] + PongAll, + #[command( + name = "user-addresses", + about = "Lists the addresses of all users that have `ping`-ed in the order they have `ping`-ed." + )] + GetUserAddresses, + #[command(name = "contract-state", about = "Returns the current contract state.")] + GetContractState, + #[command(name = "ping-amount", about = "Returns the ping amount.")] + GetPingAmount, + #[command(name = "deadline", about = "Return deadline.")] + GetDeadline, + #[command( + name = "activation-timestamp", + about = "Block timestamp of the block where the contract got activated. If not specified in the constructor it is the the deploy block timestamp." + )] + GetActivationTimestamp, + #[command(name = "max-funds", about = "Optional funding cap.")] + GetMaxFunds, + #[command(name = "user-status", about = "State of user funds.")] + GetUserStatus(UserStatusArgs), + #[command( + name = "pong-all-last-user", + about = "`pongAll` status, the last user to be processed. 0 if never called `pongAll` or `pongAll` completed." + )] + PongAllLastUser, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct DeployArgs { + #[arg(short = 'p', long = "ping-amount")] + pub ping_amount: RustBigUint, + + #[arg(short = 'd', long = "duration-in-seconds")] + pub duration_in_seconds: u64, + + #[arg(short = 'a', long = "activation-timestamp")] + pub opt_activation_timestamp: Option, + + #[arg(short = 'm', long = "max-funds")] + pub max_funds: Option, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct PingArgs { + #[arg(short = 'c', long = "cost", default_value = "50000000000000000")] + pub cost: Option, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct UserStatusArgs { + #[arg(short = 'i')] + pub id: usize, +} diff --git a/contracts/ping-pong-egld/interactor/src/interact_config.rs b/contracts/ping-pong-egld/interactor/src/interact_config.rs new file mode 100644 index 00000000..013e1671 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/interact_config.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; +use std::io::Read; + +/// Config file +const CONFIG_FILE: &str = "config.toml"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChainType { + Real, + Simulator, +} + +/// Ping Pong Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub gateway_uri: String, + pub chain_type: ChainType, +} + +impl Config { + // Deserializes config from file + pub fn load_config() -> Self { + let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + toml::from_str(&content).unwrap() + } + + pub fn chain_simulator_config() -> Self { + Config { + gateway_uri: "http://localhost:8085".to_owned(), + chain_type: ChainType::Simulator, + } + } + + // Returns the gateway URI + pub fn gateway_uri(&self) -> &str { + &self.gateway_uri + } + + // Returns if chain type is chain simulator + pub fn use_chain_simulator(&self) -> bool { + match self.chain_type { + ChainType::Real => false, + ChainType::Simulator => true, + } + } +} diff --git a/contracts/ping-pong-egld/interactor/src/interact_main.rs b/contracts/ping-pong-egld/interactor/src/interact_main.rs new file mode 100644 index 00000000..1e090c46 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/interact_main.rs @@ -0,0 +1,6 @@ +extern crate ping_pong_egld_interact; + +#[tokio::main] +pub async fn main() { + ping_pong_egld_interact::ping_pong_egld_cli().await; +} diff --git a/contracts/ping-pong-egld/interactor/src/interact_state.rs b/contracts/ping-pong-egld/interactor/src/interact_state.rs new file mode 100644 index 00000000..38fb3d32 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/interact_state.rs @@ -0,0 +1,50 @@ +use multiversx_sc_snippets::imports::*; +use serde::{Deserialize, Serialize}; +use std::{ + io::{Read, Write}, + path::Path, +}; + +/// State file +const STATE_FILE: &str = "state.toml"; + +/// Multisig Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + ping_pong_egld_address: Option, +} + +impl State { + // Deserializes state from file + pub fn load_state() -> Self { + if Path::new(STATE_FILE).exists() { + let mut file = std::fs::File::open(STATE_FILE).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + toml::from_str(&content).unwrap() + } else { + Self::default() + } + } + + /// Sets the ping pong address + pub fn set_ping_pong_egld_address(&mut self, address: Bech32Address) { + self.ping_pong_egld_address = Some(address); + } + + /// Returns the ping pong contract + pub fn current_ping_pong_egld_address(&self) -> &Bech32Address { + self.ping_pong_egld_address + .as_ref() + .expect("no known ping pong contract, deploy first") + } +} + +impl Drop for State { + // Serializes state to file + fn drop(&mut self) { + let mut file = std::fs::File::create(STATE_FILE).unwrap(); + file.write_all(toml::to_string(self).unwrap().as_bytes()) + .unwrap(); + } +} diff --git a/contracts/ping-pong-egld/interactor/src/proxy_ping_pong_egld.rs b/contracts/ping-pong-egld/interactor/src/proxy_ping_pong_egld.rs new file mode 100644 index 00000000..ad894441 --- /dev/null +++ b/contracts/ping-pong-egld/interactor/src/proxy_ping_pong_egld.rs @@ -0,0 +1,263 @@ +// Code generated by the multiversx-sc proxy generator. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +#![allow(dead_code)] +#![allow(clippy::all)] + +use multiversx_sc::proxy_imports::*; + +pub struct PingPongProxy; + +impl TxProxyTrait for PingPongProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = PingPongProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + PingPongProxyMethods { wrapped_tx: tx } + } +} + +pub struct PingPongProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl PingPongProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + /// Necessary configuration when deploying: + /// `ping_amount` - the exact EGLD amount that needs to be sent when `ping`-ing. + /// `duration_in_seconds` - how much time (in seconds) until contract expires. + /// `opt_activation_timestamp` - optionally specify the contract to only activate at a later date. + /// `max_funds` - optional funding cap, no more funds than this can be added to the contract. + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg, + Arg2: ProxyArg>, + Arg3: ProxyArg>>, + >( + self, + ping_amount: Arg0, + duration_in_seconds: Arg1, + opt_activation_timestamp: Arg2, + max_funds: Arg3, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&ping_amount) + .argument(&duration_in_seconds) + .argument(&opt_activation_timestamp) + .argument(&max_funds) + .original_result() + } +} + +#[rustfmt::skip] +impl PingPongProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade< + Arg0: ProxyArg>, + Arg1: ProxyArg, + Arg2: ProxyArg>, + Arg3: ProxyArg>>, + >( + self, + ping_amount: Arg0, + duration_in_seconds: Arg1, + opt_activation_timestamp: Arg2, + max_funds: Arg3, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .argument(&ping_amount) + .argument(&duration_in_seconds) + .argument(&opt_activation_timestamp) + .argument(&max_funds) + .original_result() + } +} + +#[rustfmt::skip] +impl PingPongProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + /// User sends some EGLD to be locked in the contract for a period of time. + /// Optional `_data` argument is ignored. + pub fn ping< + Arg0: ProxyArg, + >( + self, + _data: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("ping") + .argument(&_data) + .original_result() + } + + /// User can take back funds from the contract. + /// Can only be called after expiration. + pub fn pong( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("pong") + .original_result() + } + + /// Send back funds to all users who pinged. + /// Returns + /// - `completed` if everything finished + /// - `interrupted` if run out of gas midway. + /// Can only be called after expiration. + pub fn pong_all( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("pongAll") + .original_result() + } + + /// Lists the addresses of all users that have `ping`-ed, + /// in the order they have `ping`-ed + pub fn get_user_addresses( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getUserAddresses") + .original_result() + } + + /// Returns the current contract state as a struct + /// for faster fetching from external parties + pub fn get_contract_state( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getContractState") + .original_result() + } + + pub fn ping_amount( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getPingAmount") + .original_result() + } + + pub fn deadline( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getDeadline") + .original_result() + } + + /// Block timestamp of the block where the contract got activated. + /// If not specified in the constructor it is the the deploy block timestamp. + pub fn activation_timestamp( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActivationTimestamp") + .original_result() + } + + /// Optional funding cap. + pub fn max_funds( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getMaxFunds") + .original_result() + } + + /// State of user funds. + /// 0 - user unknown, never `ping`-ed + /// 1 - `ping`-ed + /// 2 - `pong`-ed + pub fn user_status< + Arg0: ProxyArg, + >( + self, + user_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getUserStatus") + .argument(&user_id) + .original_result() + } + + /// Part of the `pongAll` status, the last user to be processed. + /// 0 if never called `pongAll` or `pongAll` completed. + pub fn pong_all_last_user( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("pongAllLastUser") + .original_result() + } +} + +#[type_abi] +#[derive(TopEncode, TopDecode, Default)] +pub struct ContractState +where + Api: ManagedTypeApi, +{ + pub ping_amount: BigUint, + pub deadline: u64, + pub activation_timestamp: u64, + pub max_funds: Option>, + pub pong_all_last_user: usize, +} + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy)] +pub enum UserStatus { + New, + Registered, + Withdrawn, +} diff --git a/contracts/ping-pong-egld/interactor/tests/interact_cs_test.rs b/contracts/ping-pong-egld/interactor/tests/interact_cs_test.rs new file mode 100644 index 00000000..f25bcfde --- /dev/null +++ b/contracts/ping-pong-egld/interactor/tests/interact_cs_test.rs @@ -0,0 +1,138 @@ +use multiversx_sc_snippets::imports::RustBigUint; +use ping_pong_egld_interact::{Config, PingPongEgldInteract}; + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_pong_egld() { + let mut interact = PingPongEgldInteract::init(Config::chain_simulator_config()).await; + let wallet_address = interact.wallet_address.clone(); + let ping_pong_owner_address = interact.ping_pong_owner_address.clone(); + + let ping_amount = 1u64; + + // test_ping_unmatched_amount + let duration_in_seconds = 5u64; + let opt_activation_timestamp = 2u64; + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + Some(opt_activation_timestamp), + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact + .ping( + 0u64, + Some("the payment must match the fixed sum"), + &wallet_address, + ) + .await; + + // test_ping_inactive_contracts + let duration_in_seconds = 5u64; + let opt_activation_timestamp = 2_000_000_000u64; + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + Some(opt_activation_timestamp), + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact + .ping(1u64, Some("smart contract not active yet"), &wallet_address) + .await; + + // test_ping_passed_deadline + let duration_in_seconds = 5u64; + let opt_activation_timestamp = 2u64; + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + Some(opt_activation_timestamp), + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact + .ping(1u64, Some("deadline has passed"), &wallet_address) + .await; + + // test_ping_max_funds + let ping_amount = 10u64; + let duration_in_seconds = 30000u64; + let max_funds = 10u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + None, + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact + .ping(10u64, Some("smart contract full"), &wallet_address) + .await; + + // test ping + let ping_amount = 1u64; + let duration_in_seconds = 20u64; + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + None, + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact.ping(1u64, None, &wallet_address).await; + interact + .ping(1u64, Some("can only ping once"), &wallet_address) + .await; + + assert_eq!(interact.get_ping_amount().await, RustBigUint::from(1u64)); + + interact + .pong(Some("can't withdraw before deadline"), &wallet_address) + .await; + + interact.pong(None, &wallet_address).await; + + // test_pong_all + let ping_amount = 1u64; + let duration_in_seconds = 18u64; + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration_in_seconds, + None, + multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + ) + .await; + + interact.ping(1u64, None, &ping_pong_owner_address).await; + + interact.ping(1u64, None, &wallet_address).await; + + interact.pong_all(None, &ping_pong_owner_address).await; + interact + .pong(Some("already withdrawn"), &wallet_address) + .await; +} diff --git a/contracts/ping-pong-egld/src/ping_pong.rs b/contracts/ping-pong-egld/src/ping_pong.rs index aee3fa3f..3e0d4ffc 100644 --- a/contracts/ping-pong-egld/src/ping_pong.rs +++ b/contracts/ping-pong-egld/src/ping_pong.rs @@ -2,9 +2,9 @@ use multiversx_sc::imports::*; -mod user_status; +mod types; -use user_status::UserStatus; +use types::{ContractState, UserStatus}; /// Derived empirically. const PONG_ALL_LOW_GAS_LIMIT: u64 = 3_000_000; @@ -25,10 +25,11 @@ const PONG_ALL_LOW_GAS_LIMIT: u64 = 3_000_000; #[multiversx_sc::contract] pub trait PingPong { /// Necessary configuration when deploying: - /// `ping_amount` - the exact EGLD amounf that needs to be sent when `ping`-ing. + /// `ping_amount` - the exact EGLD amount that needs to be sent when `ping`-ing. /// `duration_in_seconds` - how much time (in seconds) until contract expires. - /// `opt_activation_timestamp` - optionally specify the contract to only actvivate at a later date. + /// `opt_activation_timestamp` - optionally specify the contract to only activate at a later date. /// `max_funds` - optional funding cap, no more funds than this can be added to the contract. + #[allow_multiple_var_args] #[init] fn init( &self, @@ -46,6 +47,22 @@ pub trait PingPong { self.max_funds().set(max_funds.into_option()); } + #[upgrade] + fn upgrade( + &self, + ping_amount: &BigUint, + duration_in_seconds: u64, + opt_activation_timestamp: Option, + max_funds: OptionalValue, + ) { + self.init( + ping_amount, + duration_in_seconds, + opt_activation_timestamp, + max_funds, + ) + } + /// User sends some EGLD to be locked in the contract for a period of time. /// Optional `_data` argument is ignored. #[payable("EGLD")] @@ -86,14 +103,16 @@ pub trait PingPong { match user_status { UserStatus::New => { self.user_status(user_id).set(UserStatus::Registered); - } + }, UserStatus::Registered => { sc_panic!("can only ping once") - } + }, UserStatus::Withdrawn => { sc_panic!("already withdrawn") - } + }, } + + self.ping_event(&caller, &payment); } fn pong_by_user_id(&self, user_id: usize) -> Result<(), &'static str> { @@ -103,15 +122,14 @@ pub trait PingPong { UserStatus::Registered => { self.user_status(user_id).set(UserStatus::Withdrawn); if let Some(user_address) = self.user_mapper().get_user_address(user_id) { - self.tx() - .to(&user_address) - .egld(self.ping_amount().get()) - .transfer(); + let amount = self.ping_amount().get(); + self.tx().to(&user_address).egld(&amount).transfer(); + self.pong_event(&user_address, &amount); Result::Ok(()) } else { Result::Err("unknown user") } - } + }, UserStatus::Withdrawn => Result::Err("already withdrawn"), } } @@ -140,24 +158,27 @@ pub trait PingPong { /// Can only be called after expiration. #[endpoint(pongAll)] fn pong_all(&self) -> OperationCompletionStatus { + let now = self.blockchain().get_block_timestamp(); require!( - self.blockchain().get_block_timestamp() >= self.deadline().get(), + now >= self.deadline().get(), "can't withdraw before deadline" ); let num_users = self.user_mapper().get_user_count(); let mut pong_all_last_user = self.pong_all_last_user().get(); + let mut status = OperationCompletionStatus::InterruptedBeforeOutOfGas; loop { if pong_all_last_user >= num_users { // clear field and reset to 0 pong_all_last_user = 0; self.pong_all_last_user().set(pong_all_last_user); - return OperationCompletionStatus::Completed; + status = OperationCompletionStatus::Completed; + break; } if self.blockchain().get_gas_left() < PONG_ALL_LOW_GAS_LIMIT { self.pong_all_last_user().set(pong_all_last_user); - return OperationCompletionStatus::InterruptedBeforeOutOfGas; + break; } pong_all_last_user += 1; @@ -165,6 +186,10 @@ pub trait PingPong { // in case of error just ignore the error and skip let _ = self.pong_by_user_id(pong_all_last_user); } + + self.pong_all_event(now, &status, pong_all_last_user); + + status } /// Lists the addresses of all users that have `ping`-ed, @@ -174,6 +199,19 @@ pub trait PingPong { self.user_mapper().get_all_addresses().into() } + /// Returns the current contract state as a struct + /// for faster fetching from external parties + #[view(getContractState)] + fn get_contract_state(&self) -> ContractState { + ContractState { + ping_amount: self.ping_amount().get(), + deadline: self.deadline().get(), + activation_timestamp: self.activation_timestamp().get(), + max_funds: self.max_funds().get(), + pong_all_last_user: self.pong_all_last_user().get(), + } + } + // storage #[view(getPingAmount)] @@ -207,8 +245,27 @@ pub trait PingPong { fn user_status(&self, user_id: usize) -> SingleValueMapper; /// Part of the `pongAll` status, the last user to be processed. - /// 0 if never called `pongAll` or `pongAll` completed.. + /// 0 if never called `pongAll` or `pongAll` completed. #[view(pongAllLastUser)] #[storage_mapper("pongAllLastUser")] fn pong_all_last_user(&self) -> SingleValueMapper; + + // events + + /// Signals a successful ping by user with amount + #[event] + fn ping_event(&self, #[indexed] caller: &ManagedAddress, pinged_amount: &BigUint); + + /// Signals a successful pong by user with amount + #[event] + fn pong_event(&self, #[indexed] caller: &ManagedAddress, ponged_amount: &BigUint); + + /// Signals the beginning of the pong_all operation, status and last user + #[event] + fn pong_all_event( + &self, + #[indexed] timestamp: u64, + #[indexed] status: &OperationCompletionStatus, + #[indexed] pong_all_last_user: usize, + ); } diff --git a/contracts/ping-pong-egld/src/types.rs b/contracts/ping-pong-egld/src/types.rs new file mode 100644 index 00000000..e83cc714 --- /dev/null +++ b/contracts/ping-pong-egld/src/types.rs @@ -0,0 +1,20 @@ +use multiversx_sc::derive_imports::*; +use multiversx_sc::imports::*; + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy)] +pub enum UserStatus { + New, + Registered, + Withdrawn, +} + +#[type_abi] +#[derive(TopEncode, TopDecode, Default)] +pub struct ContractState { + pub ping_amount: BigUint, + pub deadline: u64, + pub activation_timestamp: u64, + pub max_funds: Option>, + pub pong_all_last_user: usize, +} diff --git a/contracts/ping-pong-egld/src/user_status.rs b/contracts/ping-pong-egld/src/user_status.rs deleted file mode 100644 index a464b742..00000000 --- a/contracts/ping-pong-egld/src/user_status.rs +++ /dev/null @@ -1,9 +0,0 @@ -use multiversx_sc::derive_imports::*; - -#[type_abi] -#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy)] -pub enum UserStatus { - New, - Registered, - Withdrawn, -} diff --git a/contracts/ping-pong-egld/tests/ping_pong_egld_scenario_rs_test.rs b/contracts/ping-pong-egld/tests/ping_pong_egld_scenario_rs_test.rs index 55a8e591..46d6d0c1 100644 --- a/contracts/ping-pong-egld/tests/ping_pong_egld_scenario_rs_test.rs +++ b/contracts/ping-pong-egld/tests/ping_pong_egld_scenario_rs_test.rs @@ -3,6 +3,7 @@ use multiversx_sc_scenario::*; fn world() -> ScenarioWorld { let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/ping-pong-egld"); blockchain.register_contract( "mxsc:output/ping-pong-egld.mxsc.json", ping_pong_egld::ContractBuilder, diff --git a/contracts/ping-pong-egld/wasm/Cargo.lock b/contracts/ping-pong-egld/wasm/Cargo.lock index a39836ef..9f873830 100644 --- a/contracts/ping-pong-egld/wasm/Cargo.lock +++ b/contracts/ping-pong-egld/wasm/Cargo.lock @@ -38,14 +38,25 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "multiversx-chain-core" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c483122550dca9dd4286f970000d855e9d0184e4fe5c5527209a131ac1207638" +dependencies = [ + "bitflags", + "multiversx-sc-codec", +] + [[package]] name = "multiversx-sc" -version = "0.53.2" +version = "0.54.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ea89a26f0aacda21437a8ae5ccfbefab99d8191942b3d2eddbcbf84f9866d7" +checksum = "ef92d6ad383937403c2055b273153d8d487d7535e4882c614626d0bc2e55ae03" dependencies = [ "bitflags", "hex-literal", + "multiversx-chain-core", "multiversx-sc-codec", "multiversx-sc-derive", "num-traits", @@ -54,9 +65,9 @@ dependencies = [ [[package]] name = "multiversx-sc-codec" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d7a5a8534e5dc9128cb8f15a65a21dd378e135c6016c7cd1491cd012bc8cb" +checksum = "2b72f7f6a3e0828c5e38fe0e98298706837f8337fe06bca3ff21df22c84b6256" dependencies = [ "arrayvec", "multiversx-sc-codec-derive", @@ -65,9 +76,9 @@ dependencies = [ [[package]] name = "multiversx-sc-codec-derive" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffba1dce273ed5b61ee1b90aeea5c8c744617d0f12624f620768c144d83e753" +checksum = "23b915f0f463fd1811de124b51d68cb2067f5a4796cde2a9570316780cf18961" dependencies = [ "hex", "proc-macro2", @@ -77,9 +88,9 @@ dependencies = [ [[package]] name = "multiversx-sc-derive" -version = "0.53.2" +version = "0.54.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c17fdf90fafca2f19085ae67b0502d9f71bf8ab1be3c83808eb88e02a8c18b9" +checksum = "e823a3b80b6a864ad992c22808e851dacee4387e6d22006bdb1bd9de32f7c9d8" dependencies = [ "hex", "proc-macro2", @@ -90,9 +101,9 @@ dependencies = [ [[package]] name = "multiversx-sc-wasm-adapter" -version = "0.53.2" +version = "0.54.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20659915a4377d375c46d7f237e810053a03f7e084fad6362dd5748a7233defb" +checksum = "d780e724ea8262df19e49cf3c06bab8716b6508e785809dc770747e9341282a8" dependencies = [ "multiversx-sc", ] diff --git a/contracts/ping-pong-egld/wasm/Cargo.toml b/contracts/ping-pong-egld/wasm/Cargo.toml index d130e245..b25e1086 100644 --- a/contracts/ping-pong-egld/wasm/Cargo.toml +++ b/contracts/ping-pong-egld/wasm/Cargo.toml @@ -1,3 +1,9 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + [package] name = "ping-pong-egld-wasm" version = "0.0.0" diff --git a/contracts/ping-pong-egld/wasm/src/lib.rs b/contracts/ping-pong-egld/wasm/src/lib.rs index b7f15fc3..ece506bf 100644 --- a/contracts/ping-pong-egld/wasm/src/lib.rs +++ b/contracts/ping-pong-egld/wasm/src/lib.rs @@ -5,9 +5,10 @@ //////////////////////////////////////////////////// // Init: 1 -// Endpoints: 10 +// Upgrade: 1 +// Endpoints: 11 // Async Callback (empty): 1 -// Total number of exported functions: 12 +// Total number of exported functions: 14 #![no_std] @@ -18,10 +19,12 @@ multiversx_sc_wasm_adapter::endpoints! { ping_pong_egld ( init => init + upgrade => upgrade ping => ping pong => pong pongAll => pong_all getUserAddresses => get_user_addresses + getContractState => get_contract_state getPingAmount => ping_amount getDeadline => deadline getActivationTimestamp => activation_timestamp