diff --git a/Cargo.lock b/Cargo.lock index d9dedaf7f1..aa580de334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2860,6 +2860,21 @@ dependencies = [ "multiversx-sc-meta-lib", ] +[[package]] +name = "lottery-interactor" +version = "0.0.0" +dependencies = [ + "clap", + "lottery-esdt", + "multiversx-sc", + "multiversx-sc-snippets", + "serde", + "serde_json", + "serial_test", + "tokio", + "toml 0.8.23", +] + [[package]] name = "loupe" version = "0.1.3" @@ -5449,7 +5464,19 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -5498,6 +5525,20 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.13", +] + [[package]] name = "toml_parser" version = "1.0.4" @@ -5507,6 +5548,12 @@ dependencies = [ "winnow 0.7.13", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.4" @@ -6367,6 +6414,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 155bffb935..10b1f2a549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ members = [ "contracts/examples/fractional-nfts/meta", "contracts/examples/lottery-esdt", "contracts/examples/lottery-esdt/meta", + "contracts/examples/lottery-esdt/interactor", "contracts/examples/multisig", "contracts/examples/multisig/meta", "contracts/examples/multisig/interact", diff --git a/contracts/examples/lottery-esdt/interactor/.gitignore b/contracts/examples/lottery-esdt/interactor/.gitignore new file mode 100644 index 0000000000..88af50ac47 --- /dev/null +++ b/contracts/examples/lottery-esdt/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 diff --git a/contracts/examples/lottery-esdt/interactor/Cargo.toml b/contracts/examples/lottery-esdt/interactor/Cargo.toml new file mode 100644 index 0000000000..ce0d2fbeee --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "lottery-interactor" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" +publish = false + +[[bin]] +name = "lottery-interactor" +path = "src/lottery_interactor_main.rs" + +[lib] +path = "src/lottery_interactor.rs" + +[dependencies.lottery-esdt] +path = ".." + +[dependencies.multiversx-sc-snippets] +version = "0.62.1" +path = "../../../../framework/snippets" + +[dependencies.multiversx-sc] +version = "=0.62.1" +path = "../../../../framework/base" + +[dependencies] +clap = { version = "4.4.7", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.6" +serde_json = "1.0" +tokio = { version = "1.24" } +serial_test = { version = "3.2.0" } + +[features] +chain-simulator-tests = [] diff --git a/contracts/examples/lottery-esdt/interactor/config.toml b/contracts/examples/lottery-esdt/interactor/config.toml new file mode 100644 index 0000000000..a8e45600b0 --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/config.toml @@ -0,0 +1,6 @@ + +# chain_type = 'simulator' +# gateway_uri = 'http://localhost:8085' + +chain_type = 'real' +gateway_uri = 'https://devnet-gateway.multiversx.com' diff --git a/contracts/examples/lottery-esdt/interactor/set_state.json b/contracts/examples/lottery-esdt/interactor/set_state.json new file mode 100644 index 0000000000..47bd206b5e --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/set_state.json @@ -0,0 +1,89 @@ +[ + { + "address": "erd1uv40ahysflse896x4ktnh6ecx43u7cmy9wnxnvcyp7deg299a4sq6vaywa", + "nonce": 6221, + "balance": "37980784869999986", + "pairs": { + "454c524f4e44657364745453542d343265356138": "1209004563918244f40000", + "454c524f4e44657364745453542d393836646663": "12020064", + "454c524f4e4465736474544553542d61366131663601": "080512020001223a080112056d794e46541a20e32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60201e32003a090000000000000002032a0510e2f09003", + "454c524f4e44657364745453542d306632306637": "12020064", + "454c524f4e44657364745453542d656338383735": "12020064", + "454c524f4e4465736474544553542d65373261666101": "080512020001223a080112056d794e46541a200139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1201e32003a09000000000000000203", + "454c524f4e44657364745453542d303362373664": "12020064", + "454c524f4e44726f6c6565736474544553542d396262623231": "0a1545534454526f6c654d6f6469667943726561746f72", + "454c524f4e4465736474544553542d39626262323101": "080512020001223a080112056d794e46541a20e32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60201e32003a090000000000000002032a0510f9f5ae03", + "454c524f4e44657364745453542d336339363762": "12020064", + "454c524f4e44657364745453542d343562383235": "12020064", + "454c524f4e44657364745453542d363437383930": "1209004563918244f40000", + "454c524f4e4465736474424358535542542d33393264366172": "080112020001", + "454c524f4e44726f6c656573647450544d2d353336666162": "0a1145534454526f6c654e46544372656174650a0f45534454526f6c654e46544275726e", + "454c524f4e44657364744c5453542d346638343965": "1209000de0b6b3a763fc19", + "454c524f4e44657364745453542d353966316165": "1209004563918244f40000", + "454c524f4e44657364745453542d343138613232": "1209004563918244f40000", + "454c524f4e44657364745453542d363434633935": "12020064", + "454c524f4e44657364745453542d633636666535": "1209004563918244f40000", + "454c524f4e44657364745453542d643862306438": "12020064", + "454c524f4e44657364745453542d333639646531": "1209004563918244f40000", + "454c524f4e44657364744c5453542d376266336431": "1209000de0b6b3a763fc19", + "454c524f4e44657364745453542d643964336136": "1209004563918244f40000", + "454c524f4e44657364745453542d393864633566": "1209004563918244f40000", + "454c524f4e4465736474475245454e2d306531363163": "120b00152d02c7e14af67fffdc", + "454c524f4e44657364745453542d353538616434": "12020064", + "454c524f4e44657364745453542d623136363735": "1209004563918244f40000", + "454c524f4e446573647450544d2d35333666616201": "08021202000122ef0108011212546573742d5061696e742d486172766573741a20e32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed6020c4132a2e516d57564239575362674b52655a64615a434344766b454b70705a6b4d696d397563736e7857565041414c6a4374324368747470733a2f2f697066732e696f2f697066732f516d57564239575362674b52655a64615a434344766b454b70705a6b4d696d397563736e7857565041414c6a43743a3d746167733a3b6d657461646174613a516d52635039346b5872357a5a6a52477669376d4a36756e374c7078556859565234523452706963787a67596b74", + "454c524f4e44657364745453542d303637373232": "1209004563918244f40000", + "454c524f4e44657364745453542d396230323030": "1209004563918244f40000", + "454c524f4e44657364745453542d623830663863": "1209004563918244f40000", + "454c524f4e446573647455544b2d313464353764": "120b0001e6ce88d5ebbfd00000", + "454c524f4e44657364745453542d363835303064": "1209004563918244f40000", + "454c524f4e44657364745453542d373639313337": "1209004563918244f40000", + "454c524f4e44657364745453542d613562663131": "12020064", + "454c524f4e44657364745453542d386564363538": "1209004563918244f40000", + "454c524f4e44657364745453542d333331386638": "1209004563918244f40000", + "454c524f4e44657364745745474c442d613238633539": "120900389351ce08f09e12", + "454c524f4e4465736474544553542d393236313861": "1202005a", + "454c524f4e44657364745453542d346634303238": "12020064", + "454c524f4e44726f6c6565736474544553542d613661316636": "0a1545534454526f6c654d6f6469667943726561746f72", + "454c524f4e44657364745453542d346230653865": "1209004563918244f40000", + "454c524f4e44657364745453542d623130616461": "1209004563918244f40000", + "454c524f4e4465736474544553542d326130616532": "12020064", + "454c524f4e446e6f6e636550544d2d353336666162": "01", + "454c524f4e44657364745453542d323833633361": "12020064", + "454c524f4e44657364745453542d633565303835": "1209004563918244f40000", + "454c524f4e44657364745453542d633933336139": "1209004563918244f40000" + }, + "code": "", + "code_hash": "", + "root_hash": "bm7koGXVtATCN5jJdsU2nmEx9MQGQ3Szb9Gq/Yb7Di0=", + "code_metadata": "", + "owner_address": "", + "developer_reward": "0" + }, + { + "address": "erd13x29rvmp4qlgn4emgztd8jgvyzdj0p6vn37tqxas3v9mfhq4dy7shalqrx", + "nonce": 1550, + "balance": "4950745448587014056", + "pairs": { + "454c524f4e446573647445564e544e4f544946592d393634383835": "120b00152d02c7e14af6800000", + "454c524f4e44657364744e4943552d393730323932": "120b00d3c21bcecceda1000000", + "454c524f4e4465736474424358535542542d3339326436616e": "080112020001", + "454c524f4e4465736474494e5445524e532d63393332356601": "0801120b0013097d1fb962e12fff47", + "454c524f4e446573647442534b2d343736343730": "120b00021e19e0c9bab23fff7b", + "454c524f4e44657364744e455453432d623635306261": "120b00d137965aa7a731800000", + "454c524f4e446e6f6e6365494e5445524e532d633933323566": "01", + "454c524f4e44726f6c6565736474494e5445524e532d633933323566": "0a1145534454526f6c654e46544372656174650a1645534454526f6c654e46544164645175616e74697479", + "454c524f4e44657364744e45543253432d306438663962": "120f0004ee2d6d3f3d6bcc25c64dc00000", + "454c524f4e4465736474424358535542542d3339326436616c": "080112020001", + "454c524f4e44657364745745474c442d613238633539": "120800010593b233281b", + "454c524f4e446e6f6e63654d4554414e46542d643062623339": "01", + "454c524f4e44726f6c65657364744d4554414e46542d643062623339": "0a1145534454526f6c654e4654437265617465" + }, + "code": "", + "code_hash": "", + "root_hash": "AJ2jyOcPXgZAl0kHAlbWZIlG3F1VDtcoLAHR6eqehBA=", + "code_metadata": "", + "owner_address": "", + "developer_reward": "0" + } +] \ No newline at end of file diff --git a/contracts/examples/lottery-esdt/interactor/src/lottery_interactor.rs b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor.rs new file mode 100644 index 0000000000..65f75ffadc --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor.rs @@ -0,0 +1,230 @@ +mod lottery_interactor_cli; +mod lottery_interactor_config; +mod lottery_interactor_state; + +use clap::Parser; +use lottery_esdt::lottery_proxy; +pub use lottery_interactor_config::Config; +use lottery_interactor_state::State; + +use multiversx_sc_snippets::imports::*; + +const LOTTERY_CODE_PATH: MxscPath = MxscPath::new("../output/lottery-esdt.mxsc.json"); + +pub async fn lottery_cli() { + env_logger::init(); + + let config = Config::load_config(); + + let mut lottery_interact = LotteryInteract::new(config).await; + + let cli = lottery_interactor_cli::InteractCli::parse(); + match &cli.command { + Some(lottery_interactor_cli::InteractCliCommand::Deploy) => { + lottery_interact.deploy().await; + } + Some(lottery_interactor_cli::InteractCliCommand::CreateLotteryPool(args)) => { + lottery_interact + .create_lottery_pool( + &args.lottery_name, + &args.token_identifier, + args.ticket_price, + args.opt_total_tickets, + args.opt_deadline, + args.opt_max_entries_per_user, + args.opt_prize_distribution.clone(), + args.get_opt_whitelist_arg(), + OptionalValue::from(args.opt_burn_percentage), + ) + .await; + } + Some(lottery_interactor_cli::InteractCliCommand::BuyTicket(args)) => { + let caller = Bech32Address::from_bech32_string(args.caller.clone()); + lottery_interact.buy_ticket(&caller, &args.name).await; + } + Some(lottery_interactor_cli::InteractCliCommand::DetermineWinner(args)) => { + let caller = Bech32Address::from_bech32_string(args.caller.clone()); + lottery_interact + .determine_winner(&caller, &args.name, None) + .await; + } + Some(lottery_interactor_cli::InteractCliCommand::ClaimRewards(args)) => { + lottery_interact + .claim_rewards(args.tokens.iter().map(TokenIdentifier::from).collect()) + .await; + } + None => {} + } +} + +#[derive(Clone)] +pub struct AddressWithShard { + pub address: Bech32Address, + pub shard: u8, +} + +pub struct LotteryInteract { + pub interactor: Interactor, + pub lottery_owner: AddressWithShard, + pub account_1: AddressWithShard, + pub account_2: AddressWithShard, + pub other_shard_account: AddressWithShard, + pub state: State, +} + +impl LotteryInteract { + 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("contracts/examples/lottery-esdt/interactor"); + + let lottery_owner_wallet = test_wallets::heidi(); // shard 1 + let account_1_wallet = test_wallets::carol(); // shard 0 + let account_2_wallet = test_wallets::bob(); // shard 2 + let other_shard_wallet = test_wallets::alice(); // shard 0 + + let lottery_owner_address = interactor.register_wallet(lottery_owner_wallet).await; + let account_1_address = interactor.register_wallet(account_1_wallet).await; + let account_2_address = interactor.register_wallet(account_2_wallet).await; + let other_shard_address = interactor.register_wallet(other_shard_wallet).await; + + interactor.generate_blocks(30u64).await.unwrap(); + + LotteryInteract { + interactor, + lottery_owner: AddressWithShard { + address: lottery_owner_address.clone().into(), + shard: lottery_owner_wallet.get_shard(), + }, + account_1: AddressWithShard { + address: account_1_address.into(), + shard: account_1_wallet.get_shard(), + }, + account_2: AddressWithShard { + address: account_2_address.into(), + shard: account_2_wallet.get_shard(), + }, + other_shard_account: AddressWithShard { + address: other_shard_address.into(), + shard: other_shard_wallet.get_shard(), + }, + state: State::load_state(), + } + } + + pub async fn generate_blocks_until_epoch(&mut self, epoch: u64) { + self.interactor + .generate_blocks_until_epoch(epoch) + .await + .unwrap(); + } + + pub async fn deploy(&mut self) { + let new_address = self + .interactor + .tx() + .from(&self.lottery_owner.address) + .gas(50_000_000) + .typed(lottery_proxy::LotteryProxy) + .init() + .code(LOTTERY_CODE_PATH) + .returns(ReturnsNewBech32Address) + .run() + .await; + let shard = self.lottery_owner.shard; // SC shard is always the shard of the address deploying it + println!("new address: {new_address} on shard {shard}"); + self.state.set_lottery_address(new_address); + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_lottery_pool( + &mut self, + lottery_name: &String, + token_identifier: &String, + ticket_price: u128, + opt_total_tickets: Option, + opt_deadline: Option, + opt_max_entries_per_user: Option, + opt_prize_distribution: Option>, + opt_whitelist: Option>, + opt_burn_percentage: OptionalValue, + ) { + self.interactor + .tx() + .from(&self.account_1.address) + .to(self.state.current_lottery_address()) + .gas(6_000_000u64) + .typed(lottery_proxy::LotteryProxy) + .create_lottery_pool( + lottery_name, + TokenIdentifier::from(token_identifier), + ticket_price, + opt_total_tickets, + opt_deadline, + opt_max_entries_per_user, + opt_prize_distribution, + opt_whitelist, + opt_burn_percentage, + ) + .run() + .await; + + println!("Successfully performed create_lottery_poll"); + } + + pub async fn buy_ticket(&mut self, caller: &Bech32Address, lottery_name: &String) { + self.interactor + .tx() + .from(caller) + .to(self.state.current_lottery_address()) + .gas(6_000_000u64) + .typed(lottery_proxy::LotteryProxy) + .buy_ticket(lottery_name) + .run() + .await; + + println!("Successfully performed buy_ticket"); + } + + pub async fn determine_winner( + &mut self, + caller: &Bech32Address, + lottery_name: &String, + error: Option>, + ) { + let tx = self + .interactor + .tx() + .from(caller) + .to(self.state.current_lottery_address()) + .gas(6_000_000u64) + .typed(lottery_proxy::LotteryProxy) + .determine_winner(lottery_name); + + match error { + None => { + tx.returns(ReturnsResultUnmanaged).run().await; + } + Some(expect_error) => { + tx.returns(expect_error).run().await; + } + } + } + + pub async fn claim_rewards( + &mut self, + tokens: MultiValueEncoded>, + ) { + self.interactor + .tx() + .from(&self.account_1.address) + .to(self.state.current_lottery_address()) + .gas(6_000_000u64) + .typed(lottery_proxy::LotteryProxy) + .claim_rewards(tokens) + .run() + .await; + println!("Successfully performed claim_rewards"); + } +} diff --git a/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_cli.rs b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_cli.rs new file mode 100644 index 0000000000..c8bd16c03b --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_cli.rs @@ -0,0 +1,79 @@ +use clap::{Args, Parser, Subcommand}; +use multiversx_sc_snippets::imports::Address; + +/// Lottery Interact CLI +#[derive(Default, PartialEq, Eq, Debug, Parser)] +#[command(version, about)] +#[command(propagate_version = true)] +pub struct InteractCli { + #[command(subcommand)] + pub command: Option, +} + +/// Lottery Interact CLI Commands +#[derive(Clone, PartialEq, Eq, Debug, Subcommand)] +pub enum InteractCliCommand { + #[command(name = "deploy", about = "Deploy contract")] + Deploy, + #[command(name = "create_lottery_pool", about = "Create Lottery Pool")] + CreateLotteryPool(CreateLotteryPollArgs), + #[command(name = "buy_ticket", about = "Buy Ticket")] + BuyTicket(LotteryNameArg), + #[command(name = "determine_winner", about = "Determine Winner")] + DetermineWinner(LotteryNameArg), + #[command(name = "claim_rewards", about = "Claim Rewards")] + ClaimRewards(ClaimRewardsArg), +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct CreateLotteryPollArgs { + /// The value to add + #[arg(short = 'n', long = "name")] + pub lottery_name: String, + #[arg(short = 'n', long = "name")] + pub token_identifier: String, + #[arg(short = 'n', long = "name")] + pub ticket_price: u128, + #[arg(short = 'n', long = "name")] + pub opt_total_tickets: Option, + #[arg(short = 'n', long = "name")] + pub opt_deadline: Option, + #[arg(short = 'n', long = "name")] + pub opt_max_entries_per_user: Option, + #[arg(short = 'n', long = "name")] + pub opt_prize_distribution: Option>, + #[arg(short = 'n', long = "name")] + pub opt_whitelist: Option>, + #[arg(short = 'n', long = "name")] + pub opt_burn_percentage: Option, +} + +impl CreateLotteryPollArgs { + pub fn get_opt_whitelist_arg(&self) -> Option> { + let mut opt_whitelist_with_addresses = Vec::new(); + self.opt_whitelist.as_ref()?; + + for str_address in self.opt_whitelist.as_ref().unwrap() { + opt_whitelist_with_addresses.push(Address::from_slice(str_address.as_bytes())); + } + + Some(opt_whitelist_with_addresses) + } +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct LotteryNameArg { + /// The caller address + #[arg(short = 'c', long = "address")] + pub caller: String, + /// The name of the lottery + #[arg(short = 'n', long = "name")] + pub name: String, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct ClaimRewardsArg { + /// The name of the lottery + #[arg(short = 'n', long = "name")] + pub tokens: Vec, +} diff --git a/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_config.rs b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_config.rs new file mode 100644 index 0000000000..c891d2d1d4 --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_config.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; +use std::io::Read; + +/// Config file +const CONFIG_FILE: &str = "config.toml"; +pub const CHAIN_SIMULATOR_GATEWAY: &str = "http://localhost:8085"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChainType { + Real, + Simulator, +} + +/// Lottery 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: CHAIN_SIMULATOR_GATEWAY.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/examples/lottery-esdt/interactor/src/lottery_interactor_main.rs b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_main.rs new file mode 100644 index 0000000000..cda0392531 --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_main.rs @@ -0,0 +1,6 @@ +extern crate lottery_interactor; + +#[tokio::main] +pub async fn main() { + lottery_interactor::lottery_cli().await; +} diff --git a/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_state.rs b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_state.rs new file mode 100644 index 0000000000..a49256abde --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/src/lottery_interactor_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"; + +/// Lottery Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + lottery_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 lottery address + pub fn set_lottery_address(&mut self, address: Bech32Address) { + self.lottery_address = Some(address); + } + + /// Returns the lottery contract + pub fn current_lottery_address(&self) -> &Bech32Address { + self.lottery_address + .as_ref() + .expect("no known lottery 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/examples/lottery-esdt/interactor/tests/determine_winner_with_caller_shard_check_test.rs b/contracts/examples/lottery-esdt/interactor/tests/determine_winner_with_caller_shard_check_test.rs new file mode 100644 index 0000000000..7d2635e141 --- /dev/null +++ b/contracts/examples/lottery-esdt/interactor/tests/determine_winner_with_caller_shard_check_test.rs @@ -0,0 +1,69 @@ +use lottery_interactor::{Config, LotteryInteract}; +use multiversx_sc_snippets::{imports::*, sdk::gateway::NetworkStatusRequest}; +use serial_test::serial; + +pub const CHAIN_SIMULATOR_GATEWAY: &str = "http://localhost:8085"; +const ONE_MINUTE_IN_SECONDS: u64 = 60; +const LOTTERY_NAME: &str = "LOTTERY"; + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn determine_winner_with_caller_shard_check_test() { + let mut interact = LotteryInteract::new(Config::chain_simulator_config()).await; + + interact.deploy().await; + let current_timestamp = get_current_timestamp().await; + + interact + .create_lottery_pool( + &LOTTERY_NAME.to_string(), + &"LOTTERY-123456".to_string(), + 100u128, + Some(11), + Some(current_timestamp + ONE_MINUTE_IN_SECONDS), + Some(1), + Some(vec![10, 50, 25, 5, 5, 1, 1, 1, 1, 1]), + None, + OptionalValue::None, + ) + .await; + + interact.generate_blocks_until_epoch(5).await; + + // Call `determine_winner` from the same shard as the SC - should fail + interact + .determine_winner( + &interact.lottery_owner.address.clone(), + &LOTTERY_NAME.to_string(), + Some(ExpectError(4, "Caller needs to be on a remote shard")), + ) + .await; + + // Call `determine_winner` from a different shard - should pass + interact + .determine_winner( + &interact.other_shard_account.address.clone(), + &LOTTERY_NAME.to_string(), + None, + ) + .await; + // Call `determine_winner` after awarding ended as a safe check that awarding the same lotteyr cannot be done twice - should fail- + interact + .determine_winner( + &interact.other_shard_account.address.clone(), + &LOTTERY_NAME.to_string(), + Some(ExpectError(4, "Lottery is inactive!")), + ) + .await; +} + +async fn get_current_timestamp() -> u64 { + let blockchain = GatewayHttpProxy::new(CHAIN_SIMULATOR_GATEWAY.to_string()); + + let network_config = blockchain + .http_request(NetworkStatusRequest::default()) + .await + .unwrap(); + network_config.current_block_timestamp +} diff --git a/contracts/examples/lottery-esdt/scenarios/complex-prize-distribution.scen.json b/contracts/examples/lottery-esdt/scenarios/complex-prize-distribution.scen.json index eb3a7927a8..1ed547fc9a 100644 --- a/contracts/examples/lottery-esdt/scenarios/complex-prize-distribution.scen.json +++ b/contracts/examples/lottery-esdt/scenarios/complex-prize-distribution.scen.json @@ -63,12 +63,12 @@ "balance": "60700", "storage": { "str:lotteryInfo|nested:str:lottery_name": { - "0-token_identifier": "nested:str:EGLD", + "0-token_identifier": "nested:str:LOTTERY-123456", "1-ticket_price": "biguint:100", "2-tickets-left": "u32:0", "3-deadline": "u64:123,456", "4-max_entries_per_user": "u32:1", - "5-prize_distribution": "u32:10|u8:50|u8:25|u8:10|u8:5|u8:5|u8:1|u8:1|u8:1|u8:1|u8:1", + "5-prize_distribution": "u32:10|u8:50|u8:25|u8:5|u8:5|u8:1|u8:1|u8:1|u8:1|u8:1", "6-prize_pool": "biguint:60700", "7-unawarded_amount": "biguint:60700" }, diff --git a/contracts/examples/lottery-esdt/src/basics/storage.rs b/contracts/examples/lottery-esdt/src/basics/storage.rs index abe8b04bc4..7bc7934ae2 100644 --- a/contracts/examples/lottery-esdt/src/basics/storage.rs +++ b/contracts/examples/lottery-esdt/src/basics/storage.rs @@ -18,7 +18,7 @@ pub trait StorageModule { #[storage_mapper("indexLastWinner")] fn index_last_winner(&self, lottery_name: &ManagedBuffer) -> SingleValueMapper; - #[storage_mapper("accumulatedRewards")] + #[storage_mapper("userAccumulatedRewards")] fn user_accumulated_token_rewards(&self, user_id: &u64) -> UnorderedSetMapper; #[storage_mapper("numberOfEntriesForUser")] diff --git a/contracts/examples/lottery-esdt/src/lottery.rs b/contracts/examples/lottery-esdt/src/lottery.rs index 4db0845d62..2461941c2c 100644 --- a/contracts/examples/lottery-esdt/src/lottery.rs +++ b/contracts/examples/lottery-esdt/src/lottery.rs @@ -25,4 +25,7 @@ pub trait Lottery: { #[init] fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} } diff --git a/contracts/examples/lottery-esdt/src/lottery_proxy.rs b/contracts/examples/lottery-esdt/src/lottery_proxy.rs index 2385b2799a..f4a4560be8 100644 --- a/contracts/examples/lottery-esdt/src/lottery_proxy.rs +++ b/contracts/examples/lottery-esdt/src/lottery_proxy.rs @@ -53,6 +53,25 @@ where } } +#[rustfmt::skip] +impl LotteryProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade( + self, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .original_result() + } +} + #[rustfmt::skip] impl LotteryProxyMethods where @@ -138,6 +157,43 @@ where .argument(&lottery_name) .original_result() } + + pub fn start_lottery< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + Arg3: ProxyArg>, + Arg4: ProxyArg>, + Arg5: ProxyArg>, + Arg6: ProxyArg>>, + Arg7: ProxyArg>>>, + Arg8: ProxyArg>>, + >( + self, + lottery_name: Arg0, + token_identifier: Arg1, + ticket_price: Arg2, + opt_total_tickets: Arg3, + opt_deadline: Arg4, + opt_max_entries_per_user: Arg5, + opt_prize_distribution: Arg6, + opt_whitelist: Arg7, + opt_burn_percentage: Arg8, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("startLottery") + .argument(&lottery_name) + .argument(&token_identifier) + .argument(&ticket_price) + .argument(&opt_total_tickets) + .argument(&opt_deadline) + .argument(&opt_max_entries_per_user) + .argument(&opt_prize_distribution) + .argument(&opt_whitelist) + .argument(&opt_burn_percentage) + .original_result() + } } #[type_abi] diff --git a/contracts/examples/lottery-esdt/src/specific/award.rs b/contracts/examples/lottery-esdt/src/specific/award.rs index 562e2a4a87..75cbf735fd 100644 --- a/contracts/examples/lottery-esdt/src/specific/award.rs +++ b/contracts/examples/lottery-esdt/src/specific/award.rs @@ -26,29 +26,35 @@ pub trait AwardingModule: views::ViewsModule + storage::StorageModule + utils::U fn handle_awarding(&self, lottery_name: &ManagedBuffer) -> AwardingStatus { if self.total_winning_tickets(lottery_name).is_empty() { - self.prepare_awarding(lottery_name); + return self.prepare_awarding(lottery_name); + } else { + self.distribute_prizes(lottery_name) } - self.distribute_prizes(lottery_name) } - fn prepare_awarding(&self, lottery_name: &ManagedBuffer) { - let mut info = self.lottery_info(lottery_name).get(); + fn prepare_awarding(&self, lottery_name: &ManagedBuffer) -> AwardingStatus { let ticket_holders_mapper = self.ticket_holders(lottery_name); let total_tickets = ticket_holders_mapper.len(); + // the case of no tickets sold if total_tickets == 0 { - return; + self.clear_storage(lottery_name); + return AwardingStatus::Finished; } + let mut info = self.lottery_info(lottery_name).get(); + self.burn_prize_percentage(lottery_name, &mut info); // if there are less tickets than the distributed prize pool, // the 1st place gets the leftover, maybe could split between the remaining // but this is a rare case anyway and it's not worth the overhead - let total_winning_tickets = if total_tickets < info.prize_distribution.len() { + + let available_prizes_number = info.prize_distribution.len(); + let total_winning_tickets = if total_tickets < available_prizes_number { total_tickets } else { - info.prize_distribution.len() + available_prizes_number }; self.total_winning_tickets(lottery_name) @@ -56,6 +62,7 @@ pub trait AwardingModule: views::ViewsModule + storage::StorageModule + utils::U self.index_last_winner(lottery_name).set(1); self.lottery_info(lottery_name).set(info); + self.distribute_prizes(lottery_name) } fn burn_prize_percentage( @@ -87,8 +94,9 @@ pub trait AwardingModule: views::ViewsModule + storage::StorageModule + utils::U let ticket_holders_mapper = self.ticket_holders(lottery_name); let total_tickets = ticket_holders_mapper.len(); - let mut index_last_winner = self.index_last_winner(lottery_name).get(); let total_winning_tickets = self.total_winning_tickets(lottery_name).get(); + + let mut index_last_winner = self.index_last_winner(lottery_name).get(); require!( index_last_winner <= total_winning_tickets, "Awarding has ended" @@ -155,7 +163,7 @@ pub trait AwardingModule: views::ViewsModule + storage::StorageModule + utils::U // distribute to the first place last. Laws of probability say that order doesn't matter. // this is done to mitigate the effects of BigUint division leading to "spare" prize money being left out at times // 1st place will get the spare money instead. - if *index_last_winner <= total_winning_tickets { + if *index_last_winner < total_winning_tickets { let prize = self.calculate_percentage_of( &info.prize_pool, &BigUint::from( diff --git a/contracts/examples/lottery-esdt/wasm/src/lib.rs b/contracts/examples/lottery-esdt/wasm/src/lib.rs index 7becf19438..f3c5f3b4a2 100644 --- a/contracts/examples/lottery-esdt/wasm/src/lib.rs +++ b/contracts/examples/lottery-esdt/wasm/src/lib.rs @@ -5,9 +5,10 @@ //////////////////////////////////////////////////// // Init: 1 +// Upgrade: 1 // Endpoints: 7 // Async Callback (empty): 1 -// Total number of exported functions: 9 +// Total number of exported functions: 10 #![no_std] @@ -18,6 +19,7 @@ multiversx_sc_wasm_adapter::endpoints! { lottery_esdt ( init => init + upgrade => upgrade determine_winner => determine_winner status => status getLotteryInfo => lottery_info