diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 04bb65a..80b4d85 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -13,10 +13,9 @@ permissions: jobs: contracts: name: Contracts - uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@72dc7659e6945c8749d01ec28638843bae33437e + uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v4.2.0 with: rust-toolchain: stable - sc-meta-hash-git: ca051fc3d0c03ec1ce5624cf81bdada57b5fc3e2 mx-scenario-go-version: v3.0.0 enable-contracts-size-report: false secrets: diff --git a/Cargo.toml b/Cargo.toml index 81d78ac..fe0ec31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["ping-pong", "ping-pong/meta"] +members = ["ping-pong", "ping-pong/meta", "ping-pong/interactor"] diff --git a/ping-pong/interaction/Ping-pong.erdjs.md b/ping-pong/interaction/Ping-pong.erdjs.md deleted file mode 100644 index 0f97357..0000000 --- a/ping-pong/interaction/Ping-pong.erdjs.md +++ /dev/null @@ -1,23 +0,0 @@ -# Ping-pong - -First [set up a node terminal](../../../../tutorial/src/interaction/interaction-basic.md). - -```javascript -let erdjs = await require('@elrondnetwork/erdjs'); -let { erdSys, Egld, wallets: { alice, bob, carol, dan } } = await erdjs.setupInteractive("local-testnet"); - -let pingPong = await erdSys.loadWrapper("contracts/examples/ping-pong"); - -await pingPong.sender(alice).gas(150_000_000).call.deploy(Egld(0.5), 2 * 60, null, Egld(1.5)); - -await pingPong.gas(20_000_000).sender(alice).value(Egld(0.5)).ping("note 1"); - -await pingPong.sender(bob).value(Egld(0.5)).ping(null); -await pingPong.sender(carol).value(Egld(0.5)).ping(null); - -// this fails because of the balance limit of 1.5 egld -await pingPong.sender(dan).value(Egld(0.5).ping(null); - -await pingPong.pongAll(); - -``` diff --git a/ping-pong/interaction/out.json b/ping-pong/interaction/out.json deleted file mode 100644 index e69de29..0000000 diff --git a/ping-pong/interaction/snippets.sh b/ping-pong/interaction/snippets.sh deleted file mode 100755 index 087f23c..0000000 --- a/ping-pong/interaction/snippets.sh +++ /dev/null @@ -1,39 +0,0 @@ -PEM_FILE="./ping-pong.pem" -PING_PONG_CONTRACT="output/ping-pong.wasm" - -PROXY_ARGUMENT="--proxy=https://devnet-api.multiversx.com" -CHAIN_ARGUMENT="--chain=D" - -build_ping_pong() { - (set -x; mxpy --verbose contract build "$PING_PONG_CONTRACT") -} - -deploy_ping_pong() { - # local TOKEN_ID=0x45474c44 # "EGLD" - local PING_AMOUNT=1500000000000000000 # 1.5 EGLD - local DURATION=86400 # 1 day in seconds - # local ACTIVATION_TIMESTAMP= # skipped - # local MAX_FUNDS= #skipped - - local OUTFILE="out.json" - (set -x; mxpy contract deploy --bytecode="$PING_PONG_CONTRACT" \ - --pem="$PEM_FILE" \ - $PROXY_ARGUMENT $CHAIN_ARGUMENT \ - --outfile="$OUTFILE" --recall-nonce --gas-limit=60000000 \ - --arguments ${PING_AMOUNT} ${DURATION} --send \ - || return) - - local RESULT_ADDRESS=$(mxpy data parse --file="$OUTFILE" --expression="data['emitted_tx']['address']") - local RESULT_TRANSACTION=$(mxpy data parse --file="$OUTFILE" --expression="data['emitted_tx']['hash']") - - echo "" - echo "Deployed contract with:" - echo " \$RESULT_ADDRESS == ${RESULT_ADDRESS}" - echo " \$RESULT_TRANSACTION == ${RESULT_TRANSACTION}" - echo "" -} - -number_to_u64() { - local NUMBER=$1 - printf "%016x" $NUMBER -} diff --git a/ping-pong/interactor/.gitignore b/ping-pong/interactor/.gitignore new file mode 100644 index 0000000..e4741b7 --- /dev/null +++ b/ping-pong/interactor/.gitignore @@ -0,0 +1,5 @@ +# Pem files are used for interactions, but shouldn't be committed +*.pem + +# Temporary storage of deployed contract address, so we can preserve the context between executions. +state.toml \ No newline at end of file diff --git a/ping-pong/interactor/Cargo.toml b/ping-pong/interactor/Cargo.toml new file mode 100644 index 0000000..00a33a0 --- /dev/null +++ b/ping-pong/interactor/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ping-pong-interact" +version = "0.0.0" +authors = ["you"] +edition = "2021" +publish = false + +[[bin]] +name = "ping-pong-interact" +path = "src/interact_main.rs" + +[lib] +path = "src/interact.rs" + +[dependencies.ping-pong] +path = ".." + +[dependencies.multiversx-sc-snippets] +version = "0.54.6" + +[dependencies.multiversx-sc] +version = "0.54.6" + +[dependencies] +clap = { version = "4.4.7", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.6" +tokio = { version = "1.24" } + +[features] +chain-simulator-tests = [] diff --git a/ping-pong/interactor/config.toml b/ping-pong/interactor/config.toml new file mode 100644 index 0000000..97acd5a --- /dev/null +++ b/ping-pong/interactor/config.toml @@ -0,0 +1,7 @@ + +# chain_type = 'simulator' +# gateway_uri = 'http://localhost:8085' + +chain_type = 'real' +gateway_uri = 'https://devnet-gateway.multiversx.com' + diff --git a/ping-pong/interactor/src/interact.rs b/ping-pong/interactor/src/interact.rs new file mode 100644 index 0000000..8411035 --- /dev/null +++ b/ping-pong/interactor/src/interact.rs @@ -0,0 +1,326 @@ +mod interact_cli; +mod interact_config; +mod interact_state; +mod ping_pong_proxy; + +use clap::Parser; +pub use interact_config::Config; +use interact_state::State; +use multiversx_sc_snippets::imports::*; + +const PING_PONG_CODE: MxscPath = MxscPath::new("output/ping-pong.mxsc.json"); + +pub async fn ping_pong_cli() { + env_logger::init(); + + let config = Config::load_config(); + + let mut interact = PingPongInteract::new(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.token_id.clone(), + ) + .await; + } + Some(interact_cli::InteractCliCommand::Upgrade(args)) => { + interact + .upgrade(args.ping_amount.clone(), args.duration_in_seconds) + .await; + } + Some(interact_cli::InteractCliCommand::Ping(args)) => { + interact + .ping( + &args.token, + args.nonce, + args.amount, + &interact.alice_wallet_address.clone(), + None, + ) + .await; + } + Some(interact_cli::InteractCliCommand::Pong) => { + interact + .pong(&interact.alice_wallet_address.clone(), None) + .await; + } + Some(interact_cli::InteractCliCommand::DidUserPing(args)) => { + let address = Bech32Address::from_bech32_string(args.address.clone()); + interact.did_user_ping(address).await; + } + Some(interact_cli::InteractCliCommand::GetPongEnableTimestamp(args)) => { + let address = Bech32Address::from_bech32_string(args.address.clone()); + interact.get_pong_enable_timestamp(address).await; + } + Some(interact_cli::InteractCliCommand::GetTimeToPong(args)) => { + let address = Bech32Address::from_bech32_string(args.address.clone()); + interact.get_time_to_pong(address).await; + } + Some(interact_cli::InteractCliCommand::GetAcceptedPaymentToken) => { + interact.accepted_payment_token_id().await; + } + Some(interact_cli::InteractCliCommand::GetPingAmount) => { + interact.ping_amount().await; + } + Some(interact_cli::InteractCliCommand::GetDurationTimestamp) => { + interact.duration_in_seconds().await; + } + Some(interact_cli::InteractCliCommand::GetUserPingTimestamp(args)) => { + let address = Bech32Address::from_bech32_string(args.address.clone()); + interact.user_ping_timestamp(address).await; + } + None => {} + } +} + +pub struct PingPongInteract { + pub interactor: Interactor, + pub alice_wallet_address: Bech32Address, + pub mike_wallet_address: Bech32Address, + pub state: State, +} + +impl PingPongInteract { + pub async fn new(config: Config) -> Self { + let mut interactor = Interactor::new(config.gateway_uri()) + .await + .use_chain_simulator(config.use_chain_simulator()); + + interactor.set_current_dir_from_workspace("ping-pong"); + let alice_wallet_address = interactor.register_wallet(test_wallets::alice()).await; + let mike_wallet_address = interactor.register_wallet(test_wallets::mike()).await; + + // Useful in the chain simulator setting + // generate blocks until ESDTSystemSCAddress is enabled + interactor.generate_blocks_until_epoch(1).await.unwrap(); + + PingPongInteract { + interactor, + alice_wallet_address: alice_wallet_address.into(), + mike_wallet_address: mike_wallet_address.into(), + state: State::load_state(), + } + } + + pub async fn deploy( + &mut self, + ping_amount: RustBigUint, + duration_in_seconds: u64, + token_id: String, + ) { + let token = if token_id.to_uppercase() == "EGLD" { + EgldOrEsdtTokenIdentifier::egld() + } else { + EgldOrEsdtTokenIdentifier::esdt(&token_id) + }; + + let new_address = self + .interactor + .tx() + .from(&self.alice_wallet_address) + .gas(30_000_000u64) + .typed(ping_pong_proxy::PingPongProxy) + .init(ping_amount, duration_in_seconds, OptionalValue::Some(token)) + .code(PING_PONG_CODE) + .returns(ReturnsNewAddress) + .run() + .await; + let new_address_bech32 = bech32::encode(&new_address); + self.state + .set_ping_pong_address(Bech32Address::from_bech32_string( + new_address_bech32.clone(), + )); + + println!("new address: {new_address_bech32}"); + } + + pub async fn upgrade(&mut self, ping_amount: RustBigUint, duration_in_seconds: u64) { + let upgrade_address = self + .interactor + .tx() + .from(&self.alice_wallet_address) + .to(self.state.current_ping_pong_address()) + .gas(30_000_000u64) + .typed(ping_pong_proxy::PingPongProxy) + .upgrade(ping_amount, duration_in_seconds) + .code(PING_PONG_CODE) + .returns(ReturnsNewAddress) + .run() + .await; + + let upgrade_address_bech32 = bech32::encode(&upgrade_address); + self.state + .set_ping_pong_address(Bech32Address::from_bech32_string( + upgrade_address_bech32.clone(), + )); + + println!("new upgrade address: {upgrade_address_bech32}"); + } + + pub async fn ping( + &mut self, + token_id: &str, + nonce: Option, + amount: u64, + sender: &Bech32Address, + message: Option<&str>, + ) { + let response = if token_id.to_ascii_uppercase() == "EGLD" { + self.interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_address()) + .gas(30_000_000u64) + .typed(ping_pong_proxy::PingPongProxy) + .ping() + .egld(amount) + .returns(ReturnsHandledOrError::new()) + .run() + .await + } else { + self.interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_address()) + .gas(30_000_000u64) + .typed(ping_pong_proxy::PingPongProxy) + .ping() + .payment(( + TokenIdentifier::from(&token_id.to_uppercase()), + nonce.unwrap(), + BigUint::from(amount), + )) + .returns(ReturnsHandledOrError::new()) + .run() + .await + }; + + match response { + Ok(_) => println!("Ping successfully executed"), + Err(err) => { + println!("Ping failed with message: {}", err.message); + assert_eq!(message.unwrap_or_default(), err.message); + } + } + } + + pub async fn pong(&mut self, sender: &Bech32Address, message: Option<&str>) { + let response = self + .interactor + .tx() + .from(sender) + .to(self.state.current_ping_pong_address()) + .gas(30_000_000u64) + .typed(ping_pong_proxy::PingPongProxy) + .pong() + .returns(ReturnsHandledOrError::new()) + .run() + .await; + + match response { + Ok(_) => println!("Pong successfully executed"), + Err(err) => { + println!("Pong failed with message: {}", err.message); + assert_eq!(message.unwrap_or_default(), err.message); + } + } + } + + pub async fn did_user_ping(&mut self, address: Bech32Address) -> bool { + self.interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .did_user_ping(address) + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_pong_enable_timestamp(&mut self, address: Bech32Address) -> u64 { + self.interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .get_pong_enable_timestamp(address) + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn get_time_to_pong(&mut self, address: Bech32Address) -> Option { + let result_value = self + .interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .get_time_to_pong(address) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + match result_value { + OptionalValue::Some(time) => Some(time), + OptionalValue::None => { + println!("Address unavailable"); + None + } + } + } + + pub async fn accepted_payment_token_id(&mut self) -> String { + let result_value = self + .interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .accepted_payment_token_id() + .returns(ReturnsResultUnmanaged) + .run() + .await; + + if result_value.is_egld() { + return "EGLD".to_owned(); + } + + result_value.into_esdt_option().unwrap().to_string() + } + + pub async fn ping_amount(&mut self) -> RustBigUint { + self.interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .ping_amount() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn duration_in_seconds(&mut self) -> u64 { + self.interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .duration_in_seconds() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + + pub async fn user_ping_timestamp(&mut self, address: Bech32Address) -> u64 { + self.interactor + .query() + .to(self.state.current_ping_pong_address()) + .typed(ping_pong_proxy::PingPongProxy) + .user_ping_timestamp(address) + .returns(ReturnsResultUnmanaged) + .run() + .await + } +} diff --git a/ping-pong/interactor/src/interact_cli.rs b/ping-pong/interactor/src/interact_cli.rs new file mode 100644 index 0000000..00adcfd --- /dev/null +++ b/ping-pong/interactor/src/interact_cli.rs @@ -0,0 +1,104 @@ +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(UpgradeArgs), + #[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 = "did-user-ping", about = "Returns if a user ping-ed or not")] + DidUserPing(DidUserPingArgs), + #[command( + name = "pong-enable", + about = "Returns the timestamp when pong is enabled." + )] + GetPongEnableTimestamp(GetPongEnableTimestampArgs), + #[command(name = "time-to-pong", about = "Returns the time left to pong.")] + GetTimeToPong(GetTimeToPongArgs), + #[command(name = "token", about = "Returns accepted token to ping.")] + GetAcceptedPaymentToken, + #[command(name = "ping-amount", about = "Returns the ping amount.")] + GetPingAmount, + #[command(name = "duration", about = "Returns the duration in seconds.")] + GetDurationTimestamp, + #[command( + name = "user-ping", + about = "Returns the timestamp at which the user pinged" + )] + GetUserPingTimestamp(GetUserPingTimestampArgs), +} + +#[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 = 't', long = "token-id", default_value = "EGLD")] + pub token_id: String, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct UpgradeArgs { + #[arg(short = 'p', long = "ping-amount")] + pub ping_amount: RustBigUint, + + #[arg(short = 'd', long = "duration-in-seconds")] + pub duration_in_seconds: u64, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct PingArgs { + #[arg(short = 't', long = "token")] + pub token: String, + + #[arg(short = 'n', long = "nonce")] + pub nonce: Option, + + #[arg(short = 'a', long = "amount")] + pub amount: u64, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct DidUserPingArgs { + #[arg(short = 'a', long = "address")] + pub address: String, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct GetPongEnableTimestampArgs { + #[arg(short = 'a', long = "address")] + pub address: String, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct GetTimeToPongArgs { + #[arg(short = 'a', long = "address")] + pub address: String, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct GetUserPingTimestampArgs { + #[arg(short = 'a', long = "address")] + pub address: String, +} diff --git a/ping-pong/interactor/src/interact_config.rs b/ping-pong/interactor/src/interact_config.rs new file mode 100644 index 0000000..bc35101 --- /dev/null +++ b/ping-pong/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, +} + +/// Contract 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/ping-pong/interactor/src/interact_main.rs b/ping-pong/interactor/src/interact_main.rs new file mode 100644 index 0000000..5143ded --- /dev/null +++ b/ping-pong/interactor/src/interact_main.rs @@ -0,0 +1,9 @@ + +use multiversx_sc_snippets::imports::*; +use ping_pong_interact::ping_pong_cli; + +#[tokio::main] +async fn main() { + ping_pong_cli().await; +} + diff --git a/ping-pong/interactor/src/interact_state.rs b/ping-pong/interactor/src/interact_state.rs new file mode 100644 index 0000000..74694c4 --- /dev/null +++ b/ping-pong/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_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_address(&mut self, address: Bech32Address) { + self.ping_pong_address = Some(address); + } + + /// Returns the ping pong contract + pub fn current_ping_pong_address(&self) -> &Bech32Address { + self.ping_pong_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/ping-pong/interactor/src/ping_pong_proxy.rs b/ping-pong/interactor/src/ping_pong_proxy.rs new file mode 100644 index 0000000..5e67e29 --- /dev/null +++ b/ping-pong/interactor/src/ping_pong_proxy.rs @@ -0,0 +1,203 @@ +// 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 amount that needs to be sent when `ping`-ing. + /// `duration_in_seconds` - how much time (in seconds) until `pong` can be called after the initial `ping` call + /// `token_id` - Optional. The Token Identifier of the token that is going to be used. Default is "EGLD". + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg, + Arg2: ProxyArg>>, + >( + self, + ping_amount: Arg0, + duration_in_seconds: Arg1, + opt_token_id: Arg2, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&ping_amount) + .argument(&duration_in_seconds) + .argument(&opt_token_id) + .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, + >( + self, + ping_amount: Arg0, + duration_in_seconds: Arg1, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .argument(&ping_amount) + .argument(&duration_in_seconds) + .original_result() + } +} + +#[rustfmt::skip] +impl PingPongProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + /// User sends some tokens to be locked in the contract for a period of time. + pub fn ping( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("ping") + .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() + } + + pub fn did_user_ping< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("didUserPing") + .argument(&address) + .original_result() + } + + pub fn get_pong_enable_timestamp< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getPongEnableTimestamp") + .argument(&address) + .original_result() + } + + pub fn get_time_to_pong< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getTimeToPong") + .argument(&address) + .original_result() + } + + pub fn accepted_payment_token_id( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getAcceptedPaymentToken") + .original_result() + } + + pub fn ping_amount( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getPingAmount") + .original_result() + } + + pub fn duration_in_seconds( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getDurationTimestamp") + .original_result() + } + + pub fn user_ping_timestamp< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getUserPingTimestamp") + .argument(&address) + .original_result() + } +} diff --git a/ping-pong/interactor/tests/interact_cs_tests.rs b/ping-pong/interactor/tests/interact_cs_tests.rs new file mode 100644 index 0000000..bb6a3aa --- /dev/null +++ b/ping-pong/interactor/tests/interact_cs_tests.rs @@ -0,0 +1,40 @@ +use multiversx_sc_snippets::imports::*; +use ping_pong_interact::{Config, PingPongInteract}; + +const EGLD: &str = "EGLD"; + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_pong_cs() { + let mut interactor = PingPongInteract::new(Config::chain_simulator_config()).await; + + let alice = interactor.alice_wallet_address.clone(); + let mike = interactor.mike_wallet_address.clone(); + let amount = RustBigUint::from(1u32); + let time = 15u64; + + interactor.deploy(amount, time, EGLD.to_string()).await; + + interactor + .ping( + EGLD, + None, + 2, + &alice, + Some("The payment must match the fixed ping amount"), + ) + .await; + interactor.ping(EGLD, None, 1, &alice, None).await; + assert!(interactor.did_user_ping(alice.clone()).await); + + assert!(!interactor.did_user_ping(mike.clone()).await); + interactor.ping(EGLD, None, 1, &mike, None).await; + + assert_eq!(Some(15), interactor.get_time_to_pong(mike.clone()).await); + assert_eq!(EGLD, interactor.accepted_payment_token_id().await); + assert_eq!(RustBigUint::from(1u64), interactor.ping_amount().await); + assert_eq!(time, interactor.duration_in_seconds().await); + + interactor.pong(&alice, None).await; + interactor.pong(&alice, Some("Must ping first")).await; +} diff --git a/ping-pong/sc-config.toml b/ping-pong/sc-config.toml new file mode 100644 index 0000000..109751f --- /dev/null +++ b/ping-pong/sc-config.toml @@ -0,0 +1,3 @@ + +[[proxy]] +path = "interactor/src/ping_pong_proxy.rs"